Merge "Add highlight to selected item in editor dropdowns" into ub-contactsdialer-g-dev
diff --git a/Android.mk b/Android.mk
index 9e9a55a..b19df65 100644
--- a/Android.mk
+++ b/Android.mk
@@ -3,7 +3,6 @@
 
 LOCAL_MODULE_TAGS := optional
 
-contacts_common_dir := ../ContactsCommon
 phone_common_dir := ../PhoneCommon
 
 ifeq ($(TARGET_BUILD_APPS),)
@@ -12,9 +11,9 @@
 support_library_root_dir := prebuilts/sdk/current/support
 endif
 
-src_dirs := src $(contacts_common_dir)/src $(phone_common_dir)/src
-res_dirs := res res-aosp $(contacts_common_dir)/res $(contacts_common_dir)/icons/res $(phone_common_dir)/res
-asset_dirs := $(contacts_common_dir)/assets
+src_dirs := src src-bind $(phone_common_dir)/src
+res_dirs := res res-aosp res-icons $(phone_common_dir)/res
+asset_dirs := assets
 
 LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
 LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs)) \
@@ -82,5 +81,5 @@
 
 #########################################################################################
 
-# Use the folloing include to make our test apk.
+# Use the following include to make our test apk.
 include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/assets/licenses.html b/assets/licenses.html
new file mode 100644
index 0000000..c24ed63
--- /dev/null
+++ b/assets/licenses.html
@@ -0,0 +1,247 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta name="viewport" content="width=device-width">
+    <style> body { font-family: sans-serif; } pre { background-color: #eeeeee; padding: 1em; white-space: pre-wrap; word-wrap: break-word; } </style>
+</head>
+<body>
+
+<h3>Notices for: guava, libphonenumber, and libprotobuf-java-nano</h3>
+<pre>
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+</pre>
+
+<h3>Notices for JSR305</h3>
+<pre>
+Copyright (c) 2007-2009, JSR305 expert group
+All rights reserved.
+
+http://www.opensource.org/licenses/bsd-license.php
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the JSR305 expert group nor the names of its
+      contributors may be used to endorse or promote products derived from
+      this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+</pre>
+
+</body>
+</html>
diff --git a/res-icons/mipmap-hdpi/ic_contacts_launcher.png b/res-icons/mipmap-hdpi/ic_contacts_launcher.png
new file mode 100644
index 0000000..86380d1
--- /dev/null
+++ b/res-icons/mipmap-hdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/res-icons/mipmap-hdpi/ic_contacts_launcher_square.png b/res-icons/mipmap-hdpi/ic_contacts_launcher_square.png
new file mode 100644
index 0000000..64eff00
--- /dev/null
+++ b/res-icons/mipmap-hdpi/ic_contacts_launcher_square.png
Binary files differ
diff --git a/res-icons/mipmap-mdpi/ic_contacts_launcher.png b/res-icons/mipmap-mdpi/ic_contacts_launcher.png
new file mode 100644
index 0000000..85132c5
--- /dev/null
+++ b/res-icons/mipmap-mdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/res-icons/mipmap-mdpi/ic_contacts_launcher_square.png b/res-icons/mipmap-mdpi/ic_contacts_launcher_square.png
new file mode 100644
index 0000000..b4ee821
--- /dev/null
+++ b/res-icons/mipmap-mdpi/ic_contacts_launcher_square.png
Binary files differ
diff --git a/res-icons/mipmap-xhdpi/ic_contacts_launcher.png b/res-icons/mipmap-xhdpi/ic_contacts_launcher.png
new file mode 100644
index 0000000..c198749
--- /dev/null
+++ b/res-icons/mipmap-xhdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/res-icons/mipmap-xhdpi/ic_contacts_launcher_square.png b/res-icons/mipmap-xhdpi/ic_contacts_launcher_square.png
new file mode 100644
index 0000000..6feeadf
--- /dev/null
+++ b/res-icons/mipmap-xhdpi/ic_contacts_launcher_square.png
Binary files differ
diff --git a/res-icons/mipmap-xxhdpi/ic_contacts_launcher.png b/res-icons/mipmap-xxhdpi/ic_contacts_launcher.png
new file mode 100644
index 0000000..4fa10a6
--- /dev/null
+++ b/res-icons/mipmap-xxhdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/res-icons/mipmap-xxhdpi/ic_contacts_launcher_square.png b/res-icons/mipmap-xxhdpi/ic_contacts_launcher_square.png
new file mode 100644
index 0000000..01a3fde
--- /dev/null
+++ b/res-icons/mipmap-xxhdpi/ic_contacts_launcher_square.png
Binary files differ
diff --git a/res-icons/mipmap-xxxhdpi/ic_contacts_launcher.png b/res-icons/mipmap-xxxhdpi/ic_contacts_launcher.png
new file mode 100644
index 0000000..10bda63
--- /dev/null
+++ b/res-icons/mipmap-xxxhdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/res-icons/mipmap-xxxhdpi/ic_contacts_launcher_square.png b/res-icons/mipmap-xxxhdpi/ic_contacts_launcher_square.png
new file mode 100644
index 0000000..328e067
--- /dev/null
+++ b/res-icons/mipmap-xxxhdpi/ic_contacts_launcher_square.png
Binary files differ
diff --git a/res/color/popup_menu_color.xml b/res/color/popup_menu_color.xml
new file mode 100644
index 0000000..15588c2
--- /dev/null
+++ b/res/color/popup_menu_color.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false" android:alpha="0.5" android:color="#ff000000"/>
+    <item android:color="#ff000000"/>
+</selector>
\ No newline at end of file
diff --git a/res/color/tab_text_color.xml b/res/color/tab_text_color.xml
new file mode 100644
index 0000000..5ef1fe3
--- /dev/null
+++ b/res/color/tab_text_color.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/actionbar_text_color" android:state_selected="true"/>
+    <item android:color="@color/actionbar_unselected_text_color" />
+</selector>
\ No newline at end of file
diff --git a/res/drawable-hdpi/ic_ab_search.png b/res/drawable-hdpi/ic_ab_search.png
new file mode 100644
index 0000000..d86b219
--- /dev/null
+++ b/res/drawable-hdpi/ic_ab_search.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_add_to_circles_black_24.png b/res/drawable-hdpi/ic_add_to_circles_black_24.png
deleted file mode 100644
index 6eb1fcc..0000000
--- a/res/drawable-hdpi/ic_add_to_circles_black_24.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_arrow_back_24dp.png b/res/drawable-hdpi/ic_arrow_back_24dp.png
new file mode 100644
index 0000000..ddbb2c4
--- /dev/null
+++ b/res/drawable-hdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_business_white_120dp.png b/res/drawable-hdpi/ic_business_white_120dp.png
new file mode 100644
index 0000000..d5942dc
--- /dev/null
+++ b/res/drawable-hdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_call_24dp.png b/res/drawable-hdpi/ic_call_24dp.png
new file mode 100644
index 0000000..4dc5065
--- /dev/null
+++ b/res/drawable-hdpi/ic_call_24dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_call_note_white_24dp.png b/res/drawable-hdpi/ic_call_note_white_24dp.png
new file mode 100644
index 0000000..503e58e
--- /dev/null
+++ b/res/drawable-hdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_call_voicemail_holo_dark.png b/res/drawable-hdpi/ic_call_voicemail_holo_dark.png
new file mode 100644
index 0000000..6d64a36
--- /dev/null
+++ b/res/drawable-hdpi/ic_call_voicemail_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_close_dk.png b/res/drawable-hdpi/ic_close_dk.png
new file mode 100644
index 0000000..9695529
--- /dev/null
+++ b/res/drawable-hdpi/ic_close_dk.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_create_24dp.png b/res/drawable-hdpi/ic_create_24dp.png
new file mode 100644
index 0000000..540ab4d
--- /dev/null
+++ b/res/drawable-hdpi/ic_create_24dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_group_white_24dp.png b/res/drawable-hdpi/ic_group_white_24dp.png
new file mode 100644
index 0000000..017e4bb
--- /dev/null
+++ b/res/drawable-hdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_history_white_drawable_24dp.png b/res/drawable-hdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 0000000..703d30b
--- /dev/null
+++ b/res/drawable-hdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_info_outline_24dp.png b/res/drawable-hdpi/ic_info_outline_24dp.png
new file mode 100644
index 0000000..c7b1113
--- /dev/null
+++ b/res/drawable-hdpi/ic_info_outline_24dp.png
Binary files differ
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_message_24dp.png b/res/drawable-hdpi/ic_message_24dp.png
new file mode 100644
index 0000000..57177b7
--- /dev/null
+++ b/res/drawable-hdpi/ic_message_24dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_person_24dp.png b/res/drawable-hdpi/ic_person_24dp.png
new file mode 100644
index 0000000..56708b0
--- /dev/null
+++ b/res/drawable-hdpi/ic_person_24dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_person_add_24dp.png b/res/drawable-hdpi/ic_person_add_24dp.png
new file mode 100644
index 0000000..10ae5a7
--- /dev/null
+++ b/res/drawable-hdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_person_avatar.png b/res/drawable-hdpi/ic_person_avatar.png
new file mode 100644
index 0000000..2da477e
--- /dev/null
+++ b/res/drawable-hdpi/ic_person_avatar.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_person_white_120dp.png b/res/drawable-hdpi/ic_person_white_120dp.png
new file mode 100644
index 0000000..69e6f98
--- /dev/null
+++ b/res/drawable-hdpi/ic_person_white_120dp.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_scroll_handle.png b/res/drawable-hdpi/ic_scroll_handle.png
new file mode 100644
index 0000000..3aa29b8
--- /dev/null
+++ b/res/drawable-hdpi/ic_scroll_handle.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_videocam.png b/res/drawable-hdpi/ic_videocam.png
new file mode 100644
index 0000000..97905c9
--- /dev/null
+++ b/res/drawable-hdpi/ic_videocam.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_voicemail_avatar.png b/res/drawable-hdpi/ic_voicemail_avatar.png
new file mode 100644
index 0000000..2fb7826
--- /dev/null
+++ b/res/drawable-hdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/res/drawable-hdpi/list_activated_holo.9.png b/res/drawable-hdpi/list_activated_holo.9.png
new file mode 100644
index 0000000..4ea7afa
--- /dev/null
+++ b/res/drawable-hdpi/list_activated_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_background_holo.9.png b/res/drawable-hdpi/list_background_holo.9.png
new file mode 100644
index 0000000..cddf9be
--- /dev/null
+++ b/res/drawable-hdpi/list_background_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_focused_holo.9.png b/res/drawable-hdpi/list_focused_holo.9.png
new file mode 100644
index 0000000..86578be
--- /dev/null
+++ b/res/drawable-hdpi/list_focused_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_longpressed_holo_light.9.png b/res/drawable-hdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 0000000..e9afcc9
--- /dev/null
+++ b/res/drawable-hdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_pressed_holo_light.9.png b/res/drawable-hdpi/list_pressed_holo_light.9.png
new file mode 100644
index 0000000..2054530
--- /dev/null
+++ b/res/drawable-hdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-hdpi/list_title_holo.9.png b/res/drawable-hdpi/list_title_holo.9.png
new file mode 100644
index 0000000..ae93717
--- /dev/null
+++ b/res/drawable-hdpi/list_title_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi/unknown_source.png b/res/drawable-hdpi/unknown_source.png
new file mode 100644
index 0000000..0a8f37d
--- /dev/null
+++ b/res/drawable-hdpi/unknown_source.png
Binary files differ
diff --git a/res/drawable-ldrtl-hdpi/list_background_holo.9.png b/res/drawable-ldrtl-hdpi/list_background_holo.9.png
new file mode 100644
index 0000000..0d80482
--- /dev/null
+++ b/res/drawable-ldrtl-hdpi/list_background_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-hdpi/list_focused_holo.9.png b/res/drawable-ldrtl-hdpi/list_focused_holo.9.png
new file mode 100644
index 0000000..4139942
--- /dev/null
+++ b/res/drawable-ldrtl-hdpi/list_focused_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-hdpi/list_title_holo.9.png b/res/drawable-ldrtl-hdpi/list_title_holo.9.png
new file mode 100644
index 0000000..5ec4c96
--- /dev/null
+++ b/res/drawable-ldrtl-hdpi/list_title_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-mdpi/list_background_holo.9.png b/res/drawable-ldrtl-mdpi/list_background_holo.9.png
new file mode 100644
index 0000000..d86d611
--- /dev/null
+++ b/res/drawable-ldrtl-mdpi/list_background_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-mdpi/list_focused_holo.9.png b/res/drawable-ldrtl-mdpi/list_focused_holo.9.png
new file mode 100644
index 0000000..4139942
--- /dev/null
+++ b/res/drawable-ldrtl-mdpi/list_focused_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-mdpi/list_title_holo.9.png b/res/drawable-ldrtl-mdpi/list_title_holo.9.png
new file mode 100644
index 0000000..013d5e7
--- /dev/null
+++ b/res/drawable-ldrtl-mdpi/list_title_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png b/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png
new file mode 100644
index 0000000..947f03c
--- /dev/null
+++ b/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png b/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png
new file mode 100644
index 0000000..6d09d72
--- /dev/null
+++ b/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png b/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png
new file mode 100644
index 0000000..63c7456
--- /dev/null
+++ b/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-xhdpi/list_background_holo.9.png b/res/drawable-ldrtl-xhdpi/list_background_holo.9.png
new file mode 100644
index 0000000..f709f2c
--- /dev/null
+++ b/res/drawable-ldrtl-xhdpi/list_background_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png b/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png
new file mode 100644
index 0000000..4139942
--- /dev/null
+++ b/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png
Binary files differ
diff --git a/res/drawable-ldrtl-xhdpi/list_title_holo.9.png b/res/drawable-ldrtl-xhdpi/list_title_holo.9.png
new file mode 100644
index 0000000..cb801ac
--- /dev/null
+++ b/res/drawable-ldrtl-xhdpi/list_title_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_ab_search.png b/res/drawable-mdpi/ic_ab_search.png
new file mode 100644
index 0000000..2b23b1e
--- /dev/null
+++ b/res/drawable-mdpi/ic_ab_search.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_add_to_circles_black_24.png b/res/drawable-mdpi/ic_add_to_circles_black_24.png
deleted file mode 100644
index fbffc97..0000000
--- a/res/drawable-mdpi/ic_add_to_circles_black_24.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_arrow_back_24dp.png b/res/drawable-mdpi/ic_arrow_back_24dp.png
new file mode 100644
index 0000000..1a21fb4
--- /dev/null
+++ b/res/drawable-mdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_business_white_120dp.png b/res/drawable-mdpi/ic_business_white_120dp.png
new file mode 100644
index 0000000..3dddca5
--- /dev/null
+++ b/res/drawable-mdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_call_24dp.png b/res/drawable-mdpi/ic_call_24dp.png
new file mode 100644
index 0000000..77f9de5
--- /dev/null
+++ b/res/drawable-mdpi/ic_call_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_call_note_white_24dp.png b/res/drawable-mdpi/ic_call_note_white_24dp.png
new file mode 100644
index 0000000..9d359db
--- /dev/null
+++ b/res/drawable-mdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_call_voicemail_holo_dark.png b/res/drawable-mdpi/ic_call_voicemail_holo_dark.png
new file mode 100644
index 0000000..bf6d006
--- /dev/null
+++ b/res/drawable-mdpi/ic_call_voicemail_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_close_dk.png b/res/drawable-mdpi/ic_close_dk.png
new file mode 100644
index 0000000..590a728
--- /dev/null
+++ b/res/drawable-mdpi/ic_close_dk.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_create_24dp.png b/res/drawable-mdpi/ic_create_24dp.png
new file mode 100644
index 0000000..8a2df39
--- /dev/null
+++ b/res/drawable-mdpi/ic_create_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_group_white_24dp.png b/res/drawable-mdpi/ic_group_white_24dp.png
new file mode 100644
index 0000000..ad268bf
--- /dev/null
+++ b/res/drawable-mdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_history_white_drawable_24dp.png b/res/drawable-mdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 0000000..b3000d3
--- /dev/null
+++ b/res/drawable-mdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_info_outline_24dp.png b/res/drawable-mdpi/ic_info_outline_24dp.png
new file mode 100644
index 0000000..353e064
--- /dev/null
+++ b/res/drawable-mdpi/ic_info_outline_24dp.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_menu_star_holo_light.png b/res/drawable-mdpi/ic_menu_star_holo_light.png
new file mode 100644
index 0000000..8263b27
--- /dev/null
+++ b/res/drawable-mdpi/ic_menu_star_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_message_24dp.png b/res/drawable-mdpi/ic_message_24dp.png
new file mode 100644
index 0000000..3072b75
--- /dev/null
+++ b/res/drawable-mdpi/ic_message_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_person_24dp.png b/res/drawable-mdpi/ic_person_24dp.png
new file mode 100644
index 0000000..f0b1c72
--- /dev/null
+++ b/res/drawable-mdpi/ic_person_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_person_add_24dp.png b/res/drawable-mdpi/ic_person_add_24dp.png
new file mode 100644
index 0000000..38e0a28
--- /dev/null
+++ b/res/drawable-mdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_person_avatar.png b/res/drawable-mdpi/ic_person_avatar.png
new file mode 100644
index 0000000..31a40fb
--- /dev/null
+++ b/res/drawable-mdpi/ic_person_avatar.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_person_white_120dp.png b/res/drawable-mdpi/ic_person_white_120dp.png
new file mode 100644
index 0000000..397d933
--- /dev/null
+++ b/res/drawable-mdpi/ic_person_white_120dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_scroll_handle.png b/res/drawable-mdpi/ic_scroll_handle.png
new file mode 100644
index 0000000..af75db4
--- /dev/null
+++ b/res/drawable-mdpi/ic_scroll_handle.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_videocam.png b/res/drawable-mdpi/ic_videocam.png
new file mode 100644
index 0000000..dc9655b
--- /dev/null
+++ b/res/drawable-mdpi/ic_videocam.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_voicemail_avatar.png b/res/drawable-mdpi/ic_voicemail_avatar.png
new file mode 100644
index 0000000..4005f24
--- /dev/null
+++ b/res/drawable-mdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/res/drawable-mdpi/list_activated_holo.9.png b/res/drawable-mdpi/list_activated_holo.9.png
new file mode 100644
index 0000000..3bf8e03
--- /dev/null
+++ b/res/drawable-mdpi/list_activated_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_background_holo.9.png b/res/drawable-mdpi/list_background_holo.9.png
new file mode 100644
index 0000000..7d5d66d
--- /dev/null
+++ b/res/drawable-mdpi/list_background_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_focused_holo.9.png b/res/drawable-mdpi/list_focused_holo.9.png
new file mode 100644
index 0000000..86578be
--- /dev/null
+++ b/res/drawable-mdpi/list_focused_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_longpressed_holo_light.9.png b/res/drawable-mdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 0000000..3226ab7
--- /dev/null
+++ b/res/drawable-mdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_pressed_holo_light.9.png b/res/drawable-mdpi/list_pressed_holo_light.9.png
new file mode 100644
index 0000000..061904c
--- /dev/null
+++ b/res/drawable-mdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-mdpi/list_title_holo.9.png b/res/drawable-mdpi/list_title_holo.9.png
new file mode 100644
index 0000000..64bd691
--- /dev/null
+++ b/res/drawable-mdpi/list_title_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi/unknown_source.png b/res/drawable-mdpi/unknown_source.png
new file mode 100644
index 0000000..356748f
--- /dev/null
+++ b/res/drawable-mdpi/unknown_source.png
Binary files differ
diff --git a/res/drawable-sw600dp-hdpi/list_activated_holo.9.png b/res/drawable-sw600dp-hdpi/list_activated_holo.9.png
new file mode 100644
index 0000000..046b24a
--- /dev/null
+++ b/res/drawable-sw600dp-hdpi/list_activated_holo.9.png
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/list_activated_holo.9.png b/res/drawable-sw600dp-mdpi/list_activated_holo.9.png
new file mode 100644
index 0000000..1ff3373
--- /dev/null
+++ b/res/drawable-sw600dp-mdpi/list_activated_holo.9.png
Binary files differ
diff --git a/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png b/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png
new file mode 100644
index 0000000..2eb7c7e
--- /dev/null
+++ b/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/res/drawable-v21/view_pager_tab_background.xml b/res/drawable-v21/view_pager_tab_background.xml
index 00c6db7..b9e0805 100644
--- a/res/drawable-v21/view_pager_tab_background.xml
+++ b/res/drawable-v21/view_pager_tab_background.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2015 The Android Open Source Project
+  ~ Copyright (C) 2016 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.
@@ -15,8 +15,8 @@
   ~ limitations under the License
   -->
 <ripple xmlns:android="http://schemas.android.com/apk/res/android"
-    android:color="@color/tab_ripple_color">
+        android:color="@color/tab_ripple_color">
     <item android:id="@android:id/mask">
         <color android:color="@android:color/white" />
     </item>
-</ripple>
\ No newline at end of file
+</ripple>
diff --git a/res/drawable-xhdpi/ic_ab_search.png b/res/drawable-xhdpi/ic_ab_search.png
new file mode 100644
index 0000000..71f7827
--- /dev/null
+++ b/res/drawable-xhdpi/ic_ab_search.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_add_to_circles_black_24.png b/res/drawable-xhdpi/ic_add_to_circles_black_24.png
deleted file mode 100644
index 79116b5..0000000
--- a/res/drawable-xhdpi/ic_add_to_circles_black_24.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_arrow_back_24dp.png b/res/drawable-xhdpi/ic_arrow_back_24dp.png
new file mode 100644
index 0000000..bb73272
--- /dev/null
+++ b/res/drawable-xhdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_business_white_120dp.png b/res/drawable-xhdpi/ic_business_white_120dp.png
new file mode 100644
index 0000000..6256300
--- /dev/null
+++ b/res/drawable-xhdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_call_24dp.png b/res/drawable-xhdpi/ic_call_24dp.png
new file mode 100644
index 0000000..ef45e93
--- /dev/null
+++ b/res/drawable-xhdpi/ic_call_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_call_note_white_24dp.png b/res/drawable-xhdpi/ic_call_note_white_24dp.png
new file mode 100644
index 0000000..40eed1d
--- /dev/null
+++ b/res/drawable-xhdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_call_voicemail_holo_dark.png b/res/drawable-xhdpi/ic_call_voicemail_holo_dark.png
new file mode 100644
index 0000000..d9684d1
--- /dev/null
+++ b/res/drawable-xhdpi/ic_call_voicemail_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_close_dk.png b/res/drawable-xhdpi/ic_close_dk.png
new file mode 100644
index 0000000..5769f11
--- /dev/null
+++ b/res/drawable-xhdpi/ic_close_dk.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_create_24dp.png b/res/drawable-xhdpi/ic_create_24dp.png
new file mode 100644
index 0000000..48e75be
--- /dev/null
+++ b/res/drawable-xhdpi/ic_create_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_group_white_24dp.png b/res/drawable-xhdpi/ic_group_white_24dp.png
new file mode 100644
index 0000000..09c0e3e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_history_white_drawable_24dp.png b/res/drawable-xhdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 0000000..e188d4a
--- /dev/null
+++ b/res/drawable-xhdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_info_outline_24dp.png b/res/drawable-xhdpi/ic_info_outline_24dp.png
new file mode 100644
index 0000000..c571b2e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_info_outline_24dp.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_message_24dp.png b/res/drawable-xhdpi/ic_message_24dp.png
new file mode 100644
index 0000000..763767b
--- /dev/null
+++ b/res/drawable-xhdpi/ic_message_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_person_24dp.png b/res/drawable-xhdpi/ic_person_24dp.png
new file mode 100644
index 0000000..aea15f0
--- /dev/null
+++ b/res/drawable-xhdpi/ic_person_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_person_add_24dp.png b/res/drawable-xhdpi/ic_person_add_24dp.png
new file mode 100644
index 0000000..7e7c289
--- /dev/null
+++ b/res/drawable-xhdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_person_avatar.png b/res/drawable-xhdpi/ic_person_avatar.png
new file mode 100644
index 0000000..aecc9af
--- /dev/null
+++ b/res/drawable-xhdpi/ic_person_avatar.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_person_white_120dp.png b/res/drawable-xhdpi/ic_person_white_120dp.png
new file mode 100644
index 0000000..8d80a05
--- /dev/null
+++ b/res/drawable-xhdpi/ic_person_white_120dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_scroll_handle.png b/res/drawable-xhdpi/ic_scroll_handle.png
new file mode 100644
index 0000000..2d43c4d
--- /dev/null
+++ b/res/drawable-xhdpi/ic_scroll_handle.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_videocam.png b/res/drawable-xhdpi/ic_videocam.png
new file mode 100644
index 0000000..c1783de
--- /dev/null
+++ b/res/drawable-xhdpi/ic_videocam.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_voicemail_avatar.png b/res/drawable-xhdpi/ic_voicemail_avatar.png
new file mode 100644
index 0000000..f24505d
--- /dev/null
+++ b/res/drawable-xhdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_activated_holo.9.png b/res/drawable-xhdpi/list_activated_holo.9.png
new file mode 100644
index 0000000..eda10e6
--- /dev/null
+++ b/res/drawable-xhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_background_holo.9.png b/res/drawable-xhdpi/list_background_holo.9.png
new file mode 100644
index 0000000..b652725
--- /dev/null
+++ b/res/drawable-xhdpi/list_background_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_focused_holo.9.png b/res/drawable-xhdpi/list_focused_holo.9.png
new file mode 100644
index 0000000..86578be
--- /dev/null
+++ b/res/drawable-xhdpi/list_focused_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_longpressed_holo_light.9.png b/res/drawable-xhdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 0000000..5532e88
--- /dev/null
+++ b/res/drawable-xhdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_pressed_holo_light.9.png b/res/drawable-xhdpi/list_pressed_holo_light.9.png
new file mode 100644
index 0000000..f4af926
--- /dev/null
+++ b/res/drawable-xhdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/list_title_holo.9.png b/res/drawable-xhdpi/list_title_holo.9.png
new file mode 100644
index 0000000..f4f00ca
--- /dev/null
+++ b/res/drawable-xhdpi/list_title_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/unknown_source.png b/res/drawable-xhdpi/unknown_source.png
new file mode 100644
index 0000000..35e8fb4
--- /dev/null
+++ b/res/drawable-xhdpi/unknown_source.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_ab_search.png b/res/drawable-xxhdpi/ic_ab_search.png
new file mode 100644
index 0000000..142c545
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_ab_search.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_add_to_circles_black_24.png b/res/drawable-xxhdpi/ic_add_to_circles_black_24.png
deleted file mode 100644
index 23a2084..0000000
--- a/res/drawable-xxhdpi/ic_add_to_circles_black_24.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_arrow_back_24dp.png b/res/drawable-xxhdpi/ic_arrow_back_24dp.png
new file mode 100644
index 0000000..72c51b0
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_business_white_120dp.png b/res/drawable-xxhdpi/ic_business_white_120dp.png
new file mode 100644
index 0000000..8d67e44
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_call_24dp.png b/res/drawable-xxhdpi/ic_call_24dp.png
new file mode 100644
index 0000000..90ead2e
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_call_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_call_note_white_24dp.png b/res/drawable-xxhdpi/ic_call_note_white_24dp.png
new file mode 100644
index 0000000..2656cad
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_call_voicemail_holo_dark.png b/res/drawable-xxhdpi/ic_call_voicemail_holo_dark.png
new file mode 100644
index 0000000..ac5b83b
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_call_voicemail_holo_dark.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_close_dk.png b/res/drawable-xxhdpi/ic_close_dk.png
new file mode 100644
index 0000000..670bf79
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_close_dk.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_create_24dp.png b/res/drawable-xxhdpi/ic_create_24dp.png
new file mode 100644
index 0000000..24142c7
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_create_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_group_white_24dp.png b/res/drawable-xxhdpi/ic_group_white_24dp.png
new file mode 100644
index 0000000..03cad4c
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png b/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 0000000..f44df1a
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_info_outline_24dp.png b/res/drawable-xxhdpi/ic_info_outline_24dp.png
new file mode 100644
index 0000000..c41a5fc
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png b/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 0000000..0fec2f2
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_message_24dp.png b/res/drawable-xxhdpi/ic_message_24dp.png
new file mode 100644
index 0000000..0a79824
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_message_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_person_24dp.png b/res/drawable-xxhdpi/ic_person_24dp.png
new file mode 100644
index 0000000..184f741
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_person_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_person_add_24dp.png b/res/drawable-xxhdpi/ic_person_add_24dp.png
new file mode 100644
index 0000000..8f744f0
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_person_avatar.png b/res/drawable-xxhdpi/ic_person_avatar.png
new file mode 100644
index 0000000..2cfc004
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_person_avatar.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_person_white_120dp.png b/res/drawable-xxhdpi/ic_person_white_120dp.png
new file mode 100644
index 0000000..b29df2f
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_person_white_120dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_scroll_handle.png b/res/drawable-xxhdpi/ic_scroll_handle.png
new file mode 100644
index 0000000..55f1d13
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_scroll_handle.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_videocam.png b/res/drawable-xxhdpi/ic_videocam.png
new file mode 100644
index 0000000..4ab5ad0
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_videocam.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_voicemail_avatar.png b/res/drawable-xxhdpi/ic_voicemail_avatar.png
new file mode 100644
index 0000000..182def8
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/res/drawable-xxhdpi/list_activated_holo.9.png b/res/drawable-xxhdpi/list_activated_holo.9.png
new file mode 100644
index 0000000..52c00dd
--- /dev/null
+++ b/res/drawable-xxhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/list_focused_holo.9.png b/res/drawable-xxhdpi/list_focused_holo.9.png
new file mode 100644
index 0000000..3e4ca68
--- /dev/null
+++ b/res/drawable-xxhdpi/list_focused_holo.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/list_longpressed_holo_light.9.png b/res/drawable-xxhdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 0000000..230d649
--- /dev/null
+++ b/res/drawable-xxhdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/list_pressed_holo_light.9.png b/res/drawable-xxhdpi/list_pressed_holo_light.9.png
new file mode 100644
index 0000000..1352a17
--- /dev/null
+++ b/res/drawable-xxhdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/list_title_holo.9.png b/res/drawable-xxhdpi/list_title_holo.9.png
new file mode 100644
index 0000000..7ddf14a
--- /dev/null
+++ b/res/drawable-xxhdpi/list_title_holo.9.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_ab_search.png b/res/drawable-xxxhdpi/ic_ab_search.png
new file mode 100644
index 0000000..2ffb2ec
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_ab_search.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_arrow_back_24dp.png b/res/drawable-xxxhdpi/ic_arrow_back_24dp.png
new file mode 100644
index 0000000..ae01a04
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_business_white_120dp.png b/res/drawable-xxxhdpi/ic_business_white_120dp.png
new file mode 100644
index 0000000..1741675
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_call_24dp.png b/res/drawable-xxxhdpi/ic_call_24dp.png
new file mode 100644
index 0000000..b0e0205
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_call_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_call_note_white_24dp.png b/res/drawable-xxxhdpi/ic_call_note_white_24dp.png
new file mode 100644
index 0000000..903c162
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_close_dk.png b/res/drawable-xxxhdpi/ic_close_dk.png
new file mode 100644
index 0000000..3a5540f
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_close_dk.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_create_24dp.png b/res/drawable-xxxhdpi/ic_create_24dp.png
new file mode 100644
index 0000000..d3ff0ec
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_create_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png b/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 0000000..5b96af5
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_info_outline_24dp.png b/res/drawable-xxxhdpi/ic_info_outline_24dp.png
new file mode 100644
index 0000000..3a82cab
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_message_24dp.png b/res/drawable-xxxhdpi/ic_message_24dp.png
new file mode 100644
index 0000000..fa7c17a
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_message_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_person_24dp.png b/res/drawable-xxxhdpi/ic_person_24dp.png
new file mode 100644
index 0000000..33d40d8
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_person_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_person_add_24dp.png b/res/drawable-xxxhdpi/ic_person_add_24dp.png
new file mode 100644
index 0000000..2fa2cca
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_person_avatar.png b/res/drawable-xxxhdpi/ic_person_avatar.png
new file mode 100644
index 0000000..3233252
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_person_avatar.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_person_white_120dp.png b/res/drawable-xxxhdpi/ic_person_white_120dp.png
new file mode 100644
index 0000000..b53cc11
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_person_white_120dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_scroll_handle.png b/res/drawable-xxxhdpi/ic_scroll_handle.png
new file mode 100644
index 0000000..d90782a
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_scroll_handle.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_videocam.png b/res/drawable-xxxhdpi/ic_videocam.png
new file mode 100644
index 0000000..0643ea5
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_videocam.png
Binary files differ
diff --git a/res/drawable/dialog_background_material.xml b/res/drawable/dialog_background_material.xml
new file mode 100644
index 0000000..fb586a0
--- /dev/null
+++ b/res/drawable/dialog_background_material.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:inset="16dp">
+    <shape android:shape="rectangle">
+        <corners android:radius="2dp" />
+        <solid android:color="@color/call_subject_history_background" />
+    </shape>
+</inset>
diff --git a/res/drawable/fastscroll_thumb.xml b/res/drawable/fastscroll_thumb.xml
new file mode 100644
index 0000000..eca4b39
--- /dev/null
+++ b/res/drawable/fastscroll_thumb.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true" android:drawable="@drawable/ic_scroll_handle_pressed" />
+    <item android:drawable="@drawable/ic_scroll_handle_default" />
+</selector>
\ No newline at end of file
diff --git a/res/drawable/ic_back_arrow.xml b/res/drawable/ic_back_arrow.xml
new file mode 100644
index 0000000..68a875d
--- /dev/null
+++ b/res/drawable/ic_back_arrow.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 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
+  -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_arrow_back_24dp"
+        android:autoMirrored="true"
+        android:tint="@color/actionbar_icon_color" />
\ No newline at end of file
diff --git a/res/drawable/ic_call.xml b/res/drawable/ic_call.xml
new file mode 100644
index 0000000..e06317b
--- /dev/null
+++ b/res/drawable/ic_call.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 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
+  -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_call_24dp"
+        android:autoMirrored="true" />
diff --git a/res/drawable/ic_cancel_black_24dp.xml b/res/drawable/ic_cancel_black_24dp.xml
new file mode 100644
index 0000000..30f8ef5
--- /dev/null
+++ b/res/drawable/ic_cancel_black_24dp.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<!-- An 'x' with a circle around it (used as a delete button). -->
+<vector android:alpha="0.54" android:height="24dp"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#000000" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
+</vector>
diff --git a/res/drawable/ic_check_mark.xml b/res/drawable/ic_check_mark.xml
new file mode 100644
index 0000000..b0d73cd
--- /dev/null
+++ b/res/drawable/ic_check_mark.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<!-- Checkmark icon used when some task is done -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_close_black_24dp.xml b/res/drawable/ic_close_black_24dp.xml
new file mode 100644
index 0000000..4ddacdf
--- /dev/null
+++ b/res/drawable/ic_close_black_24dp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<!-- 'X' icon (used in multi select mode and search bar). -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+    <path
+        android:fillColor="#000000"
+        android:pathData="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
+</vector>
diff --git a/res/drawable/ic_device.xml b/res/drawable/ic_device.xml
new file mode 100644
index 0000000..7fd3bd5
--- /dev/null
+++ b/res/drawable/ic_device.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- ic_device is a phone-shaped icon. Since it is not tinted so we set the tint color in here. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+<path
+    android:fillColor="#7f7f7f"
+    android:pathData="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_material_star.xml b/res/drawable/ic_material_star.xml
new file mode 100644
index 0000000..cd7c61c
--- /dev/null
+++ b/res/drawable/ic_material_star.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<!-- Material design star icon -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+<path
+    android:fillColor="#000000"
+    android:pathData="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_message_24dp_mirrored.xml b/res/drawable/ic_message_24dp_mirrored.xml
new file mode 100644
index 0000000..b1bd743
--- /dev/null
+++ b/res/drawable/ic_message_24dp_mirrored.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 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
+  -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+    android:src="@drawable/ic_message_24dp"
+    android:autoMirrored="true" />
diff --git a/res/drawable/ic_more_vert.xml b/res/drawable/ic_more_vert.xml
new file mode 100644
index 0000000..749316a
--- /dev/null
+++ b/res/drawable/ic_more_vert.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
+</vector>
diff --git a/res/drawable/ic_person_add_tinted_24dp.xml b/res/drawable/ic_person_add_tinted_24dp.xml
new file mode 100644
index 0000000..fdbf4fa
--- /dev/null
+++ b/res/drawable/ic_person_add_tinted_24dp.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 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
+  -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+    android:src="@drawable/ic_person_add_24dp"
+    android:autoMirrored="true"/>
diff --git a/res/drawable/ic_scroll_handle_default.xml b/res/drawable/ic_scroll_handle_default.xml
new file mode 100644
index 0000000..055005e
--- /dev/null
+++ b/res/drawable/ic_scroll_handle_default.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2014 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+    android:src="@drawable/ic_scroll_handle"
+    android:tint="@color/dialtacts_secondary_text_color" />
\ No newline at end of file
diff --git a/res/drawable/ic_scroll_handle_pressed.xml b/res/drawable/ic_scroll_handle_pressed.xml
new file mode 100644
index 0000000..9109c81
--- /dev/null
+++ b/res/drawable/ic_scroll_handle_pressed.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2014 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+    android:src="@drawable/ic_scroll_handle"
+    android:tint="@color/dialtacts_theme_color" />
\ No newline at end of file
diff --git a/res/drawable/ic_search_add_contact.xml b/res/drawable/ic_search_add_contact.xml
new file mode 100644
index 0000000..9a313cd
--- /dev/null
+++ b/res/drawable/ic_search_add_contact.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2014 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+    android:src="@drawable/ic_person_add_24dp"
+    android:autoMirrored="true" />
diff --git a/res/drawable/ic_search_video_call.xml b/res/drawable/ic_search_video_call.xml
new file mode 100644
index 0000000..c2390e5
--- /dev/null
+++ b/res/drawable/ic_search_video_call.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 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
+  -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@drawable/ic_videocam"
+        android:autoMirrored="true"
+        android:tint="@color/search_video_call_icon_tint" />
diff --git a/res/drawable/ic_work_profile.xml b/res/drawable/ic_work_profile.xml
new file mode 100644
index 0000000..bacfe69
--- /dev/null
+++ b/res/drawable/ic_work_profile.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+
+    <!-- //java/com/google/android/assets/quantum:ic_enterprise_grey600_16 -->
+
+    <path
+        android:fillColor="#757575"
+        android:pathData="M28 33h-8v-3H6v8c0 2.2 1.8 4 4 4h28c2.2 0 4-1.8
+4-4v-8H28v3zm12-21h-7V9l-3-3H18l-3 3.1V12H8c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4
+4h12v-3h8v3h12c2.2 0 4-1.8 4-4v-8c0-2.2-1.8-4-4-4zm-10 0H18V9h12v3z" />
+    <path
+        android:pathData="M0 0h48v48H0z" />
+</vector>
diff --git a/res/drawable/item_background_material_borderless_dark.xml b/res/drawable/item_background_material_borderless_dark.xml
new file mode 100644
index 0000000..693bcaf
--- /dev/null
+++ b/res/drawable/item_background_material_borderless_dark.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<!-- Based on the Theme.Material's default selectableItemBackgroundBorderless -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/ripple_material_dark" />
\ No newline at end of file
diff --git a/res/drawable/item_background_material_dark.xml b/res/drawable/item_background_material_dark.xml
new file mode 100644
index 0000000..87b1e17
--- /dev/null
+++ b/res/drawable/item_background_material_dark.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<!-- Based on the Theme.Material's default selectableItemBackground -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/ripple_material_dark" >
+    <item android:id="@android:id/mask">
+        <color android:color="@android:color/white" />
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/res/drawable/list_item_activated_background.xml b/res/drawable/list_item_activated_background.xml
new file mode 100644
index 0000000..a58577e
--- /dev/null
+++ b/res/drawable/list_item_activated_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_activated="true" android:drawable="@drawable/list_activated_holo" />
+    <item android:drawable="@drawable/list_background_holo" />
+</selector>
diff --git a/res/drawable/searchedittext_custom_cursor.xml b/res/drawable/searchedittext_custom_cursor.xml
new file mode 100644
index 0000000..a6bb90f
--- /dev/null
+++ b/res/drawable/searchedittext_custom_cursor.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 Google Inc. All Rights Reserved. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android" >
+    <size android:width="2dp" android:height="22dp" />
+    <solid android:color="@color/dialtacts_theme_color" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/unread_count_background.xml b/res/drawable/unread_count_background.xml
new file mode 100644
index 0000000..f70f84a
--- /dev/null
+++ b/res/drawable/unread_count_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="@dimen/tab_unread_count_background_radius"/>
+    <solid android:color="@color/tab_unread_count_background_color" />
+</shape>
diff --git a/res/drawable/view_pager_tab_background.xml b/res/drawable/view_pager_tab_background.xml
index f1ddbe2..9f59845 100644
--- a/res/drawable/view_pager_tab_background.xml
+++ b/res/drawable/view_pager_tab_background.xml
@@ -17,5 +17,5 @@
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
     <item
         android:state_pressed="true"
-        android:drawable="@color/primary_color_dark"/>
+        android:drawable="@color/tab_ripple_color"/>
 </selector>
\ No newline at end of file
diff --git a/res/layout-ldrtl/unread_count_tab.xml b/res/layout-ldrtl/unread_count_tab.xml
new file mode 100644
index 0000000..b23ab57
--- /dev/null
+++ b/res/layout-ldrtl/unread_count_tab.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<!-- layoutDirection set to ltr as a workaround to a framework bug (b/22010411) causing view with
+     layout_centerInParent inside a RelativeLayout to expand to screen width when RTL is active -->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/view_pager_tab_background"
+    android:layoutDirection="ltr">
+    <!-- The tab icon -->
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"/>
+    <TextView
+        android:id="@+id/count"
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/tab_unread_count_background_size"
+        android:layout_marginTop="@dimen/tab_unread_count_margin_top"
+        android:layout_marginStart="@dimen/tab_unread_count_margin_left"
+        android:layout_toStartOf="@id/icon"
+        android:paddingLeft="@dimen/tab_unread_count_text_padding"
+        android:paddingRight="@dimen/tab_unread_count_text_padding"
+        android:background="@drawable/unread_count_background"
+        android:fontFamily="sans-serif-medium"
+        android:importantForAccessibility="no"
+        android:minWidth="@dimen/tab_unread_count_background_size"
+        android:textAlignment="center"
+        android:textColor="@color/tab_accent_color"
+        android:textSize="@dimen/tab_unread_count_text_size"
+        android:layoutDirection="locale"/>
+</RelativeLayout>
+
diff --git a/res/layout/account_filter_header.xml b/res/layout/account_filter_header.xml
new file mode 100644
index 0000000..d348e82
--- /dev/null
+++ b/res/layout/account_filter_header.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Layout showing the type of account filter
+     (e.g. All contacts filter, custom filter, etc.),
+     which is the header of all contact lists. -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/account_filter_header_container"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/contact_browser_list_header_height"
+    android:paddingTop="@dimen/list_header_extra_top_padding"
+    android:visibility="gone"
+    android:orientation="horizontal"
+    android:background="@color/background_primary">
+
+    <ImageView
+        android:id="@+id/account_filter_icon"
+        android:layout_height="@dimen/contact_browser_list_header_icon_size"
+        android:layout_width="@dimen/contact_browser_list_header_icon_size"
+        android:layout_marginStart="@dimen/contact_browser_list_header_icon_left_margin"
+        android:layout_marginEnd="@dimen/contact_browser_list_header_icon_right_margin"
+        android:layout_gravity="center_vertical"/>
+
+    <TextView
+        android:id="@+id/account_filter_header"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="@dimen/contact_browser_list_header_text_margin"
+        android:layout_marginEnd="@dimen/contact_browser_list_header_right_margin"
+        style="@style/ContactListSeparatorTextViewStyle"
+        android:textAlignment="viewStart"
+        android:paddingLeft="@dimen/contact_browser_list_item_text_indent"
+        android:paddingStart="@dimen/contact_browser_list_item_text_indent" />
+</LinearLayout>
diff --git a/res/layout/account_selector_list_item.xml b/res/layout/account_selector_list_item.xml
new file mode 100644
index 0000000..ba86a9b
--- /dev/null
+++ b/res/layout/account_selector_list_item.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="?android:attr/listPreferredItemHeight"
+    android:orientation="horizontal">
+    <ImageView android:id="@android:id/icon"
+        android:layout_width="@dimen/detail_network_icon_size"
+        android:layout_height="@dimen/detail_network_icon_size"
+        android:layout_margin="16dip"
+        android:layout_gravity="center_vertical" />
+
+    <LinearLayout
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:layout_marginLeft="8dp"
+        android:orientation="vertical"
+        android:layout_gravity="center_vertical">
+
+        <TextView android:id="@android:id/text1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="8dip"
+            android:layout_marginEnd="8dip"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:singleLine="true"
+            android:ellipsize="end"/>
+
+        <TextView android:id="@android:id/text2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="8dip"
+            android:layout_marginEnd="8dip"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="?android:attr/textColorSecondary"
+            android:singleLine="true"
+            android:ellipsize="end"/>
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/account_selector_list_item_condensed.xml b/res/layout/account_selector_list_item_condensed.xml
new file mode 100644
index 0000000..6720065
--- /dev/null
+++ b/res/layout/account_selector_list_item_condensed.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="280dp"
+    android:layout_height="72dp"
+    android:orientation="horizontal">
+    <ImageView android:id="@android:id/icon"
+        android:layout_width="@dimen/detail_network_icon_size"
+        android:layout_height="@dimen/detail_network_icon_size"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="16dp"
+        android:layout_gravity="center_vertical" />
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="vertical"
+        android:layout_gravity="center_vertical">
+
+        <TextView android:id="@android:id/text1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="8dip"
+            android:layout_marginEnd="8dip"
+            android:textSize="16sp"
+            android:textColor="@color/contacts_text_color"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:singleLine="true"
+            android:ellipsize="end"/>
+
+        <TextView android:id="@android:id/text2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="8dip"
+            android:layout_marginEnd="8dip"
+            android:textSize="14sp"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="?android:attr/textColorSecondary"
+            android:singleLine="true"
+            android:ellipsize="end"/>
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/call_subject_history_list_item.xml b/res/layout/call_subject_history_list_item.xml
new file mode 100644
index 0000000..b8cce47
--- /dev/null
+++ b/res/layout/call_subject_history_list_item.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@android:id/text1"
+          android:gravity="center_vertical"
+          android:layout_width="match_parent"
+          android:layout_height="wrap_content"
+          android:paddingStart="@dimen/call_subject_dialog_margin"
+          android:paddingEnd="@dimen/call_subject_dialog_margin"
+          android:paddingTop="@dimen/call_subject_history_item_padding"
+          android:paddingBottom="@dimen/call_subject_history_item_padding"
+          android:singleLine="true"
+          android:textColor="@color/dialtacts_primary_text_color"
+          android:textSize="@dimen/call_subject_dialog_primary_text_size" />
diff --git a/res/layout/contact_list_card.xml b/res/layout/contact_list_card.xml
new file mode 100644
index 0000000..c20dbe7
--- /dev/null
+++ b/res/layout/contact_list_card.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+        android:id="@+id/list_card"
+        android:visibility="invisible">
+    <View
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@color/contact_all_list_background_color"/>
+</LinearLayout>
diff --git a/res/layout/contact_list_content.xml b/res/layout/contact_list_content.xml
new file mode 100644
index 0000000..f18267d
--- /dev/null
+++ b/res/layout/contact_list_content.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- android:paddingTop is used instead of android:layout_marginTop. It looks
+     android:layout_marginTop is ignored when used with <fragment></fragment>, which
+     only happens in Tablet UI since we rely on ViewPager in Phone UI.
+     Instead, android:layout_marginTop inside <fragment /> is effective. -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/pinned_header_list_layout"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:visibility="gone"
+    android:background="?attr/contact_browser_background" >
+
+    <!-- Shown only when an Account filter is set.
+         - paddingTop should be here to show "shade" effect correctly. -->
+    <include layout="@layout/account_filter_header" />
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+        android:id="@+id/contact_list">
+
+        <include layout="@layout/contact_list_card"/>
+        <view
+            class="com.android.contacts.common.list.PinnedHeaderListView"
+            android:id="@android:id/list"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_marginLeft="?attr/contact_browser_list_padding_left"
+            android:layout_marginRight="?attr/contact_browser_list_padding_right"
+            android:layout_marginStart="?attr/contact_browser_list_padding_left"
+            android:layout_marginEnd="?attr/contact_browser_list_padding_right"
+            android:paddingTop="?attr/list_item_padding_top"
+            android:clipToPadding="false"
+            android:fastScrollEnabled="true"
+            android:visibility="gone"
+            android:fadingEdge="none" />
+        <ProgressBar
+            android:id="@+id/search_progress"
+            style="?android:attr/progressBarStyleLarge"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:visibility="gone" />
+    </FrameLayout>
+
+</LinearLayout>
diff --git a/res/layout/contact_list_filter.xml b/res/layout/contact_list_filter.xml
new file mode 100644
index 0000000..34c713c
--- /dev/null
+++ b/res/layout/contact_list_filter.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2011 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:fillViewport="true">
+
+    <ListView
+        android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1" />
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dip"
+        android:background="?android:attr/dividerHorizontal" />
+</LinearLayout>
diff --git a/res/layout/contact_list_filter_custom.xml b/res/layout/contact_list_filter_custom.xml
new file mode 100644
index 0000000..845d52f
--- /dev/null
+++ b/res/layout/contact_list_filter_custom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/CustomContactListFilterView"
+    android:orientation="vertical"
+    android:fillViewport="true">
+
+    <ExpandableListView
+        android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+        android:overScrollMode="always" />
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dip"
+        android:layout_marginLeft="16dip"
+        android:layout_marginRight="16dip"
+        android:layout_marginStart="16dip"
+        android:layout_marginEnd="16dip"
+        android:background="?android:attr/dividerHorizontal" />
+
+</LinearLayout>
diff --git a/res/layout/contact_list_filter_item.xml b/res/layout/contact_list_filter_item.xml
new file mode 100644
index 0000000..9f297e6
--- /dev/null
+++ b/res/layout/contact_list_filter_item.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<view
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    class="com.android.contacts.common.list.ContactListFilterView"
+    android:descendantFocusability="blocksDescendants"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingStart="@dimen/contact_filter_left_margin"
+    android:paddingEnd="@dimen/contact_filter_right_margin"
+    android:minHeight="@dimen/contact_filter_item_min_height"
+    android:gravity="center_vertical">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:scaleType="fitCenter"
+        android:layout_width="@dimen/contact_filter_icon_size"
+        android:layout_height="@dimen/contact_filter_icon_size"/>
+
+    <LinearLayout
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:layout_marginTop="-1dip"
+        android:orientation="vertical"
+        android:layout_marginLeft="8dip"
+        android:layout_marginStart="8dip">
+
+        <TextView
+            android:id="@+id/accountType"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="?android:attr/textAppearanceListItem"
+            android:textColor="@color/account_filter_text_color"
+            android:singleLine="true"
+            android:ellipsize="end"/>
+
+        <TextView
+            android:id="@+id/accountUserName"
+            android:layout_marginTop="-3dip"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:textColor="@color/account_filter_text_color"
+            android:singleLine="true"
+            android:ellipsize="end"/>
+    </LinearLayout>
+
+    <RadioButton
+        android:id="@+id/radioButton"
+        android:clickable="false"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="end|center_vertical" />
+</view>
+
diff --git a/res/layout/contact_tile_frequent.xml b/res/layout/contact_tile_frequent.xml
new file mode 100644
index 0000000..b1e83ce
--- /dev/null
+++ b/res/layout/contact_tile_frequent.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.
+-->
+<view
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    class="com.android.contacts.common.list.ContactTileFrequentView"
+    android:focusable="true"
+    android:background="?android:attr/selectableItemBackground">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:gravity="center_vertical"
+        android:paddingTop="?list_item_padding_top"
+        android:paddingBottom="?list_item_padding_bottom">
+
+        <com.android.contacts.common.widget.LayoutSuppressingImageView
+            android:id="@+id/contact_tile_image"
+            android:layout_width="?list_item_photo_size"
+            android:layout_height="?list_item_photo_size"
+            android:scaleType="centerCrop"
+            android:layout_marginEnd="?list_item_gap_between_image_and_text"/>
+
+        <TextView
+            android:id="@+id/contact_tile_name"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="@android:color/black"
+            android:textSize="@dimen/contact_browser_list_item_text_size"
+            android:singleLine="true"
+            android:fadingEdge="horizontal"
+            android:fadingEdgeLength="3dip"
+            android:ellipsize="marquee"
+            android:textAlignment="viewStart" />
+
+    </LinearLayout>
+
+</view>
diff --git a/res/layout/contact_tile_frequent_phone.xml b/res/layout/contact_tile_frequent_phone.xml
new file mode 100644
index 0000000..f87dff7
--- /dev/null
+++ b/res/layout/contact_tile_frequent_phone.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Layout parameters are set programmatically. -->
+<view
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/contact_tile_frequent_phone"
+    class="com.android.contacts.common.list.ContactTilePhoneFrequentView"
+    android:focusable="true"
+    android:background="?android:attr/selectableItemBackground"
+    android:nextFocusLeft="@+id/contact_tile_quick">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" >
+
+        <com.android.contacts.common.widget.LayoutSuppressingQuickContactBadge
+            android:id="@id/contact_tile_quick"
+            android:layout_width="64dip"
+            android:layout_height="64dip"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentStart="true"
+            android:nextFocusRight="@id/contact_tile_frequent_phone"
+            android:scaleType="centerCrop"
+            android:focusable="true" />
+
+        <TextView
+            android:id="@+id/contact_tile_name"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="8dip"
+            android:layout_marginStart="8dip"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:layout_marginTop="8dip"
+            android:layout_toRightOf="@id/contact_tile_quick"
+            android:layout_toEndOf="@id/contact_tile_quick"
+            android:singleLine="true"
+            android:fadingEdge="horizontal"
+            android:fadingEdgeLength="3dip"
+            android:ellipsize="marquee"
+            android:textAlignment="viewStart" />
+
+        <LinearLayout
+            android:orientation="horizontal"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/contact_tile_name"
+            android:layout_toRightOf="@id/contact_tile_quick"
+            android:layout_toEndOf="@id/contact_tile_quick"
+            android:gravity="center_vertical">
+
+            <TextView
+                android:id="@+id/contact_tile_phone_number"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="?attr/list_item_data_width_weight"
+                android:textSize="14sp"
+                android:ellipsize="marquee"
+                android:textColor="@color/dialtacts_secondary_text_color"
+                android:layout_marginLeft="8dip"
+                android:layout_marginStart="8dip"
+                android:singleLine="true"
+                android:layout_gravity="bottom"
+                android:textDirection="ltr"
+                android:textAlignment="viewStart" />
+
+            <TextView
+                android:id="@+id/contact_tile_phone_type"
+                android:layout_width="0dip"
+                android:layout_height="wrap_content"
+                android:layout_weight="?attr/list_item_label_width_weight"
+                android:textSize="12sp"
+                android:ellipsize="marquee"
+                android:singleLine="true"
+                android:textAllCaps="true"
+                android:textColor="@color/dialtacts_secondary_text_color"
+                android:layout_marginLeft="8dip"
+                android:layout_marginStart="8dip"
+                android:gravity="end"
+                android:layout_gravity="bottom" />
+
+        </LinearLayout>
+
+        <View
+            android:id="@+id/contact_tile_horizontal_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1px"
+            android:background="?android:attr/listDivider"
+            android:layout_below="@id/contact_tile_quick" />
+
+    </RelativeLayout>
+
+</view>
diff --git a/res/layout/contact_tile_starred.xml b/res/layout/contact_tile_starred.xml
new file mode 100644
index 0000000..777cc05
--- /dev/null
+++ b/res/layout/contact_tile_starred.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.
+-->
+<view
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:ex="http://schemas.android.com/apk/res-auto"
+    class="com.android.contacts.common.list.ContactTileStarredView"
+    android:focusable="true"
+    android:background="?android:attr/selectableItemBackground">
+
+    <LinearLayout
+        android:id="@+id/contact_tile_push_state"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:paddingTop="24dp">
+        <view
+            android:id="@+id/contact_tile_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            ex:direction="widthToHeight"
+            ex:ratio="1.0"
+            class="com.android.contacts.common.widget.ProportionalLayout" >
+            <ImageView
+                android:id="@+id/contact_tile_image"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+        </view>
+        <TextView
+            android:id="@+id/contact_tile_name"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingTop="7dp"
+            android:textColor="#202020"
+            android:textSize="@dimen/contact_browser_list_item_text_size"
+            android:singleLine="true"
+            android:fadingEdge="horizontal"
+            android:fadingEdgeLength="3dip"
+            android:ellipsize="marquee"
+            android:textAlignment="center"/>
+    </LinearLayout>
+</view>
diff --git a/res/layout/contact_tile_starred_quick_contact.xml b/res/layout/contact_tile_starred_quick_contact.xml
new file mode 100644
index 0000000..ecbe583
--- /dev/null
+++ b/res/layout/contact_tile_starred_quick_contact.xml
@@ -0,0 +1,82 @@
+<?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.
+-->
+<view
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:paddingBottom="1dip"
+    android:paddingRight="1dip"
+    android:paddingEnd="1dip"
+    android:background="@null"
+    class="com.android.contacts.common.list.ContactTileStarredView" >
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" >
+
+        <com.android.contacts.common.widget.LayoutSuppressingImageView
+            android:id="@+id/contact_tile_image"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scaleType="centerCrop" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/contact_tile_shadowbox_height"
+            android:orientation="vertical"
+            android:background="@color/contact_tile_shadow_box_color"
+            android:layout_alignParentBottom="true"
+            android:gravity="center_vertical"
+            android:paddingLeft="8dip"
+            android:paddingRight="8dip"
+            android:paddingStart="8dip"
+            android:paddingEnd="8dip">
+
+            <TextView
+                android:id="@+id/contact_tile_name"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textColor="@android:color/white"
+                android:textSize="16sp"
+                android:singleLine="true"
+                android:fadingEdge="horizontal"
+                android:fadingEdgeLength="3dip"
+                android:ellipsize="marquee"
+                android:textAlignment="viewStart" />
+
+            <TextView
+                android:id="@+id/contact_tile_status"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:textColor="@color/people_contact_tile_status_color"
+                android:singleLine="true"
+                android:drawablePadding="4dip"
+                android:fadingEdge="horizontal"
+                android:fadingEdgeLength="3dip"
+                android:layout_marginTop="-3dip"
+                android:ellipsize="marquee" />
+
+        </LinearLayout>
+
+        <QuickContactBadge
+            android:id="@+id/contact_tile_quick"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:focusable="true"
+            android:background="@null" />
+
+    </RelativeLayout>
+
+</view>
diff --git a/res/layout/custom_contact_list_filter_account.xml b/res/layout/custom_contact_list_filter_account.xml
new file mode 100644
index 0000000..c7a6cb5
--- /dev/null
+++ b/res/layout/custom_contact_list_filter_account.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:gravity="center_vertical"
+    android:paddingLeft="@dimen/contact_filter_list_item_padding_start"
+    android:paddingRight="?android:attr/scrollbarSize"
+    android:paddingStart="@dimen/contact_filter_list_item_padding_start"
+    android:paddingEnd="?android:attr/scrollbarSize">
+
+    <RelativeLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginRight="6dip"
+        android:layout_marginEnd="6dip"
+        android:layout_marginTop="6dip"
+        android:layout_marginBottom="6dip"
+        android:layout_weight="1"
+        android:duplicateParentState="true">
+
+        <TextView
+            android:id="@android:id/text1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:ellipsize="marquee"
+            android:textAppearance="?android:attr/textAppearanceListItem"
+            android:duplicateParentState="true" />
+
+        <TextView
+            android:id="@android:id/text2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@android:id/text1"
+            android:layout_alignLeft="@android:id/text1"
+            android:layout_alignStart="@android:id/text1"
+            android:maxLines="1"
+            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:textColor="?android:attr/textColorTertiary"
+            android:duplicateParentState="true" />
+
+    </RelativeLayout>
+
+</LinearLayout>
diff --git a/res/layout/custom_contact_list_filter_group.xml b/res/layout/custom_contact_list_filter_group.xml
new file mode 100644
index 0000000..c67ce16
--- /dev/null
+++ b/res/layout/custom_contact_list_filter_group.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="@dimen/contact_filter_list_item_height"
+    android:gravity="center_vertical"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:minHeight="@dimen/contact_filter_list_item_height"
+        android:gravity="center_vertical"
+        android:paddingLeft="@dimen/contact_filter_list_item_padding_start"
+        android:paddingRight="?android:attr/scrollbarSize"
+        android:paddingStart="@dimen/contact_filter_list_item_padding_start"
+        android:paddingEnd="?android:attr/scrollbarSize">
+
+        <RelativeLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="6dip"
+            android:layout_marginEnd="6dip"
+            android:layout_marginTop="6dip"
+            android:layout_marginBottom="6dip"
+            android:layout_weight="1"
+            android:duplicateParentState="true"
+        >
+
+            <TextView
+                android:id="@android:id/text1"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:singleLine="true"
+                android:ellipsize="marquee"
+                android:textAppearance="?android:attr/textAppearanceListItem"
+                android:textColor="@color/account_filter_text_color"
+                android:duplicateParentState="true"
+            />
+
+            <TextView
+                android:id="@android:id/text2"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@android:id/text1"
+                android:layout_alignLeft="@android:id/text1"
+                android:layout_alignStart="@android:id/text1"
+                android:maxLines="2"
+                android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+                android:textColor="@color/account_filter_text_color"
+                android:duplicateParentState="true"
+            />
+
+        </RelativeLayout>
+
+        <CheckBox
+            android:id="@android:id/checkbox"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:focusable="false"
+            android:clickable="false"
+            android:gravity="center_vertical"
+            android:orientation="vertical"
+            android:duplicateParentState="true"
+        />
+
+    </LinearLayout>
+
+    <View
+        android:id="@+id/adapter_divider_bottom"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="@color/custom_filter_divider" />
+
+</LinearLayout>
diff --git a/res/layout/default_account_checkbox.xml b/res/layout/default_account_checkbox.xml
new file mode 100644
index 0000000..9a1a450
--- /dev/null
+++ b/res/layout/default_account_checkbox.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/default_account_checkbox_layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="4dp"
+    android:orientation="vertical">
+    <CheckBox
+        android:id="@+id/default_account_checkbox_view"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingStart="15dip"
+        android:layout_marginLeft="13dip"
+        android:layout_marginBottom="20dip"
+        android:gravity="center"
+        android:textAlignment="viewStart"
+        android:text="@string/set_default_account"
+        android:textColor="@color/dialtacts_secondary_text_color"
+        />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/dialog_call_subject.xml b/res/layout/dialog_call_subject.xml
new file mode 100644
index 0000000..d6365c2
--- /dev/null
+++ b/res/layout/dialog_call_subject.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2015 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/transparent"
+    android:id="@+id/call_subject_dialog"
+    android:orientation="vertical">
+
+    <RelativeLayout
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:layout_width="match_parent">
+
+        <!-- The call subject dialog will be centered in the space above the subject list. -->
+        <LinearLayout
+            android:id="@+id/dialog_view"
+            android:orientation="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:clickable="true"
+            android:theme="@android:style/Theme.Material.Light.Dialog"
+            android:elevation="16dp"
+            android:layout_centerInParent="true"
+            android:background="@drawable/dialog_background_material">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                android:layout_marginStart="@dimen/call_subject_dialog_margin"
+                android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+                android:layout_marginTop="@dimen/call_subject_dialog_margin">
+
+                <QuickContactBadge
+                    android:id="@+id/contact_photo"
+                    android:layout_width="@dimen/call_subject_dialog_contact_photo_size"
+                    android:layout_height="@dimen/call_subject_dialog_contact_photo_size"
+                    android:layout_gravity="top"
+                    android:focusable="true"
+                    android:layout_marginEnd="@dimen/call_subject_dialog_margin" />
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical"
+                    android:gravity="center_vertical">
+
+                    <TextView
+                        android:id="@+id/name"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:textColor="@color/dialtacts_primary_text_color"
+                        android:textSize="@dimen/call_subject_dialog_secondary_text_size"
+                        android:singleLine="true" />
+
+                    <TextView
+                        android:id="@+id/number"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_gravity="center_vertical"
+                        android:layout_marginTop="@dimen/call_subject_dialog_between_line_margin"
+                        android:textColor="@color/dialtacts_secondary_text_color"
+                        android:textSize="@dimen/call_subject_dialog_secondary_text_size"
+                        android:singleLine="true" />
+                </LinearLayout>
+            </LinearLayout>
+
+            <EditText
+                android:id="@+id/call_subject"
+                android:hint="@string/call_subject_hint"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                android:layout_gravity="top"
+                android:textColor="@color/dialtacts_secondary_text_color"
+                android:textSize="@dimen/call_subject_dialog_secondary_text_size"
+                android:gravity="top"
+                android:background="@null"
+                android:layout_marginTop="@dimen/call_subject_dialog_edit_spacing"
+                android:layout_marginStart="@dimen/call_subject_dialog_margin"
+                android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+                />
+
+            <TextView
+                android:id="@+id/character_limit"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textColor="@color/dialtacts_secondary_text_color"
+                android:textSize="@dimen/call_subject_dialog_secondary_text_size"
+                android:singleLine="true"
+                android:layout_marginStart="@dimen/call_subject_dialog_margin"
+                android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+                android:layout_marginTop="@dimen/call_subject_dialog_margin"
+                android:layout_marginBottom="@dimen/call_subject_dialog_margin"/>
+
+            <View
+                android:layout_width="fill_parent"
+                android:layout_height="1dp"
+                android:background="@color/call_subject_divider"/>
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/call_subject_dialog_margin"
+                android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+                android:layout_marginTop="@dimen/call_subject_dialog_margin"
+                android:layout_marginBottom="@dimen/call_subject_dialog_margin">
+
+                <ImageView
+                    android:id="@+id/history_button"
+                    android:layout_width="25dp"
+                    android:layout_height="25dp"
+                    android:src="@drawable/ic_history_white_drawable_24dp"
+                    android:tint="@color/call_subject_history_icon"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true" />
+
+                <TextView
+                    android:id="@+id/send_and_call_button"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/send_and_call_button"
+                    android:textColor="@color/call_subject_button"
+                    android:textSize="@dimen/call_subject_dialog_secondary_text_size"
+                    android:singleLine="true"
+                    android:layout_alignParentEnd="true"
+                    android:layout_centerVertical="true" />
+
+            </RelativeLayout>
+        </LinearLayout>
+    </RelativeLayout>
+    <!-- The subject list is pinned to the bottom of the screen. -->
+    <ListView
+        android:id="@+id/subject_list"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:layout_below="@id/dialog_view"
+        android:background="@color/call_subject_history_background"
+        android:divider="@null"
+        android:elevation="8dp" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/dialog_title.xml b/res/layout/dialog_title.xml
new file mode 100644
index 0000000..b88f47c
--- /dev/null
+++ b/res/layout/dialog_title.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:paddingTop="18dp"
+    android:paddingStart="24dp"
+    android:paddingEnd="24dp"
+    android:textSize="20sp"
+    android:textColor="@color/contacts_text_color"
+    android:fontFamily="sans-serif-medium"
+    android:ellipsize="end"
+/>
\ No newline at end of file
diff --git a/res/layout/directory_header.xml b/res/layout/directory_header.xml
new file mode 100644
index 0000000..3e41aa7
--- /dev/null
+++ b/res/layout/directory_header.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Layout used for list section separators. -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/DirectoryHeader"
+    android:id="@+id/directory_header"
+    android:background="?attr/contact_browser_background"
+    android:paddingLeft="@dimen/directory_header_left_padding"
+    android:paddingRight="?attr/list_item_padding_right"
+    android:paddingStart="@dimen/directory_header_left_padding"
+    android:paddingEnd="?attr/list_item_padding_right"
+    android:paddingTop="@dimen/directory_header_extra_top_padding"
+    android:paddingBottom="@dimen/directory_header_extra_bottom_padding"
+    android:minHeight="@dimen/list_section_divider_min_height"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content" >
+    <TextView
+        android:id="@+id/label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/DirectoryHeaderStyle"
+        android:singleLine="true"
+        android:textAlignment="viewStart" />
+    <TextView
+        android:id="@+id/display_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="8dp"
+        android:textAppearance="@style/DirectoryHeaderStyle"
+        android:singleLine="true"
+        android:textAlignment="viewStart" />
+    <TextView
+        android:id="@+id/count"
+        android:paddingTop="1dip"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:gravity="end"
+        android:singleLine="true"
+        android:textAppearance="@style/DirectoryHeaderStyle" />
+</LinearLayout>
diff --git a/res/layout/licenses.xml b/res/layout/licenses.xml
new file mode 100644
index 0000000..66d4f46
--- /dev/null
+++ b/res/layout/licenses.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2015 Google Inc. All Rights Reserved. -->
+
+<WebView
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/webview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+</WebView>
\ No newline at end of file
diff --git a/res/layout/list_separator.xml b/res/layout/list_separator.xml
new file mode 100644
index 0000000..80abacb
--- /dev/null
+++ b/res/layout/list_separator.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/title"
+    android:textColor="@color/frequently_contacted_title_color"
+    android:paddingLeft="16dip"
+    android:paddingStart="16dip"
+    android:paddingRight="16dip"
+    android:paddingEnd="16dip"
+    android:paddingBottom="15dip"
+    android:paddingTop="16dip"
+    android:textStyle="bold"
+    android:textSize="@dimen/frequently_contacted_title_text_size"/>
diff --git a/res/layout/search_bar_expanded.xml b/res/layout/search_bar_expanded.xml
new file mode 100644
index 0000000..ecadbd1
--- /dev/null
+++ b/res/layout/search_bar_expanded.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/search_box_expanded"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal"
+    android:gravity="center_vertical"
+    android:visibility="gone" >
+
+    <ImageButton
+        android:id="@+id/search_back_button"
+        android:layout_width="@dimen/search_box_icon_size"
+        android:layout_height="@dimen/search_box_icon_size"
+        android:layout_marginEnd="@dimen/search_box_navigation_icon_margin"
+        android:src="@drawable/ic_back_arrow"
+        android:background="?attr/selectableItemBackgroundBorderless"
+        android:contentDescription="@string/action_menu_back_from_search"
+        android:tint="@color/actionbar_background_color" />
+
+    <android.widget.AutoCompleteTextView
+        android:id="@+id/search_view"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/search_box_icon_size"
+        android:layout_weight="1"
+        android:layout_marginLeft="@dimen/search_box_text_left_margin"
+        android:textSize="@dimen/search_text_size"
+        android:fontFamily="@string/search_font_family"
+        android:textColor="@color/searchbox_text_color"
+        android:textColorHint="@color/searchbox_hint_text_color"
+        android:textCursorDrawable="@drawable/searchedittext_custom_cursor"
+        android:background="@null"
+        android:inputType="textFilter"
+        android:singleLine="true"
+        android:imeOptions="flagNoExtractUi" />
+
+    <ImageView
+        android:id="@+id/search_close_button"
+        android:layout_height="@dimen/search_box_close_icon_size"
+        android:layout_width="@dimen/search_box_close_icon_size"
+        android:padding="@dimen/search_box_close_icon_padding"
+        android:src="@drawable/ic_close_black_24dp"
+        android:clickable="true"
+        android:background="?attr/selectableItemBackgroundBorderless"
+        android:contentDescription="@string/description_clear_search"
+        android:alpha="@dimen/close_icon_alpha" />
+
+</LinearLayout>
diff --git a/res/layout/select_account_list_item.xml b/res/layout/select_account_list_item.xml
new file mode 100644
index 0000000..0ba4336
--- /dev/null
+++ b/res/layout/select_account_list_item.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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 of a single item in the InCallUI Account Chooser Dialog. -->
+<view class="com.android.contacts.common.widget.ActivityTouchLinearLayout"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="8dp" >
+
+    <ImageView android:id="@+id/icon"
+               android:layout_width="48dp"
+               android:layout_height="48dp"
+               android:scaleType="center" />
+
+    <LinearLayout
+              android:id="@+id/text"
+              android:gravity="start|center_vertical"
+              android:layout_marginLeft="8dp"
+              android:layout_width="0dp"
+              android:layout_weight="1"
+              android:layout_height="match_parent"
+              android:orientation="vertical" >
+         <TextView android:id="@+id/label"
+                   android:textAppearance="?android:attr/textAppearanceMedium"
+                   android:textColor="@color/dialtacts_primary_text_color"
+                   android:includeFontPadding="false"
+                   android:layout_width="match_parent"
+                   android:layout_height="wrap_content" />
+         <TextView android:id="@+id/number"
+                   android:textAppearance="?android:attr/textAppearanceSmall"
+                   android:includeFontPadding="false"
+                   android:layout_width="match_parent"
+                   android:layout_height="wrap_content"
+                   android:visibility="gone" />
+    </LinearLayout>
+
+</view>
diff --git a/res/layout/select_dialog_item.xml b/res/layout/select_dialog_item.xml
new file mode 100644
index 0000000..0c524fd
--- /dev/null
+++ b/res/layout/select_dialog_item.xml
@@ -0,0 +1,32 @@
+<?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.
+-->
+
+<!--
+  List item in the pop-up window that appears when tapping a contact's photo
+  in the contact editor. This is similar to the framework's select_dialog_item_material.xml layout
+  except the text appearance is medium and the padding is set to match the material spec.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/text1"
+    android:layout_width="match_parent"
+    android:layout_height="48dp"
+    android:textAlignment="viewStart"
+    android:textColor="@color/contacts_text_color"
+    android:textSize="16sp"
+    android:gravity="center_vertical"
+    android:paddingStart="24dip"
+    android:paddingEnd="24dip"
+    android:ellipsize="marquee" />
\ No newline at end of file
diff --git a/res/layout/unread_count_tab.xml b/res/layout/unread_count_tab.xml
new file mode 100644
index 0000000..783f1c1
--- /dev/null
+++ b/res/layout/unread_count_tab.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/view_pager_tab_background">
+    <!-- The tab icon -->
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true" />
+    <TextView
+        android:id="@+id/count"
+        android:background="@drawable/unread_count_background"
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/tab_unread_count_background_size"
+        android:gravity="center"
+        android:minWidth="@dimen/tab_unread_count_background_size"
+        android:layout_marginStart="@dimen/tab_unread_count_margin_left"
+        android:layout_marginTop="@dimen/tab_unread_count_margin_top"
+        android:layout_toEndOf="@id/icon"
+        android:paddingLeft="@dimen/tab_unread_count_text_padding"
+        android:paddingRight="@dimen/tab_unread_count_text_padding"
+        android:textAlignment="center"
+        android:textSize="@dimen/tab_unread_count_text_size"
+        android:textColor="@color/tab_accent_color"
+        android:fontFamily="sans-serif-medium"
+        android:importantForAccessibility="no" />
+</RelativeLayout>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 8b73bad..945980d 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Stel verstek op"</string>
     <string name="clear_default" msgid="7193185801596678067">"Vee verstek uit"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Teks gekopieer"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Gooi jou veranderings weg en hou op om te wysig?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Ignoreer veranderinge?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Gooi weg"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Hou aan wysig"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Kanselleer"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Deursoek kontakte"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Verwyder kontakte"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Meer oor"</string>
     <string name="send_message" msgid="8938418965550543196">"Stuur boodskap"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Skep tans \'n persoonlike kopie..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Gister"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Môre"</string>
     <string name="today" msgid="8041090779381781781">"Vandag"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Vandag om <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 1bfc997..ff9d817 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"ነባሪ አዘጋጅ"</string>
     <string name="clear_default" msgid="7193185801596678067">"ነባሪ አጽዳ"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"ፅሁፍ ገልብጧል"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"ለውጦችዎ ይወገዱ እና ማርትዕ ይቁም?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"ለውጦች ይወገዱ?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"አስወግድ"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"አርትዖቱን ቀጥል"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"ተወው"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"እውቅያዎችን ፈልግ"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"ዕውቂያዎችን ያስወግዱ"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"ስለ"</string>
     <string name="send_message" msgid="8938418965550543196">"መልዕክት ላክ"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"የግል ቅጂ በመፍጠር ላይ..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"ትላንት"</string>
     <string name="tomorrow" msgid="6241969467795308581">"ነገ"</string>
     <string name="today" msgid="8041090779381781781">"ዛሬ"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"ዛሬ <xliff:g id="TIME_INTERVAL">%s</xliff:g> ላይ"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 0df6033..e0ebf86 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -260,9 +260,9 @@
     <string name="set_default" msgid="4417505153468300351">"تعيين كافتراضي"</string>
     <string name="clear_default" msgid="7193185801596678067">"محو الإعدادات الافتراضية"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"تم نسخ النص"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"هل تريد تجاهل التغييرات ومغادرة التعديل؟"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"هل تريد إلغاء التغييرات؟"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"تجاهل"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"متابعة التعديلات"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"إلغاء"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"البحث في جهات الاتصال"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"إزالة جهات الاتصال"</string>
@@ -287,7 +287,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"حول"</string>
     <string name="send_message" msgid="8938418965550543196">"إرسال رسالة"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"جارٍ إنشاء نسخة شخصية..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"أمس"</string>
     <string name="tomorrow" msgid="6241969467795308581">"غدًا"</string>
     <string name="today" msgid="8041090779381781781">"اليوم"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"اليوم في <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-az-rAZ/strings.xml b/res/values-az-rAZ/strings.xml
index 2557079..45579aa 100644
--- a/res/values-az-rAZ/strings.xml
+++ b/res/values-az-rAZ/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Defolt ayarlayın"</string>
     <string name="clear_default" msgid="7193185801596678067">"Defoltu təmizləyin"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Mətn kopyalandı"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Dəyişiklikləriniz kənarlaşdırılsın və redaktə sonlandırılsın?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Dəyişikliklər ləğv edilsin?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Ləğv edin"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Redaktəyə davam edin"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Ləğv edin"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Kontakt axtarın"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Kontaktları silin"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Haqqında"</string>
     <string name="send_message" msgid="8938418965550543196">"Mesaj göndərin"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Şəxsi nüsxə yaradılır..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Dünən"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Sabah"</string>
     <string name="today" msgid="8041090779381781781">"Bu gün"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Bu gün saat <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 6b0cb8d..13ee552 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Задаване като стандартна настройка"</string>
     <string name="clear_default" msgid="7193185801596678067">"Изчистване на стандартната настройка"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Текстът бе копиран"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Искате ли да отхвърлите направените от вас промени и да излезете от редактирането?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Отхвърляне"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Продължаване с редактирането"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Търсене в контактите"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Премахване на контакти"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Информация"</string>
     <string name="send_message" msgid="8938418965550543196">"Изпращане на съобщение"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Създава се лично копие..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Вчера"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Утре"</string>
     <string name="today" msgid="8041090779381781781">"Днес"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Днес от <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-bn-rBD/strings.xml b/res/values-bn-rBD/strings.xml
index 21ddf31..648de98 100644
--- a/res/values-bn-rBD/strings.xml
+++ b/res/values-bn-rBD/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"ডিফল্ট সেট করুন"</string>
     <string name="clear_default" msgid="7193185801596678067">"ডিফল্ট সাফ করুন"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"পাঠ্য অনুলিপি হয়েছে"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"আপনার পরিবর্তনগুলি বাতিল করতে এবং সম্পাদনা থেকে প্রস্থান করতে চান?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"পরিবর্তনগুলি বাতিল করবেন?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"বাতিল করুন"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"সম্পাদনা করা চালিয়ে যান"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"বাতিল করুন"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"পরিচিতিগুলি খুঁজুন"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"পরিচিতিগুলি সরান"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"সম্পর্কে"</string>
     <string name="send_message" msgid="8938418965550543196">"বার্তা পাঠান"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"একটি ব্যক্তিগত অনুলিপি তৈরি করা হচ্ছে…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"গতকাল"</string>
     <string name="tomorrow" msgid="6241969467795308581">"আগামীকাল"</string>
     <string name="today" msgid="8041090779381781781">"আজ"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"আজ <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 95dff95..cad9595 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Predeterminat"</string>
     <string name="clear_default" msgid="7193185801596678067">"Esborra els valors predeterminats"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Text copiat"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Vols descartar els canvis i sortir del mode d\'edició?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Vols descartar els canvis?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Descarta"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Continua editant"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Cancel·la"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Cerca als contactes"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Suprimeix els contactes"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Quant a"</string>
     <string name="send_message" msgid="8938418965550543196">"Envia un missatge"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"S\'està creant una còpia personal..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Ahir"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Demà"</string>
     <string name="today" msgid="8041090779381781781">"Avui"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Avui a les <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index de11f48..b6cf011 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -246,9 +246,9 @@
     <string name="set_default" msgid="4417505153468300351">"Výchozí nastavení"</string>
     <string name="clear_default" msgid="7193185801596678067">"Vymazat výchozí nastavení"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Text zkopírován"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Zahodit změny a ukončit úpravy?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Zahodit změny?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Zrušit"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Pokračovat v úpravách"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Zrušit"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Vyhledejte kontakty"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Odebrat kontakty"</string>
@@ -273,7 +273,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"O kartě"</string>
     <string name="send_message" msgid="8938418965550543196">"Odeslat zprávu"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Vytváření osobní kopie..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Včera"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Zítra"</string>
     <string name="today" msgid="8041090779381781781">"Dnes"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Dnes v <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 6a02ce4..20438a8 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Indstil standard"</string>
     <string name="clear_default" msgid="7193185801596678067">"Ryd standarder"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Kopieret tekst"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Vil du kassere ændringerne og afslutte redigering?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Vil du kassere ændringerne?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Kassér"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Fortsæt redigering"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Annuller"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Søg i kontaktpersoner"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Fjern kontaktpersoner"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Om"</string>
     <string name="send_message" msgid="8938418965550543196">"Send besked"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Opretter en privat kopi..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"I går"</string>
     <string name="tomorrow" msgid="6241969467795308581">"I morgen"</string>
     <string name="today" msgid="8041090779381781781">"I dag"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"I dag kl. <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index f2566fb..dc14b93 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Als Standard festlegen"</string>
     <string name="clear_default" msgid="7193185801596678067">"Als Standard löschen"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Text kopiert"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Möchtest du die Änderungen verwerfen und den Bearbeitungsmodus beenden?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Verwerfen"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Bearbeitung fortsetzen"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Kontakte suchen"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Kontakte entfernen"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Info"</string>
     <string name="send_message" msgid="8938418965550543196">"Nachricht senden"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Persönliche Kopie wird erstellt..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Gestern"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Morgen"</string>
     <string name="today" msgid="8041090779381781781">"Heute"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Heute um <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 15fa54f..0499bed 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Ορισμός ως προεπιλογή"</string>
     <string name="clear_default" msgid="7193185801596678067">"Εκκαθάριση προεπιλεγμένων"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Το κείμενο αντιγράφηκε"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Να απορριφθούν οι αλλαγές που πραγματοποιήσατε και να τερματιστεί η επεξεργασία;"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Να απορριφθούν οι αλλαγές;"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Απόρριψη"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Συνέχεια επεξεργασίας"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Ακύρωση"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Αναζήτηση επαφών"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Κατάργηση επαφών"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Σχετικά με"</string>
     <string name="send_message" msgid="8938418965550543196">"Αποστολή μηνύματος"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Δημιουργία προσωπικού αντιγράφου..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Χθες"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Αύριο"</string>
     <string name="today" msgid="8041090779381781781">"Σήμερα"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Σήμερα στις <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 72374d5..012cd31 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Set default"</string>
     <string name="clear_default" msgid="7193185801596678067">"Clear default"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Text copied"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Discard your changes and quit editing?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Discard changes?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Discard"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Keep editing"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Cancel"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Search contacts"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Remove contacts"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"About"</string>
     <string name="send_message" msgid="8938418965550543196">"Send message"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Creating a personal copy…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Yesterday"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Tomorrow"</string>
     <string name="today" msgid="8041090779381781781">"Today"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Today at <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 72374d5..012cd31 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Set default"</string>
     <string name="clear_default" msgid="7193185801596678067">"Clear default"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Text copied"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Discard your changes and quit editing?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Discard changes?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Discard"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Keep editing"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Cancel"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Search contacts"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Remove contacts"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"About"</string>
     <string name="send_message" msgid="8938418965550543196">"Send message"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Creating a personal copy…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Yesterday"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Tomorrow"</string>
     <string name="today" msgid="8041090779381781781">"Today"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Today at <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 72374d5..012cd31 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Set default"</string>
     <string name="clear_default" msgid="7193185801596678067">"Clear default"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Text copied"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Discard your changes and quit editing?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Discard changes?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Discard"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Keep editing"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Cancel"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Search contacts"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Remove contacts"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"About"</string>
     <string name="send_message" msgid="8938418965550543196">"Send message"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Creating a personal copy…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Yesterday"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Tomorrow"</string>
     <string name="today" msgid="8041090779381781781">"Today"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Today at <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 7805c5e..b7140f5 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Establecer como predeterminado"</string>
     <string name="clear_default" msgid="7193185801596678067">"Eliminar predeterminado"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Texto copiado"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"¿Deseas descartar los cambios y salir del editor?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Descartar"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Seguir editando"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Buscar contactos"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Quitar contactos"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Información"</string>
     <string name="send_message" msgid="8938418965550543196">"Enviar mensaje"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Creando una copia personal..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Ayer"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Mañana"</string>
     <string name="today" msgid="8041090779381781781">"Hoy"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Hoy a la hora <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 3461637..76c981a 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Establecer como predeterminado"</string>
     <string name="clear_default" msgid="7193185801596678067">"Borrar predeterminado"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Texto copiado"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"¿Descartar los cambios y dejar de editar?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Descartar"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Seguir editando"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Buscar contactos"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Quitar contactos"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Información"</string>
     <string name="send_message" msgid="8938418965550543196">"Enviar mensaje"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Creando una copia personal..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Ayer"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Mañana"</string>
     <string name="today" msgid="8041090779381781781">"Hoy"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Hoy a las <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-et-rEE/strings.xml b/res/values-et-rEE/strings.xml
index 0799d56..adafcba 100644
--- a/res/values-et-rEE/strings.xml
+++ b/res/values-et-rEE/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Määra vaikeseadeks"</string>
     <string name="clear_default" msgid="7193185801596678067">"Kustuta vaikeseaded"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Tekst on kopeeritud"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Kas soovite muudatustest loobuda ja muutmise lõpetada?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Loobu"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Jätka muutmist"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Otsige kontakte"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Kontaktide eemaldamine"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Teave"</string>
     <string name="send_message" msgid="8938418965550543196">"Saada sõnum"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Isikliku koopia loomine ..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Eile"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Homme"</string>
     <string name="today" msgid="8041090779381781781">"Täna"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Täna kell <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-eu-rES/strings.xml b/res/values-eu-rES/strings.xml
index 0b9d40e..b61aa9d 100644
--- a/res/values-eu-rES/strings.xml
+++ b/res/values-eu-rES/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Ezarri lehenetsi gisa"</string>
     <string name="clear_default" msgid="7193185801596678067">"Garbitu metodo lehenetsia"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Testua kopiatu da"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Aldaketak baztertu eta editatzeko modutik irten nahi duzu?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Baztertu"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Jarraitu editatzen"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> (<xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>)"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Bilatu kontaktuetan"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Kendu kontaktuak"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Honi buruz"</string>
     <string name="send_message" msgid="8938418965550543196">"Bidali mezua"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Kopia pertsonala sortzen…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Atzo"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Bihar"</string>
     <string name="today" msgid="8041090779381781781">"Gaur"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Gaur (<xliff:g id="TIME_INTERVAL">%s</xliff:g>)"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index cf23fc4..b4ea335 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"تنظیم پیش‌فرض"</string>
     <string name="clear_default" msgid="7193185801596678067">"پاک کردن پیش فرض‌ها"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"متن کپی شده"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"از تغییراتتان صرف‌نظر می‌کنید و از ویرایش خارج می‌شوید؟"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"صرف‌نظر کردن"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"حفظ ویرایش"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"جستجوی مخاطبین"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"حذف مخاطبین"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"درباره"</string>
     <string name="send_message" msgid="8938418965550543196">"ارسال پیام"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"ایجاد یک کپی شخصی..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"دیروز"</string>
     <string name="tomorrow" msgid="6241969467795308581">"فردا"</string>
     <string name="today" msgid="8041090779381781781">"امروز"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"امروز در <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 48ae68c..c66387a 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Aseta oletukseksi"</string>
     <string name="clear_default" msgid="7193185801596678067">"Poista oletus"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Teksti kopioitu"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Hylätäänkö muutokset ja lopetetaan muokkaaminen?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Hylkää"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Jatka muokkausta"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Hae yhteystiedoista"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Poista yhteystiedot"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Tietoja"</string>
     <string name="send_message" msgid="8938418965550543196">"Lähetä viesti"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Luodaan kopio..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Eilen"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Huomenna"</string>
     <string name="today" msgid="8041090779381781781">"Tänään"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Tänään klo <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index bb9a26e..75530f6 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Définir par défaut"</string>
     <string name="clear_default" msgid="7193185801596678067">"Effacer les valeurs par défaut"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Texte copié."</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Annuler les modifications et quitter le mode d\'édition?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Ignorer"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Continuer les modifications"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Rechercher dans les contacts"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Supprimer les contacts"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"À propos"</string>
     <string name="send_message" msgid="8938418965550543196">"Envoyer un message"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Création d\'une copie personnelle…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Hier"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Demain"</string>
     <string name="today" msgid="8041090779381781781">"Aujourd\'hui"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Aujourd\'hui à <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index de8df9c..9e0ac6c 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Définir par défaut"</string>
     <string name="clear_default" msgid="7193185801596678067">"Effacer les valeurs par défaut"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Texte copié"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Supprimer les modifications et quitter le mode d\'édition ?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Ignorer les modifications ?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Supprimer"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Poursuivre les modifications"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Annuler"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Rechercher des contacts"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Supprimer les contacts"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"À propos de"</string>
     <string name="send_message" msgid="8938418965550543196">"Envoyer le message"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Création d\'une copie personnelle…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Hier"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Demain"</string>
     <string name="today" msgid="8041090779381781781">"Aujourd\'hui"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Aujourd\'hui, <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-gl-rES/strings.xml b/res/values-gl-rES/strings.xml
index fca4cc1..bc3cd22 100644
--- a/res/values-gl-rES/strings.xml
+++ b/res/values-gl-rES/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Definir como predeterminado"</string>
     <string name="clear_default" msgid="7193185801596678067">"Borrar valores predeterminados"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Texto copiado"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Queres descartar os teus cambios e deixar de editar?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Descartar"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Seguir editando"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Busca nos contactos"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Eliminar contactos"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Acerca de"</string>
     <string name="send_message" msgid="8938418965550543196">"Enviar mensaxe"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Creando unha copia persoal..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Onte"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Mañá"</string>
     <string name="today" msgid="8041090779381781781">"Hoxe"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Hoxe ás <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-gu-rIN/strings.xml b/res/values-gu-rIN/strings.xml
index 3d3fcc5..48245be 100644
--- a/res/values-gu-rIN/strings.xml
+++ b/res/values-gu-rIN/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"ડિફોલ્ટ સેટ કરો"</string>
     <string name="clear_default" msgid="7193185801596678067">"ડિફોલ્ટ સાફ કરો"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"ટેક્સ્ટ કૉપિ કર્યો"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"તમારા ફેરફારોને નિકાળીને સંપાદન છોડી દઈએ?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"ફેરફારો નિકાળીએ?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"નિકાળો"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"સંપાદન ચાલુ રાખો"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"રદ કરો"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"સંપર્કો શોધો"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"સંપર્કો દૂર કરો"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"વિશે"</string>
     <string name="send_message" msgid="8938418965550543196">"સંદેશ મોકલો"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"વ્યક્તિગત કૉપિ બનાવી રહ્યાં છે…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"ગઈ કાલે"</string>
     <string name="tomorrow" msgid="6241969467795308581">"આવતીકાલે"</string>
     <string name="today" msgid="8041090779381781781">"આજે"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"<xliff:g id="TIME_INTERVAL">%s</xliff:g> વાગ્યે આજે"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 9ee58ff..329ca2b 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"सामान्य सेट करें"</string>
     <string name="clear_default" msgid="7193185801596678067">"सामान्य साफ़ करें"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"लेख की प्रतिलिपि बनाई गई"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"अपने बदलावों को ख़ारिज करें और संपादन से बाहर निकलें?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"क्या परिवर्तनों को छोड़ना चाहते हैं?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"अभी नहीं"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"संपादित करते रहें"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"रद्द करें"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"संपर्क खोजें"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"संपर्क निकालें"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"संक्षिप्त विवरण"</string>
     <string name="send_message" msgid="8938418965550543196">"संदेश भेजें"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"एक व्‍यक्तिगत प्रतिलिपि बना रहा है…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"कल"</string>
     <string name="tomorrow" msgid="6241969467795308581">"कल"</string>
     <string name="today" msgid="8041090779381781781">"आज"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"आज <xliff:g id="TIME_INTERVAL">%s</xliff:g> बजे"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index ca0d695..113f39a 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -239,9 +239,11 @@
     <string name="set_default" msgid="4417505153468300351">"Postavi zadano"</string>
     <string name="clear_default" msgid="7193185801596678067">"Izbriši zadano"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Tekst kopiran"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Želite li odbaciti promjene i prekinuti uređivanje?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Odbaci"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Nastavi uređivati"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Pretraživanje kontakata"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Uklanjanje kontakata"</string>
@@ -266,7 +268,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"O kartici"</string>
     <string name="send_message" msgid="8938418965550543196">"Pošalji poruku"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Stvaranje osobne kopije..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Jučer"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Sutra"</string>
     <string name="today" msgid="8041090779381781781">"Danas"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Danas u <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 454e07f..526009b 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Beállítás alapértelmezettként"</string>
     <string name="clear_default" msgid="7193185801596678067">"Alapértelmezés törlése"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Másolt szöveg"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Elveti a módosításokat, és kilép a szerkesztésből?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Elvetés"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Szerkesztés folytatása"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> -- <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Keresés a névjegyek között"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Névjegyek eltávolítása"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Névjegy"</string>
     <string name="send_message" msgid="8938418965550543196">"Üzenet küldése"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Személyes másolat készítése..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Tegnap"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Holnap"</string>
     <string name="today" msgid="8041090779381781781">"Ma"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Ma <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-hy-rAM/strings.xml b/res/values-hy-rAM/strings.xml
index 2c80ad2..5980d2c 100644
--- a/res/values-hy-rAM/strings.xml
+++ b/res/values-hy-rAM/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Սահմանել լռելյայն"</string>
     <string name="clear_default" msgid="7193185801596678067">"Մաքրել լռելյայն"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Տեքսը պատճենված է"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Անտեսե՞լ փոփոխությունները և դադարեցնել խմբագրումը:"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Անտեսել"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Շարունակել խմբագրումը"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Որոնեք կոնտակտներ"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Կոնտակտների հեռացում"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Տեղեկատվություն"</string>
     <string name="send_message" msgid="8938418965550543196">"Ուղարկել հաղորդագրություն"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Ստեղծվում է անձնական պատճենը..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Երեկ"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Վաղը"</string>
     <string name="today" msgid="8041090779381781781">"Այսօր"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Այսօր՝ <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 598d168..9df66c9 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Setel sebagai default"</string>
     <string name="clear_default" msgid="7193185801596678067">"Hapus default"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Teks disalin"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Buang perubahan dan berhenti mengedit?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Buang"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Simpan pengeditan"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Telusuri kontak"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Hapus kontak"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Tentang"</string>
     <string name="send_message" msgid="8938418965550543196">"Kirim pesan"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Membuat salinan pribadi..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Kemarin"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Besok"</string>
     <string name="today" msgid="8041090779381781781">"Hari ini"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Hari ini pukul <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-is-rIS/strings.xml b/res/values-is-rIS/strings.xml
index 0bae040..01fd6d2 100644
--- a/res/values-is-rIS/strings.xml
+++ b/res/values-is-rIS/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Velja sem sjálfgefið"</string>
     <string name="clear_default" msgid="7193185801596678067">"Hreinsa sjálfgefið"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Texti afritaður"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Viltu fleygja breytingum og hætta að breyta?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Fleygja"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Breyta áfram"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Leita í tengiliðum"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Fjarlægja tengiliði"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Um"</string>
     <string name="send_message" msgid="8938418965550543196">"Senda skilaboð"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Býr til afrit til einkanota…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Í gær"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Á morgun"</string>
     <string name="today" msgid="8041090779381781781">"Í dag"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Í dag klukkan <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 78ea37b..91075b5 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Imposta come predefinito"</string>
     <string name="clear_default" msgid="7193185801596678067">"Cancella impostazione predefinita"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Testo copiato"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Annullare le modifiche e uscire dalla modalità di modifica?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Ignorare le modifiche?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Ignora"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Continua la modifica"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Annulla"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Cerca contatti"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Rimuovi contatti"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Informazioni"</string>
     <string name="send_message" msgid="8938418965550543196">"Invia messaggio"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Creazione di una copia personale..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Ieri"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Domani"</string>
     <string name="today" msgid="8041090779381781781">"Oggi"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Oggi alle ore <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index ab62ba9..f5864ab 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -246,9 +246,9 @@
     <string name="set_default" msgid="4417505153468300351">"קבע כברירת מחדל"</string>
     <string name="clear_default" msgid="7193185801596678067">"נקה ברירת מחדל"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"טקסט שהועתק"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"האם להתעלם מהשינויים שביצעת ולהפסיק לערוך?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"האם לבטל את השינויים?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"מחק"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"המשך לערוך"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"ביטול"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"חיפוש אנשי קשר"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"הסרה של אנשי הקשר"</string>
@@ -273,7 +273,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"מידע כללי"</string>
     <string name="send_message" msgid="8938418965550543196">"שלח הודעה"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"יוצר עותק אישי..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"אתמול"</string>
     <string name="tomorrow" msgid="6241969467795308581">"מחר"</string>
     <string name="today" msgid="8041090779381781781">"היום"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"היום ב-<xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-ja/donottranslate_config.xml b/res/values-ja/donottranslate_config.xml
new file mode 100644
index 0000000..ff8a8eb
--- /dev/null
+++ b/res/values-ja/donottranslate_config.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, 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>
+    <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+    <bool name="config_sort_order_user_changeable">false</bool>
+
+    <!-- If true, the default sort order is primary (i.e. by given name) -->
+    <bool name="config_default_sort_order_primary">true</bool>
+
+    <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+    <bool name="config_display_order_user_changeable">false</bool>
+
+    <!-- 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">false</bool>
+
+    <!-- If true, phonetic name is included in the contact editor by default -->
+    <bool name="config_editor_include_phonetic_name">true</bool>
+</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 1857827..6e43b9d 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"デフォルトに設定"</string>
     <string name="clear_default" msgid="7193185801596678067">"デフォルトを解除"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"テキストをコピーしました"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"変更を破棄して編集を終了しますか?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"破棄"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"編集を続ける"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"連絡先を検索"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"連絡先の削除"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"概要"</string>
     <string name="send_message" msgid="8938418965550543196">"メッセージの送信"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"個人用コピーを作成しています..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"昨日"</string>
     <string name="tomorrow" msgid="6241969467795308581">"明日"</string>
     <string name="today" msgid="8041090779381781781">"今日"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"今日の<xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-ka-rGE/strings.xml b/res/values-ka-rGE/strings.xml
index 66ed650..a8205ea 100644
--- a/res/values-ka-rGE/strings.xml
+++ b/res/values-ka-rGE/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"ნაგულისხმევად დაყენება"</string>
     <string name="clear_default" msgid="7193185801596678067">"ნაგულისხმევის წაშლა"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"კოპირებული ტექსტი"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"გსურთ ცვლილებების გაუქმება და რედაქტირებიდან გასვლა?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"გაუქმება"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"რედაქტირების გაგრძელება"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"კონტაქტების ძიება"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"კონტაქტების წაშლა"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"შესახებ"</string>
     <string name="send_message" msgid="8938418965550543196">"შეტყობინების გაგზავნა"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"პირადი ასლის შექმნა…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"გუშინ"</string>
     <string name="tomorrow" msgid="6241969467795308581">"ხვალ"</string>
     <string name="today" msgid="8041090779381781781">"დღეს"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"დღეს <xliff:g id="TIME_INTERVAL">%s</xliff:g>-ზე"</string>
diff --git a/res/values-kk-rKZ/strings.xml b/res/values-kk-rKZ/strings.xml
index 500727e..2079469 100644
--- a/res/values-kk-rKZ/strings.xml
+++ b/res/values-kk-rKZ/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Бастапқы ретінде орнату"</string>
     <string name="clear_default" msgid="7193185801596678067">"Бастапқыны өшіру"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Мәтін көшірмесі жасалды"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Өзгертулерді алып тастап, өңдеуден шығу керек пе?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Өзгертулерді алып тастау қажет пе?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Алып тастау"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Өңдеуді жалғастыру"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Бас тарту"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Контактілерді іздеу"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Контактілерді жою"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Туралы"</string>
     <string name="send_message" msgid="8938418965550543196">"Хабарды жіберу"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Жеке көшірме жасау…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Кеше"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Ертең"</string>
     <string name="today" msgid="8041090779381781781">"Бүгін"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Бүгін, <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-km-rKH/strings.xml b/res/values-km-rKH/strings.xml
index c4d77fa..1897ec9 100644
--- a/res/values-km-rKH/strings.xml
+++ b/res/values-km-rKH/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"កំណត់​លំនាំដើម"</string>
     <string name="clear_default" msgid="7193185801596678067">"សម្អាត​លំនាំដើម"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"បាន​ចម្លង​អត្ថបទ"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"បោះបង់ការប្ដូររបស់អ្នក ហើយបញ្ឈប់ការកែសម្រួលមែនទេ?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"បោះបង់ការផ្លាស់ប្ដូរដែរ ឬ​ទេ?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"បោះបង់"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"នៅ​បន្ត​កែសម្រួល"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"បោះ​បង់​"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"ស្វែងរក​ទំនាក់ទំនង"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"ដក​ទំនាក់ទំនង​ចេញ"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"អំពី"</string>
     <string name="send_message" msgid="8938418965550543196">"ផ្ញើ​សារ"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"កំពុង​បង្កើត​ច្បាប់​ចម្លង​ផ្ទាល់​ខ្លួន..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"ម្សិលមិញ"</string>
     <string name="tomorrow" msgid="6241969467795308581">"ស្អែក"</string>
     <string name="today" msgid="8041090779381781781">"ថ្ងៃនេះ"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"ថ្ងៃនេះ​នៅ <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-kn-rIN/strings.xml b/res/values-kn-rIN/strings.xml
index 50ea5ac..a6cb3a4 100644
--- a/res/values-kn-rIN/strings.xml
+++ b/res/values-kn-rIN/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"ಡೀಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
     <string name="clear_default" msgid="7193185801596678067">"ಡಿಫಾಲ್ಟ್‌ ತೆರವುಗೊಳಿಸಿ"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"ಪಠ್ಯವನ್ನು ನಕಲಿಸಲಾಗಿದೆ"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"ನಿಮ್ಮ ಬದಲಾವಣೆಗಳನ್ನು ತ್ಯಜಿಸಿ ಸಂಪಾದನೆಯನ್ನು ನಿರ್ಗಮಿಸುವುದೇ?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"ಬದಲಾವಣೆಗಳನ್ನು ತ್ಯಜಿಸುವುದೇ?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"ತ್ಯಜಿಸು"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"ಸಂಪಾದಿಸುತ್ತಿರಿ"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"ರದ್ದುಮಾಡು"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"ಸಂಪರ್ಕಗಳನ್ನು ಹುಡುಕಿ"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"ಸಂಪರ್ಕಗಳನ್ನು ತೆಗೆದುಹಾಕಿ"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"ಕುರಿತು"</string>
     <string name="send_message" msgid="8938418965550543196">"ಸಂದೇಶ ಕಳುಹಿಸಿ"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"ವೈಯಕ್ತಿಕ ಪ್ರತಿಯನ್ನು ರಚಿಸಲಾಗುತ್ತಿದೆ…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"ನಿನ್ನೆ"</string>
     <string name="tomorrow" msgid="6241969467795308581">"ನಾಳೆ"</string>
     <string name="today" msgid="8041090779381781781">"ಇಂದು"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"<xliff:g id="TIME_INTERVAL">%s</xliff:g> ಕ್ಕೆ ಇಂದು"</string>
diff --git a/res/values-ko/donottranslate_config.xml b/res/values-ko/donottranslate_config.xml
new file mode 100644
index 0000000..14cdd4d
--- /dev/null
+++ b/res/values-ko/donottranslate_config.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, 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>
+    <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+    <bool name="config_sort_order_user_changeable">false</bool>
+
+    <!-- If true, the default sort order is primary (i.e. by given name) -->
+    <bool name="config_default_sort_order_primary">false</bool>
+
+    <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+    <bool name="config_display_order_user_changeable">false</bool>
+
+    <!-- If true, the default sort order is primary (i.e. by given name) -->
+    <bool name="config_default_display_order_primary">false</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">false</bool>
+</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 5c346de..7d87637 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"기본으로 설정"</string>
     <string name="clear_default" msgid="7193185801596678067">"기본 설정 지우기"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"텍스트 복사됨"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"변경사항을 취소하고 수정을 중단하시겠습니까?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"취소"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"계속 수정"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"연락처 검색"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"연락처 삭제"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"정보"</string>
     <string name="send_message" msgid="8938418965550543196">"메시지 보내기"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"개인 사본 작성 중..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"어제"</string>
     <string name="tomorrow" msgid="6241969467795308581">"내일"</string>
     <string name="today" msgid="8041090779381781781">"오늘"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"오늘 <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-ky-rKG/strings.xml b/res/values-ky-rKG/strings.xml
index 61c6594..725a533 100644
--- a/res/values-ky-rKG/strings.xml
+++ b/res/values-ky-rKG/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Негизги кылуу"</string>
     <string name="clear_default" msgid="7193185801596678067">"Негизгини тазалоо"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"SMS көчүрүлдү"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Өзгөртүүлөр жарактан чыгарылып, түзөтүү жабылсынбы?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Жарактан чыгаруу"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Түзөтө берүү"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Байланыштардан издеп көрүңүз"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Байланыштарды алып салуу"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Жөнүндө"</string>
     <string name="send_message" msgid="8938418965550543196">"Билдирүү жөнөтүү"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Жеке көчүрмөсүн түзүү…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Кечээ"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Эртең"</string>
     <string name="today" msgid="8041090779381781781">"Бүгүн"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Бүгүн саат <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-land/integers.xml b/res/values-land/integers.xml
index 8bc7b04..4b26d4c 100644
--- a/res/values-land/integers.xml
+++ b/res/values-land/integers.xml
@@ -22,4 +22,9 @@
 
     <!-- Top margin ratio for the image for empty account view -->
     <integer name="empty_account_view_image_margin_divisor">6</integer>
+
+    <integer name="contact_tile_column_count_in_favorites">3</integer>
+
+    <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+    <integer name="snippet_length_before_tokenize">60</integer>
 </resources>
diff --git a/res/values-lo-rLA/strings.xml b/res/values-lo-rLA/strings.xml
index 12bb70b..e7c5dd7 100644
--- a/res/values-lo-rLA/strings.xml
+++ b/res/values-lo-rLA/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"ຕັ້ງຄ່າເລີ່ມຕົ້ນ"</string>
     <string name="clear_default" msgid="7193185801596678067">"ລຶບຄ່າເລີ່ມຕົ້ນ"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"ສຳເນົາຂໍ້ຄວາມແລ້ວ"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"ຍົກເລີກການປ່ຽນແປງຂອງທ່ານ ແລະ ອອກຈາກການແກ້ໄຂບໍ?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"ຍົກເລີກການແກ້ໄຂບໍ່?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"ຍົກເລີກ"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"ສືບຕໍ່ແກ້ໄຂ"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"ຍົກເລີກ"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"ຊອກຫາລາຍຊື່ຜູ້ຕິດຕໍ່"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"ລຶບລາຍຊື່ຜູ້ຕິດຕໍ່"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"ກ່ຽວກັບ"</string>
     <string name="send_message" msgid="8938418965550543196">"ສົ່ງຂໍ້ຄວາມ"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"ກຳລັງສ້າງສຳເນົາສ່ວນໂຕ..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"ມື້​ວານ​ນີ້"</string>
     <string name="tomorrow" msgid="6241969467795308581">"ມື້ອື່ນ"</string>
     <string name="today" msgid="8041090779381781781">"ມື້ນີ້"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"ມື້ນີ້ໃນເວລາ <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 1e51eeb..6088bc9 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -246,9 +246,11 @@
     <string name="set_default" msgid="4417505153468300351">"Nustatyti numatytuosius nustatymus"</string>
     <string name="clear_default" msgid="7193185801596678067">"Išvalyti numatytuosius nustatymus"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Tekstas nukopijuotas"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Atmesti pakeitimus ir baigti redagavimą?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Atmesti"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Toliau redaguoti"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Ieškokite kontaktų"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Pašalinti kontaktus"</string>
@@ -273,7 +275,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Apie"</string>
     <string name="send_message" msgid="8938418965550543196">"Siųsti pranešimą"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Kuriama asmeninė kopija..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Vakar"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Rytoj"</string>
     <string name="today" msgid="8041090779381781781">"Šiandien"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Šiandien, <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 0bee0f6..326a4e8 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -239,9 +239,11 @@
     <string name="set_default" msgid="4417505153468300351">"Iestatīt kā noklusējumu"</string>
     <string name="clear_default" msgid="7193185801596678067">"Notīrīt noklusējuma iestatījumus"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Teksts ir nokopēts"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Vai atmest veiktās izmaiņas un beigt rediģēšanu?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Atmest"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Turpināt rediģēšanu"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> — <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Meklēt kontaktpersonas"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Kontaktpersonu noņemšana"</string>
@@ -266,7 +268,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Par"</string>
     <string name="send_message" msgid="8938418965550543196">"Sūtīt ziņojumu"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Notiek personīgā eksemplāra izveide..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Vakar"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Rīt"</string>
     <string name="today" msgid="8041090779381781781">"Šodien"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Šodien plkst. <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-mk-rMK/strings.xml b/res/values-mk-rMK/strings.xml
index 8e34554..880317a 100644
--- a/res/values-mk-rMK/strings.xml
+++ b/res/values-mk-rMK/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Постави стандарден метод"</string>
     <string name="clear_default" msgid="7193185801596678067">"Исчисти стандарден метод"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Текстот е копиран"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Дали да се отфрлат промените и да се прекине уредувањето?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Отфрли"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Продолжи со уредување"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Пребарајте контакти"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Отстрани контакти"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"За"</string>
     <string name="send_message" msgid="8938418965550543196">"Испрати порака"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Се создава лична копија..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Вчера"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Утре"</string>
     <string name="today" msgid="8041090779381781781">"Денес"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Денес во <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-ml-rIN/strings.xml b/res/values-ml-rIN/strings.xml
index 76c72ba..77af785 100644
--- a/res/values-ml-rIN/strings.xml
+++ b/res/values-ml-rIN/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"സ്ഥിരമായി സജ്ജമാക്കുക"</string>
     <string name="clear_default" msgid="7193185801596678067">"സ്ഥിരമായത് മായ്‌ക്കുക"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"വാചകം പകർത്തി"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"നിങ്ങളുടെ മാറ്റങ്ങൾ തള്ളിക്കളയുകയും എഡിറ്റുചെയ്യൽ ഉപേക്ഷിക്കുകയും ചെയ്യണോ?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"മാറ്റങ്ങൾ നിരസിക്കണോ?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"തള്ളിക്കളയുക"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"എഡിറ്റുചെയ്യുന്നത് തുടരുക"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"റദ്ദാക്കൂ"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"കോണ്‍‌ടാക്റ്റുകള്‍ തിരയുക"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"കോൺടാക്‌റ്റുകൾ നീക്കംചെയ്യുക"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"ആമുഖം"</string>
     <string name="send_message" msgid="8938418965550543196">"സന്ദേശം അയയ്ക്കുക"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"ഒരു വ്യക്തിഗത പകർപ്പ് സൃഷ്‌ടിക്കുന്നു…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"ഇന്നലെ"</string>
     <string name="tomorrow" msgid="6241969467795308581">"നാളെ"</string>
     <string name="today" msgid="8041090779381781781">"ഇന്ന്"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"ഇന്ന് <xliff:g id="TIME_INTERVAL">%s</xliff:g> മണിയ്‌ക്ക്"</string>
diff --git a/res/values-mn-rMN/strings.xml b/res/values-mn-rMN/strings.xml
index 6396aa5..97a5ebf 100644
--- a/res/values-mn-rMN/strings.xml
+++ b/res/values-mn-rMN/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Үндсэн болгох"</string>
     <string name="clear_default" msgid="7193185801596678067">"Үндсэнээс хасах"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Текст хуулагдав"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Таны өөрчлөлтийн устгал, засварыг болих уу?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Устгах"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Үргэлжлүүлэн засах"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Харилцагчдаас хайх"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Харилцагчдыг арилгах"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Тухай"</string>
     <string name="send_message" msgid="8938418965550543196">"Зурвас илгээх"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Хувийн хуулбар үүсгэж байна…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Өчигдөр"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Маргааш"</string>
     <string name="today" msgid="8041090779381781781">"Өнөөдөр"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Өнөөдөр <xliff:g id="TIME_INTERVAL">%s</xliff:g>-д"</string>
diff --git a/res/values-mr-rIN/strings.xml b/res/values-mr-rIN/strings.xml
index 4f03f81..0f62ed4 100644
--- a/res/values-mr-rIN/strings.xml
+++ b/res/values-mr-rIN/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"डीफॉल्ट म्हणून सेट करा"</string>
     <string name="clear_default" msgid="7193185801596678067">"डीफॉल्ट साफ करा"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"मजकूर कॉपी केला"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"आपले बदल टाकून देऊन संपादन सोडायचे?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"टाकून द्या"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"संपादन करणे सुरु ठेवा"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"संपर्क शोधा"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"संपर्क काढा"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"विषयी"</string>
     <string name="send_message" msgid="8938418965550543196">"संदेश पाठवा"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"वैयक्तिक प्रत तयार करीत आहे..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"काल"</string>
     <string name="tomorrow" msgid="6241969467795308581">"उद्या"</string>
     <string name="today" msgid="8041090779381781781">"आज"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"आज <xliff:g id="TIME_INTERVAL">%s</xliff:g> वाजता"</string>
diff --git a/res/values-ms-rMY/strings.xml b/res/values-ms-rMY/strings.xml
index 50703d5..c9a7f7d 100644
--- a/res/values-ms-rMY/strings.xml
+++ b/res/values-ms-rMY/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Tetapkan lalai"</string>
     <string name="clear_default" msgid="7193185801596678067">"Kosongkan lalai"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Teks disalin"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Buang perubahan anda dan keluar daripada pengeditan?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Buang perubahan?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Buang"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Teruskan mengedit"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Batal"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Cari dalam kenalan"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Alih keluar kenalan"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Mengenai"</string>
     <string name="send_message" msgid="8938418965550543196">"Hantar mesej"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Membuat salinan peribadi..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Semalam"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Esok"</string>
     <string name="today" msgid="8041090779381781781">"Hari ini"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Hari ini pada <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-my-rMM/strings.xml b/res/values-my-rMM/strings.xml
index 53e6cbf..b50d7f0 100644
--- a/res/values-my-rMM/strings.xml
+++ b/res/values-my-rMM/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"ပုံသေအဖြစ် သတ်မှတ်ခြင်း"</string>
     <string name="clear_default" msgid="7193185801596678067">"မူရင်းများကို ရှင်းလင်းခြင်း"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"စာသားကူးယူပြီး"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"သင့်ပြောင်းလဲမှုများကို စွန့်ပစ်ပြီး တည်းဖြတ်မှုကို ရပ်မလား။"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"စွန့်ပစ်ရန်"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"ဆက်လက်တည်းဖြတ်ပါ"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"အဆက်အသွယ်များရှာပါ"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"အဆက်အသွယ်များ ဖယ်ရှားရန်"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"အကြောင်း"</string>
     <string name="send_message" msgid="8938418965550543196">"စာတို ပို့ရန်"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"မိမိအတွက် ကိုယ်ပိုင်ကော်ပီ ပြုလုပ်နေစဉ်…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"မနေ့က"</string>
     <string name="tomorrow" msgid="6241969467795308581">"နက်ဖြန်"</string>
     <string name="today" msgid="8041090779381781781">"ယနေ့"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"ယနေ့ <xliff:g id="TIME_INTERVAL">%s</xliff:g> တွင်"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 71f2cdf..0b5d6f6 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Angi som standard"</string>
     <string name="clear_default" msgid="7193185801596678067">"Fjern som standard"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Tekst kopiert"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Vil du forkaste endringene og avslutte endringsmodusen?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Forkast"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Fortsett å endre"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Søk etter kontakter"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Fjern kontakter"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Info"</string>
     <string name="send_message" msgid="8938418965550543196">"Send melding"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Oppretter personlig kopi …"</string>
-    <string name="yesterday" msgid="6840858548955018569">"I går"</string>
     <string name="tomorrow" msgid="6241969467795308581">"I morgen"</string>
     <string name="today" msgid="8041090779381781781">"I dag"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"I dag kl. <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-ne-rNP/strings.xml b/res/values-ne-rNP/strings.xml
index d435073..2222439 100644
--- a/res/values-ne-rNP/strings.xml
+++ b/res/values-ne-rNP/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"पूर्वनिर्धारित मिलाउनुहोस्"</string>
     <string name="clear_default" msgid="7193185801596678067">"पूर्वनिर्धारित हटाउनुहोस्"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"पाठको प्रतिलिपि  गरियो"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"तपाईंका परिवर्तनहरू खारेज गरी सम्पादनलाई छाड्ने हो?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"परिवर्तनहरू खारेज गर्ने हो?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"खारेज गर्नुहोस्"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"सम्पादन गरिरहनुहोस्"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"रद्द गर्नुहोस्"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"सम्पर्कहरू खोज्नुहोस्"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"सम्पर्कहरू हटाउनुहोस्"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"बारेमा"</string>
     <string name="send_message" msgid="8938418965550543196">"सन्देश पठाउनुहोस्"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"एउटा व्यक्तिगत प्रतिलिपि बनाउँदै..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"हिजो"</string>
     <string name="tomorrow" msgid="6241969467795308581">"भोलि"</string>
     <string name="today" msgid="8041090779381781781">"आज"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"<xliff:g id="TIME_INTERVAL">%s</xliff:g>मा आज"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index ee1e7a2..1a35f88 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Standaard instellen"</string>
     <string name="clear_default" msgid="7193185801596678067">"Standaardwaarden wissen"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Tekst gekopieerd"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Je wijzigingen weggooien en ophouden met bewerken?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Weggooien"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Blijven bewerken"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Contacten zoeken"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Contacten verwijderen"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Over"</string>
     <string name="send_message" msgid="8938418965550543196">"Bericht verzenden"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Een persoonlijke kopie maken..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Gisteren"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Morgen"</string>
     <string name="today" msgid="8041090779381781781">"Vandaag"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Vandaag om <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-pa-rIN/strings.xml b/res/values-pa-rIN/strings.xml
index 766187a..ca5fd10 100644
--- a/res/values-pa-rIN/strings.xml
+++ b/res/values-pa-rIN/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"ਡਿਫੌਲਟ ਸੈਟ ਕਰੋ"</string>
     <string name="clear_default" msgid="7193185801596678067">"ਡਿਫੌਲਟ ਹਟਾਓ"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"ਟੈਕਸਟ ਕਾਪੀ ਕੀਤਾ"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"ਕੀ ਆਪਣੀਆਂ ਤਬਦੀਲੀਆਂ ਨੂੰ ਛੱਡਣਾ ਅਤੇ ਸੰਪਾਦਨ ਨੂੰ ਰੱਦ ਕਰਨਾ ਹੈ?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"ਕੀ ਤਬਦੀਲੀਆਂ ਖਾਰਜ ਕਰਨੀਆਂ ਹਨ?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"ਛੱਡੋ"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"ਸੋਧ ਕਰਦੇ ਰਹੋ"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"ਰੱਦ ਕਰੋ"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"ਸੰਪਰਕ ਖੋਜੋ"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"ਸੰਪਰਕ ਹਟਾਓ"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"ਇਸਦੇ ਬਾਰੇ"</string>
     <string name="send_message" msgid="8938418965550543196">"ਸੁਨੇਹਾ ਭੇਜੋ"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"ਇੱਕ ਨਿੱਜੀ ਕਾਪੀ ਬਣਾ ਰਿਹਾ ਹੈ..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"ਕੱਲ੍ਹ"</string>
     <string name="tomorrow" msgid="6241969467795308581">"ਕੱਲ੍ਹ ਨੂੰ"</string>
     <string name="today" msgid="8041090779381781781">"ਅੱਜ"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"ਅੱਜ <xliff:g id="TIME_INTERVAL">%s</xliff:g> ਵਜੇ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index e652f6e..92949a8 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -246,9 +246,9 @@
     <string name="set_default" msgid="4417505153468300351">"Ustaw jako wartość domyślną"</string>
     <string name="clear_default" msgid="7193185801596678067">"Wyczyść wartość domyślną"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Tekst skopiowany"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Odrzucić zmiany i zakończyć edycję?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Odrzucić zmiany?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Odrzuć"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Edytuj dalej"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Anuluj"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Szukaj kontaktów"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Usuń kontakty"</string>
@@ -273,7 +273,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Informacje"</string>
     <string name="send_message" msgid="8938418965550543196">"Wyślij wiadomość"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Tworzenie kopii osobistej…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Wczoraj"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Jutro"</string>
     <string name="today" msgid="8041090779381781781">"Dzisiaj"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Dzisiaj: <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index c57de4c..641745e 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Definir padrão"</string>
     <string name="clear_default" msgid="7193185801596678067">"Limpar padrão"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Texto copiado"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Descartar as alterações e sair da edição?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Descartar"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Continuar edição"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Pesquisar contatos"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Remover contatos"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Sobre"</string>
     <string name="send_message" msgid="8938418965550543196">"Enviar mensagem"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Criando uma cópia pessoal..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Ontem"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Amanhã"</string>
     <string name="today" msgid="8041090779381781781">"Hoje"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Hoje, às <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 4219e08..7b5b495 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Definir a predefinição"</string>
     <string name="clear_default" msgid="7193185801596678067">"Limpar predefinição"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Texto copiado"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Pretende rejeitar as alterações e sair do editor?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Rejeitar alterações?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Rejeitar"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Continuar a editar"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Cancelar"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Pesquisar contactos"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Remover contactos"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Acerca de"</string>
     <string name="send_message" msgid="8938418965550543196">"Enviar mensagem"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"A criar uma cópia pessoal"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Ontem"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Amanhã"</string>
     <string name="today" msgid="8041090779381781781">"Hoje"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Hoje, às <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index c57de4c..641745e 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Definir padrão"</string>
     <string name="clear_default" msgid="7193185801596678067">"Limpar padrão"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Texto copiado"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Descartar as alterações e sair da edição?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Descartar"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Continuar edição"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Pesquisar contatos"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Remover contatos"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Sobre"</string>
     <string name="send_message" msgid="8938418965550543196">"Enviar mensagem"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Criando uma cópia pessoal..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Ontem"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Amanhã"</string>
     <string name="today" msgid="8041090779381781781">"Hoje"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Hoje, às <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 0ed0842..7b17040 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -239,9 +239,11 @@
     <string name="set_default" msgid="4417505153468300351">"Setați ca prestabilit"</string>
     <string name="clear_default" msgid="7193185801596678067">"Ștergeți datele prestabilite"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Text copiat"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Renunțați la modificări și părăsiți editarea?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Renunțați"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Editați în continuare"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Căutați în Agendă"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Eliminați intrările din Agendă"</string>
@@ -266,7 +268,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Despre"</string>
     <string name="send_message" msgid="8938418965550543196">"Trimiteți mesajul"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Se creează o copie personală..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Ieri"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Mâine"</string>
     <string name="today" msgid="8041090779381781781">"Astăzi"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Astăzi, la <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index e348274..1d54270 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -246,9 +246,9 @@
     <string name="set_default" msgid="4417505153468300351">"Установить по умолчанию"</string>
     <string name="clear_default" msgid="7193185801596678067">"Удалить настройки по умолчанию"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Текст скопирован"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Отменить изменения и завершить редактирование?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Отменить изменения?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Отменить"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Продолжить редактирование"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Отмена"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> (<xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>)"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Поиск контактов"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Удалить контакты"</string>
@@ -273,7 +273,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"О контакте"</string>
     <string name="send_message" msgid="8938418965550543196">"Отправить сообщение"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Копирование..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Вчера"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Завтра"</string>
     <string name="today" msgid="8041090779381781781">"Сегодня"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Сегодня, <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-si-rLK/strings.xml b/res/values-si-rLK/strings.xml
index 153d970..0bdfcfa 100644
--- a/res/values-si-rLK/strings.xml
+++ b/res/values-si-rLK/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"සුපුරුදු ලෙස සකසන්න"</string>
     <string name="clear_default" msgid="7193185801596678067">"සුපුරුදු හිස් කරන්න"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"පෙළ පිටපත් කරන ලදී"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"ඔබේ වෙනස් කිරීම් ඉවත දමා සංස්කරණය කිරීමෙන් ඉවත් වන්නද?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"ඉවතලන්න"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"සංස්කරණය කරගෙන යන්න"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"සම්බන්ධතා සොයන්න"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"සම්බන්ධතා ඉවත් කරන්න"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"පිළිබඳ"</string>
     <string name="send_message" msgid="8938418965550543196">"පණිවිඩය යවන්න"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"පුද්ගලික පිටපතක් නිර්මාණය කරමින්…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"ඊයේ"</string>
     <string name="tomorrow" msgid="6241969467795308581">"හෙට"</string>
     <string name="today" msgid="8041090779381781781">"අද"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"අද <xliff:g id="TIME_INTERVAL">%s</xliff:g> ට"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index cfd7f9c..6e0ff34 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -246,9 +246,11 @@
     <string name="set_default" msgid="4417505153468300351">"Nastaviť ako predvolené"</string>
     <string name="clear_default" msgid="7193185801596678067">"Vymazať predvolené nastavenia"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Text bol skopírovaný"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Zahodiť zmeny a ukončiť upravovanie?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Zahodiť"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Pokračovať v úprave"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Hľadať v kontaktoch"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Odstránenie kontaktov"</string>
@@ -273,7 +275,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"O karte"</string>
     <string name="send_message" msgid="8938418965550543196">"Odoslať správu"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Prebieha vytváranie osobnej kópie..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Včera"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Zajtra"</string>
     <string name="today" msgid="8041090779381781781">"Dnes"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Dnes o <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index fcaf9bf..fd31f28 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -246,9 +246,9 @@
     <string name="set_default" msgid="4417505153468300351">"Nastavi za privzeto"</string>
     <string name="clear_default" msgid="7193185801596678067">"Počisti privzeto"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Besedilo kopirano"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Želite zavreči spremembe in prenehati urejati?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Želite zavreči spremembe?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Zavrzi"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Nadaljevanje urejanja"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Prekliči"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Iščite med stiki"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Odstranitev stikov"</string>
@@ -273,7 +273,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Vizitka"</string>
     <string name="send_message" msgid="8938418965550543196">"Pošlji sporočilo"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Ustvarjanje osebne kopije ..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Včeraj"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Jutri"</string>
     <string name="today" msgid="8041090779381781781">"Danes"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Danes ob <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-sq-rAL/strings.xml b/res/values-sq-rAL/strings.xml
index 3095b66..9bab93b 100644
--- a/res/values-sq-rAL/strings.xml
+++ b/res/values-sq-rAL/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Vendos të parazgjedhurën"</string>
     <string name="clear_default" msgid="7193185801596678067">"Pastro të paracaktuarin"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Teksti u kopjua"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Hiqi ndryshimet dhe mbylle redaktimin?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Hiq"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Vazhdo redaktimin"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Kërko kontaktet"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Hiq kontaktet"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Informacion rreth"</string>
     <string name="send_message" msgid="8938418965550543196">"Dërgo mesazh"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Po krijon një kopje personale..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Dje"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Nesër"</string>
     <string name="today" msgid="8041090779381781781">"Sot"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Sot në <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 49adda9..5354b72 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -239,9 +239,9 @@
     <string name="set_default" msgid="4417505153468300351">"Постави на подразумевано"</string>
     <string name="clear_default" msgid="7193185801596678067">"Обриши подразумевану вредност"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Текст је копиран"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Желите ли да одбаците промене и прекинете са изменама?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Желите да одбаците промене?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Одбаци"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Настави изменe"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Откажи"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Претражите контакте"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Уклоните контакте"</string>
@@ -266,7 +266,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Основни подаци"</string>
     <string name="send_message" msgid="8938418965550543196">"Пошаљи поруку"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Прављење личне копије..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Јуче"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Сутра"</string>
     <string name="today" msgid="8041090779381781781">"Данас"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Данас у <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 265c47c..2f9ca2a 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Ange standard"</string>
     <string name="clear_default" msgid="7193185801596678067">"Rensa standardinställningar"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Texten har kopierats"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Vill du tar bort ändringarna och sluta redigera?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Ignorera"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Fortsätt redigera"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Sök efter kontakter"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Ta bort kontakter"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Om"</string>
     <string name="send_message" msgid="8938418965550543196">"Skicka meddelande"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"En personlig kopia skapas ..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"I går"</string>
     <string name="tomorrow" msgid="6241969467795308581">"I morgon"</string>
     <string name="today" msgid="8041090779381781781">"I dag"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"I dag kl. <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 4dd493d..bbfcf70 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Weka chaguo-msingi"</string>
     <string name="clear_default" msgid="7193185801596678067">"Ondoa chaguo-msingi"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Maandishi yamenakiliwa"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Je, ungependa kutupa mabadiliko yako na uache kubadilisha?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Tupa"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Endelea kubadilisha"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Tafuta anwani"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Ondoa anwani"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Kuhusu"</string>
     <string name="send_message" msgid="8938418965550543196">"Tuma ujumbe"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Inaunda nakala ya kibinafsi..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Jana"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Kesho"</string>
     <string name="today" msgid="8041090779381781781">"Leo"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Leo saa <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-sw600dp-land/integers.xml b/res/values-sw600dp-land/integers.xml
index 1785443..b6fe15a 100644
--- a/res/values-sw600dp-land/integers.xml
+++ b/res/values-sw600dp-land/integers.xml
@@ -26,4 +26,9 @@
 
     <!-- Top margin ratio for the image for empty account view -->
     <integer name="empty_account_view_image_margin_divisor">5</integer>
+
+    <integer name="contact_tile_column_count_in_favorites">3</integer>
+
+    <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+    <integer name="snippet_length_before_tokenize">20</integer>
 </resources>
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
index 6e4f808..6f6f4e1 100644
--- a/res/values-sw600dp/dimens.xml
+++ b/res/values-sw600dp/dimens.xml
@@ -59,4 +59,22 @@
 
     <!-- Margin offset b/w the image top and app bar bottom for no account empty view -->
     <dimen name="contacts_no_account_empty_image_offset">238dp</dimen>
+
+    <dimen name="detail_item_side_margin">0dip</dimen>
+
+    <dimen name="contact_browser_list_header_left_margin">@dimen/list_visible_scrollbar_padding</dimen>
+    <dimen name="contact_browser_list_header_right_margin">24dip</dimen>
+    <dimen name="contact_browser_list_top_margin">16dip</dimen>
+
+    <!-- End margin of the account filter header icon -->
+    <dimen name="contact_browser_list_header_icon_right_margin">22dp</dimen>
+    <dimen name="contact_browser_list_header_icon_right_margin_alt">24dp</dimen>
+
+    <dimen name="contact_filter_list_item_padding_start">24dp</dimen>
+    <dimen name="contact_filter_left_margin">16dp</dimen>
+
+    <!-- Right margin of the floating action button -->
+    <dimen name="floating_action_button_margin_right">32dp</dimen>
+    <!-- Bottom margin of the floating action button -->
+    <dimen name="floating_action_button_margin_bottom">32dp</dimen>
 </resources>
diff --git a/res/values-sw600dp/integers.xml b/res/values-sw600dp/integers.xml
index 4eb0061..4b2d73f 100644
--- a/res/values-sw600dp/integers.xml
+++ b/res/values-sw600dp/integers.xml
@@ -3,4 +3,11 @@
 <resources>
     <!-- Top margin ratio for the image for empty group view -->
     <integer name="empty_group_view_image_margin_divisor">4</integer>
-</resources>
\ No newline at end of file
+
+    <integer name="contact_tile_column_count_in_favorites">3</integer>
+
+    <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+    <!-- Yikes, there is less space on a tablet!  This makes the search experience rather
+         poor. Another reason to get rid of the exist tablet layout. -->
+    <integer name="snippet_length_before_tokenize">15</integer>
+</resources>
diff --git a/res/values-sw600dp/strings.xml b/res/values-sw600dp/strings.xml
new file mode 100644
index 0000000..c9946f9
--- /dev/null
+++ b/res/values-sw600dp/strings.xml
@@ -0,0 +1,29 @@
+<?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
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- 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. [CHAR LIMIT=20] -->
+    <string name="account_phone">Device</string>
+
+    <!-- This error message shown when the user actually have no contact
+         (e.g. just after data-wiping), or, data providers of the contact list prohibits their
+         contacts from being exported to outside world via vcard exporter, etc. [CHAR LIMIT=NONE] -->
+    <string name="composer_has_no_exportable_contact">There are no exportable contacts. If you do have contacts on your tablet, some data providers may not allow the contacts to be exported from the tablet.</string>
+
+</resources>
diff --git a/res/values-sw720dp-land/integers.xml b/res/values-sw720dp-land/integers.xml
new file mode 100644
index 0000000..a83cdff
--- /dev/null
+++ b/res/values-sw720dp-land/integers.xml
@@ -0,0 +1,22 @@
+<?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.
+  -->
+<resources>
+    <integer name="contact_tile_column_count_in_favorites">4</integer>
+
+    <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+    <integer name="snippet_length_before_tokenize">30</integer>
+</resources>
diff --git a/res/values-sw720dp/integers.xml b/res/values-sw720dp/integers.xml
index 1cc510e..93bbc68 100644
--- a/res/values-sw720dp/integers.xml
+++ b/res/values-sw720dp/integers.xml
@@ -20,4 +20,9 @@
     <!-- Layout weight of the content column for tile favorites list, all contacts list, and
          QuickContact -->
     <integer name="contact_list_card_layout_weight">81</integer>
+
+    <integer name="contact_tile_column_count_in_favorites">2</integer>
+
+    <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+    <integer name="snippet_length_before_tokenize">20</integer>
 </resources>
diff --git a/res/values-ta-rIN/strings.xml b/res/values-ta-rIN/strings.xml
index 54a3f68..583b37a 100644
--- a/res/values-ta-rIN/strings.xml
+++ b/res/values-ta-rIN/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"இயல்புநிலையாக அமை"</string>
     <string name="clear_default" msgid="7193185801596678067">"இயல்பை அழி"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"உரை நகலெடுக்கப்பட்டது"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"மாற்றங்களை நிராகரித்து, திருத்துவதிலிருந்து வெளியேறவா?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"நிராகரி"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"தொடர்ந்து திருத்து"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"தொடர்புகளில் தேடுக"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"தொடர்புகளை அகற்று"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"அறிமுகம்"</string>
     <string name="send_message" msgid="8938418965550543196">"செய்தி அனுப்பு"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"தனிப்பட்ட நகலை உருவாக்குகிறது…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"நேற்று"</string>
     <string name="tomorrow" msgid="6241969467795308581">"நாளை"</string>
     <string name="today" msgid="8041090779381781781">"இன்று"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"இன்று <xliff:g id="TIME_INTERVAL">%s</xliff:g> மணிக்கு"</string>
diff --git a/res/values-te-rIN/strings.xml b/res/values-te-rIN/strings.xml
index 21fcf0c..b8741da 100644
--- a/res/values-te-rIN/strings.xml
+++ b/res/values-te-rIN/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"డిఫాల్ట్‌గా సెట్ చేయి"</string>
     <string name="clear_default" msgid="7193185801596678067">"డిఫాల్ట్‌ను క్లియర్ చేయి"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"వచనం కాపీ చేయబడింది"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"మీ మార్పులను విస్మరించి, సవరణ నుండి నిష్క్రమించాలా?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"విస్మరించు"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"సవరణను కొనసాగించు"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"పరిచయాలను శోధించండి"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"పరిచయాలను తీసివేయండి"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"పరిచయం"</string>
     <string name="send_message" msgid="8938418965550543196">"సందేశాన్ని పంపండి"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"వ్యక్తిగత కాపీని సృష్టిస్తోంది..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"నిన్న"</string>
     <string name="tomorrow" msgid="6241969467795308581">"రేపు"</string>
     <string name="today" msgid="8041090779381781781">"ఈ రోజు"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"ఈ రోజు <xliff:g id="TIME_INTERVAL">%s</xliff:g>కి"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index c55f8ce..3db43e2 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"ตั้งเป็นค่าเริ่มต้น"</string>
     <string name="clear_default" msgid="7193185801596678067">"ล้างจากค่าเริ่มต้น"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"คัดลอกข้อความแล้ว"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"ยกเลิกการเปลี่ยนแปลงและออกจากการแก้ไขใช่ไหม"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"ยกเลิกการเปลี่ยนแปลงหรือไม่"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"ยกเลิก"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"แก้ไขต่อ"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"ยกเลิก"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"ค้นหารายชื่อติดต่อ"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"นำรายชื่อติดต่อออก"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"เกี่ยวกับ"</string>
     <string name="send_message" msgid="8938418965550543196">"ส่งข้อความ"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"กำลังสร้างสำเนาส่วนบุคคล..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"เมื่อวานนี้"</string>
     <string name="tomorrow" msgid="6241969467795308581">"พรุ่งนี้"</string>
     <string name="today" msgid="8041090779381781781">"วันนี้"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"วันนี้เวลา <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 5217780..4ad7eb3 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Itakda ang default"</string>
     <string name="clear_default" msgid="7193185801596678067">"I-clear ang default"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Kinopya ang teksto"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Gusto mo bang i-discard ang iyong mga pagbabago at huminto sa pag-e-edit?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"I-discard"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Patuloy na mag-edit"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Maghanap ng mga contact"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Alisin ang mga contact"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Tungkol dito"</string>
     <string name="send_message" msgid="8938418965550543196">"Magpadala ng mensahe"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Lumilikha ng personal na kopya…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Kahapon"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Bukas"</string>
     <string name="today" msgid="8041090779381781781">"Ngayon"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Ngayong <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index eb98147..b414f70 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Varsayılan olarak ayarla"</string>
     <string name="clear_default" msgid="7193185801596678067">"Varsayılanları temizle"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Metin kopyalandı"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Değişiklikleriniz yok sayılsın ve düzenlemeden çıkılsın mı?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Yok say"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Düzenlemeye devam et"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Kişilerde arayın"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Kişileri kaldır"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Hakkında"</string>
     <string name="send_message" msgid="8938418965550543196">"İleti gönder"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Kişisel kopya oluşturuluyor..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Dün"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Yarın"</string>
     <string name="today" msgid="8041090779381781781">"Bugün"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Bugün şu saatler arasında: <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index e5a0b14..856e335 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -246,9 +246,9 @@
     <string name="set_default" msgid="4417505153468300351">"Установити за умовчанням"</string>
     <string name="clear_default" msgid="7193185801596678067">"Очистити налаштування за умовчанням"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Текст скопійовано"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Відхилити зміни та закінчити редагування?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Відхилити зміни?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Відхилити"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Продовжити редагування"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Скасувати"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Пошук контактів"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Вилучити контакти"</string>
@@ -273,7 +273,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Інформація"</string>
     <string name="send_message" msgid="8938418965550543196">"Надіслати повідомлення"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Створення особистої копії..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Учора"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Завтра"</string>
     <string name="today" msgid="8041090779381781781">"Сьогодні"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Сьогодні о <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-ur-rPK/strings.xml b/res/values-ur-rPK/strings.xml
index adf405f..f4411a3 100644
--- a/res/values-ur-rPK/strings.xml
+++ b/res/values-ur-rPK/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"ڈیفالٹ طے کریں"</string>
     <string name="clear_default" msgid="7193185801596678067">"ڈیفالٹ کو صاف کریں"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"متن کاپی ہوگیا"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"اپنی تبدیلیاں مسترد کریں اور ترمیم کرنا چھوڑ دیں؟"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"رد کریں"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"ترمیم کرنا جاری رکھیں"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"رابطے تلاش کریں"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"رابطے ہٹائیں"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"تفصیل"</string>
     <string name="send_message" msgid="8938418965550543196">"پیغام بھیجیں"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"ایک ذاتی کاپی بنا رہا ہے…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"گزشتہ کل"</string>
     <string name="tomorrow" msgid="6241969467795308581">"آئندہ کل"</string>
     <string name="today" msgid="8041090779381781781">"ﺁﺝ"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"آج بوقت <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-uz-rUZ/strings.xml b/res/values-uz-rUZ/strings.xml
index d140126..c23ad88 100644
--- a/res/values-uz-rUZ/strings.xml
+++ b/res/values-uz-rUZ/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Standart sifatida o‘rnatish"</string>
     <string name="clear_default" msgid="7193185801596678067">"Standartni tozalash"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Matndan nuxsa olindi"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"O‘zgarishlar bekor qilinib, chiqib ketilsinmi?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Bekor qilish"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Tahrirlashda davom etish"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Kontaktlarni qidirish"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Kontaktlarni olib tashlash"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Ma’lumot"</string>
     <string name="send_message" msgid="8938418965550543196">"Xabar yuborish"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Shaxsiy nusxasi yaratilmoqda…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Kecha"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Ertaga"</string>
     <string name="today" msgid="8041090779381781781">"Bugun"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Bugun <xliff:g id="TIME_INTERVAL">%s</xliff:g> da"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 5ffec07..0ebe839 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"Đặt mặc định"</string>
     <string name="clear_default" msgid="7193185801596678067">"Xóa mặc định"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Đã sao chép văn bản"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Hủy các thay đổi của bạn và thoát chỉnh sửa?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Hủy"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Tiếp tục chỉnh sửa"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Tìm kiếm trong danh bạ"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Xóa liên hệ"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Giới thiệu"</string>
     <string name="send_message" msgid="8938418965550543196">"Gửi tin nhắn"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Đang tạo bản sao cá nhân..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"Hôm qua"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Ngày mai"</string>
     <string name="today" msgid="8041090779381781781">"Hôm nay"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Hôm nay lúc <xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-zh-rCN/donottranslate_config.xml b/res/values-zh-rCN/donottranslate_config.xml
new file mode 100644
index 0000000..b357856
--- /dev/null
+++ b/res/values-zh-rCN/donottranslate_config.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, 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>
+    <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+    <bool name="config_sort_order_user_changeable">false</bool>
+
+    <!-- If true, the default sort order is primary (i.e. by given name) -->
+    <bool name="config_default_sort_order_primary">true</bool>
+
+    <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+    <bool name="config_display_order_user_changeable">false</bool>
+
+    <!-- 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">false</bool>
+</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index ffafef7..9b7e396 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"设置默认值"</string>
     <string name="clear_default" msgid="7193185801596678067">"清除默认值"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"文本已复制"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"要舍弃您所做的更改并停止修改吗?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"舍弃"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"继续修改"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"搜索联系人"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"移除联系人"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"简介"</string>
     <string name="send_message" msgid="8938418965550543196">"发送短信"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"正在创建个人副本..."</string>
-    <string name="yesterday" msgid="6840858548955018569">"昨天"</string>
     <string name="tomorrow" msgid="6241969467795308581">"明天"</string>
     <string name="today" msgid="8041090779381781781">"今天"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"今天<xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 03c4931..b95ad5d 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"設為預設"</string>
     <string name="clear_default" msgid="7193185801596678067">"清除預設值"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"文字已複製"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"要捨棄變更並停止編輯嗎?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"捨棄"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"繼續編輯"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"搜尋聯絡人"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"移除聯絡人"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"關於"</string>
     <string name="send_message" msgid="8938418965550543196">"傳送訊息"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"正在建立個人副本…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"昨天"</string>
     <string name="tomorrow" msgid="6241969467795308581">"明天"</string>
     <string name="today" msgid="8041090779381781781">"今天"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"今天<xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-zh-rTW/donottranslate_config.xml b/res/values-zh-rTW/donottranslate_config.xml
new file mode 100644
index 0000000..b357856
--- /dev/null
+++ b/res/values-zh-rTW/donottranslate_config.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, 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>
+    <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+    <bool name="config_sort_order_user_changeable">false</bool>
+
+    <!-- If true, the default sort order is primary (i.e. by given name) -->
+    <bool name="config_default_sort_order_primary">true</bool>
+
+    <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+    <bool name="config_display_order_user_changeable">false</bool>
+
+    <!-- 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">false</bool>
+</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 58f3aac..fa17add 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -232,9 +232,11 @@
     <string name="set_default" msgid="4417505153468300351">"設為預設值"</string>
     <string name="clear_default" msgid="7193185801596678067">"清除預設值"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"文字已複製"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"確定要捨棄變更並結束編輯嗎?"</string>
+    <!-- no translation found for cancel_confirmation_dialog_message (9008214737653278989) -->
+    <skip />
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"捨棄"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"繼續編輯"</string>
+    <!-- no translation found for cancel_confirmation_dialog_keep_editing_button (3316573928085916146) -->
+    <skip />
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"搜尋聯絡人"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"移除聯絡人"</string>
@@ -259,7 +261,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"關於"</string>
     <string name="send_message" msgid="8938418965550543196">"傳送簡訊"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"正在建立個人副本…"</string>
-    <string name="yesterday" msgid="6840858548955018569">"昨天"</string>
     <string name="tomorrow" msgid="6241969467795308581">"明天"</string>
     <string name="today" msgid="8041090779381781781">"今天"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"今天<xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 81745fd..471b77a 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -232,9 +232,9 @@
     <string name="set_default" msgid="4417505153468300351">"Hlela okuzenzakalelayo"</string>
     <string name="clear_default" msgid="7193185801596678067">"Sula okuzenzakalelayo"</string>
     <string name="toast_text_copied" msgid="5143776250008541719">"Umbhalo okopishiwe"</string>
-    <string name="cancel_confirmation_dialog_message" msgid="5058226498605989285">"Lahla izinguquko zakho bese uyeke ukuhlela?"</string>
+    <string name="cancel_confirmation_dialog_message" msgid="9008214737653278989">"Lahla ushintsho?"</string>
     <string name="cancel_confirmation_dialog_cancel_editing_button" msgid="3057023972074640671">"Lahla"</string>
-    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="7737724111972855348">"Qhubeka uhlela"</string>
+    <string name="cancel_confirmation_dialog_keep_editing_button" msgid="3316573928085916146">"Khansela"</string>
     <string name="call_type_and_date" msgid="747163730039311423">"<xliff:g id="CALL_TYPE">%1$s</xliff:g> <xliff:g id="CALL_SHORT_DATE">%2$s</xliff:g>"</string>
     <string name="enter_contact_name" msgid="4594274696120278368">"Sesha oxhumana nabo"</string>
     <string name="title_edit_group" msgid="8602752287270586734">"Susa oxhumana nabo"</string>
@@ -259,7 +259,6 @@
     <string name="about_card_title" msgid="2920942314212825637">"Mayelana"</string>
     <string name="send_message" msgid="8938418965550543196">"Thumela umlayezo"</string>
     <string name="toast_making_personal_copy" msgid="288549957278065542">"Idala ikhophi yomuntu siqu"</string>
-    <string name="yesterday" msgid="6840858548955018569">"Izolo"</string>
     <string name="tomorrow" msgid="6241969467795308581">"Kusasa"</string>
     <string name="today" msgid="8041090779381781781">"Namhlanje"</string>
     <string name="today_at_time_fmt" msgid="605665249491030460">"Namhlanje ngo-<xliff:g id="TIME_INTERVAL">%s</xliff:g>"</string>
diff --git a/res/values/animation_constants.xml b/res/values/animation_constants.xml
new file mode 100644
index 0000000..39f6ba6
--- /dev/null
+++ b/res/values/animation_constants.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 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>
+    <integer name="floating_action_button_animation_duration">175</integer>
+</resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 86c46fc..e979611 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -49,4 +49,71 @@
         <attr name="layout_widePaddingRight" format="dimension"/>
     </declare-styleable>
 
+    <declare-styleable name="Theme">
+        <attr name="android:textColorSecondary" />
+    </declare-styleable>
+
+    <declare-styleable name="ContactsDataKind">
+        <!-- Mime-type handled by this mapping. -->
+        <attr name="android:mimeType" />
+        <!-- Icon used to represent data of this kind. -->
+        <attr name="android:icon" />
+        <!-- Column in data table that summarizes this data. -->
+        <attr name="android:summaryColumn" />
+        <!-- Column in data table that contains details for this data. -->
+        <attr name="android:detailColumn" />
+        <!-- Flag indicating that detail should be built from SocialProvider. -->
+        <attr name="android:detailSocialSummary" />
+        <!-- Resource representing the term "All Contacts" (e.g. "All Friends" or
+        "All connections"). Optional (Default is "All Contacts"). -->
+        <attr name="android:allContactsName" />
+    </declare-styleable>
+
+    <declare-styleable name="ContactListItemView">
+        <attr name="list_item_height" format="dimension"/>
+        <attr name="list_section_header_height" format="dimension"/>
+        <attr name="activated_background" format="reference"/>
+        <attr name="section_header_background" format="reference"/>
+        <attr name="list_item_padding_top" format="dimension"/>
+        <attr name="list_item_padding_right" format="dimension"/>
+        <attr name="list_item_padding_bottom" format="dimension"/>
+        <attr name="list_item_padding_left" format="dimension"/>
+        <attr name="list_item_gap_between_image_and_text" format="dimension"/>
+        <attr name="list_item_gap_between_indexer_and_image" format="dimension"/>
+        <attr name="list_item_gap_between_label_and_data" format="dimension"/>
+        <attr name="list_item_presence_icon_margin" format="dimension"/>
+        <attr name="list_item_presence_icon_size" format="dimension"/>
+        <attr name="list_item_photo_size" format="dimension"/>
+        <attr name="list_item_profile_photo_size" format="dimension"/>
+        <attr name="list_item_prefix_highlight_color" format="color"/>
+        <attr name="list_item_background_color" format="color"/>
+        <attr name="list_item_header_text_indent" format="dimension"/>
+        <attr name="list_item_header_text_color" format="color"/>
+        <attr name="list_item_header_text_size" format="dimension"/>
+        <attr name="list_item_header_height" format="dimension"/>
+        <attr name="list_item_name_text_color" format="color"/>
+        <attr name="list_item_name_text_size" format="dimension"/>
+        <attr name="list_item_text_indent" format="dimension"/>
+        <attr name="list_item_text_offset_top" format="dimension"/>
+        <attr name="list_item_avatar_offset_top" format="dimension"/>
+        <attr name="list_item_data_width_weight" format="integer"/>
+        <attr name="list_item_label_width_weight" format="integer"/>
+        <attr name="list_item_video_call_icon_size" format="dimension"/>
+        <attr name="list_item_video_call_icon_margin" format="dimension"/>
+    </declare-styleable>
+
+    <declare-styleable name="ContactBrowser">
+        <attr name="contact_browser_list_padding_left" format="dimension"/>
+        <attr name="contact_browser_list_padding_right" format="dimension"/>
+        <attr name="contact_browser_background" format="reference"/>
+    </declare-styleable>
+
+    <declare-styleable name="ProportionalLayout">
+        <attr name="direction" format="string"/>
+        <attr name="ratio" format="float"/>
+    </declare-styleable>
+
+    <declare-styleable name="Favorites">
+        <attr name="favorites_padding_bottom" format="dimension"/>
+    </declare-styleable>
 </resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index f5bf4cc..37ea0ba 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -83,4 +83,178 @@
 
     <!-- Color of hamburger icon in promo -->
     <color name="hamburger_feature_highlight_inner_color">#00ffffff</color>
+
+    <!-- Background color corresponding to the holo list 9-patch. -->
+    <color name="holo_list_background_color">#eeeeee</color>
+
+    <color name="focus_color">#44ff0000</color>
+
+    <!-- Color of ripples used for views with dark backgrounds -->
+    <color name="ripple_material_dark">#a0ffffff</color>
+
+    <!-- Divider color for header separator -->
+    <color name="primary_text_color">#363636</color>
+
+    <color name="secondary_text_color">@color/dialtacts_secondary_text_color</color>
+
+    <!-- Text color for section header. -->
+    <color name="section_header_text_color">@color/dialtacts_theme_color</color>
+
+    <!-- Divider color for header separator -->
+    <color name="main_header_separator_color">#AAAAAA</color>
+
+    <!-- Divider color for header separator -->
+    <color name="secondary_header_separator_color">#D0D0D0</color>
+
+    <!-- Color of the theme of the People app -->
+    <color name="people_app_theme_color">#363636</color>
+
+    <!-- Color of the theme of the Dialer app -->
+    <color name="dialtacts_theme_color">#0288d1</color>
+
+    <!-- Color of image view placeholder. -->
+    <color name="image_placeholder">#DDDDDD</color>
+
+    <!-- Primary text color in the Phone app -->
+    <color name="dialtacts_primary_text_color">#333333</color>
+
+    <!-- Secondary text color in the Phone app -->
+    <color name="dialtacts_secondary_text_color">#737373</color>
+
+    <!--  Color of the semi-transparent shadow box on contact tiles -->
+    <color name="contact_tile_shadow_box_color">#7F000000</color>
+
+    <!--  Color of the status message for starred contacts in the People app -->
+    <color name="people_contact_tile_status_color">#CCCCCC</color>
+
+    <color name="shortcut_overlay_text_background">#7f000000</color>
+
+    <color name="textColorIconOverlay">#fff</color>
+    <color name="textColorIconOverlayShadow">#000</color>
+
+    <!-- Background colors for LetterTileDrawables. This set of colors is a subset of
+        https://spec.googleplex.com/quantumpalette#extended which passes Google Accessibility
+        Requirements for the color in question on white with >= 3.0 contrast. We used
+        http://leaverou.github.io/contrast-ratio/#white-on-%23db4437 to double-check the contrast.
+
+        These colors are also used by MaterialColorMapUtils to generate primary activity colors.
+    -->
+    <array name="letter_tile_colors">
+        <item>#DB4437</item>
+        <item>#E91E63</item>
+        <item>#9C27B0</item>
+        <item>#673AB7</item>
+        <item>#3F51B5</item>
+        <item>#4285F4</item>
+        <item>#039BE5</item>
+        <item>#0097A7</item>
+        <item>#009688</item>
+        <item>#0F9D58</item>
+        <item>#689F38</item>
+        <item>#EF6C00</item>
+        <item>#FF5722</item>
+        <item>#757575</item>
+    </array>
+
+    <!-- Darker versions of letter_tile_colors, two shades darker. These colors are used
+        for settings secondary activity colors. -->
+    <array name="letter_tile_colors_dark">
+        <item>#C53929</item>
+        <item>#C2185B</item>
+        <item>#7B1FA2</item>
+        <item>#512DA8</item>
+        <item>#303F9F</item>
+        <item>#3367D6</item>
+        <item>#0277BD</item>
+        <item>#006064</item>
+        <item>#00796B</item>
+        <item>#0B8043</item>
+        <item>#33691E</item>
+        <item>#E65100</item>
+        <item>#E64A19</item>
+        <item>#424242</item>
+    </array>
+
+    <!-- The default color used for tinting photos when no color can be extracted via Palette,
+            this is Blue Grey 500 -->
+    <color name="quickcontact_default_photo_tint_color">#607D8B</color>
+    <!-- The default secondary color when no color can be extracted via Palette,
+            this is Blue Grey 700 -->
+    <color name="quickcontact_default_photo_tint_color_dark">#455A64</color>
+
+
+    <color name="letter_tile_default_color">#cccccc</color>
+
+    <color name="letter_tile_font_color">#ffffff</color>
+
+    <!-- Background color of action bars. Ensure this stays in sync with packages/Telephony
+         actionbar_background_color. -->
+    <color name="actionbar_background_color">#0fc6dc</color>
+    <!-- Color for icons in the actionbar -->
+    <color name="actionbar_icon_color">#ffffff</color>
+    <!-- Darker version of the actionbar color. Used for the status bar and navigation bar colors. -->
+    <color name="actionbar_background_color_dark">#008aa1</color>
+
+    <color name="tab_ripple_color">@color/tab_accent_color</color>
+    <color name="tab_accent_color">#ffffff</color>
+    <color name="tab_selected_underline_color">@color/tab_accent_color</color>
+    <color name="tab_unread_count_background_color">#700f4b70</color>
+
+    <!-- Color of the title to the Frequently Contacted section -->
+    <color name="frequently_contacted_title_color">@color/actionbar_background_color</color>
+
+    <!-- Color of action bar text. Ensure this stays in sync with packages/Telephony
+    phone_settings_actionbar_text_color-->
+    <color name="actionbar_text_color">#ffffff</color>
+    <!-- 54% black for icons -->
+    <color name="actionbar_icon_color_grey">#8C000000</color>
+    <!-- 87% black for actionbar text -->
+    <color name="actionbar_text_color_black">#DF000000</color>
+    <!-- Solid grey for status bar overlay-->
+    <color name="actionbar_color_grey_solid">#777777</color>
+    <color name="actionbar_unselected_text_color">#a6ffffff</color>
+
+    <!-- Text color of the search box text as entered by user  -->
+    <color name="searchbox_text_color">#000000</color>
+    <!-- Background color of the search box -->
+    <color name="searchbox_background_color">#ffffff</color>
+
+    <color name="searchbox_hint_text_color">#66000000</color>
+    <color name="searchbox_icon_tint">@color/searchbox_hint_text_color</color>
+
+    <color name="search_shortcut_icon_color">@color/dialtacts_theme_color</color>
+
+    <!-- Color of the background of the contact detail and editor pages -->
+    <color name="background_primary">#f9f9f9</color>
+    <color name="contact_all_list_background_color">#FFFFFF</color>
+
+    <!-- Text color used for character counter when the max limit has been exceeded -->
+    <color name="call_subject_limit_exceeded">#d1041c</color>
+
+    <!-- Tint color for the call subject history icon. -->
+    <color name="call_subject_history_icon">#000000</color>
+
+    <!-- Divider line on the call subject dialog. -->
+    <color name="call_subject_divider">#d8d8d8</color>
+
+    <!-- Text color for the SEND & CALL button on the call subject dialog. -->
+    <color name="call_subject_button">#00c853</color>
+
+    <!-- Background color for the call subject history view. -->
+    <color name="call_subject_history_background">#ffffff</color>
+    <color name="search_video_call_icon_tint">@color/searchbox_hint_text_color</color>
+
+    <!-- Text color for an action in a snackbar. -->
+    <color name="snackbar_action_text">#40c4ff</color>
+    <!-- Background color for a snackbar. -->
+    <color name="snackbar_background">#333333</color>
+
+    <!-- Color of account/custom filters -->
+    <color name="account_filter_text_color">@color/actionbar_text_color_black</color>
+    <color name="custom_filter_divider">#dbdbdb</color>
+
+    <color name="material_star_pink">#f50057</color>
+
+    <!-- Primary text color in Contacts app -->
+    <color name="contacts_text_color">#333333</color>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index a73e449..d355021 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -321,4 +321,171 @@
 
     <!-- Margin b/w add account button and import contacts button for no account empty view -->
     <dimen name="contacts_no_account_buttons_margin">8dp</dimen>
+
+    <!-- Padding between the action bar's bottom edge and the first header
+         in contacts/group lists. -->
+    <dimen name="list_header_extra_top_padding">0dip</dimen>
+
+    <dimen name="list_section_divider_min_height">32dip</dimen>
+
+    <dimen name="directory_header_extra_top_padding">18dp</dimen>
+    <dimen name="directory_header_extra_bottom_padding">8dp</dimen>
+    <dimen name="directory_header_left_padding">16dp</dimen>
+
+    <!--  Horizontal padding in between contact tiles -->
+    <dimen name="contact_tile_divider_padding">23dip</dimen>
+    <!--  Horizontal whitespace (both padding and margin) before the first tile and after the last tile -->
+    <dimen name="contact_tile_start_end_whitespace">16dip</dimen>
+
+    <!-- Left and right padding for a contact detail item -->
+    <dimen name="detail_item_side_margin">16dip</dimen>
+
+    <!-- ContactTile Layouts -->
+    <!--
+      Use sp instead of dip so that the shadowbox heights can all scale uniformly
+      when the font size is scaled for accessibility purposes
+    -->
+    <dimen name="contact_tile_shadowbox_height">48sp</dimen>
+
+    <!-- For contact filter setting screens -->
+    <dimen name="contact_filter_left_margin">8dp</dimen>
+    <dimen name="contact_filter_right_margin">16dip</dimen>
+    <dimen name="contact_filter_item_min_height">48dip</dimen>
+    <dimen name="contact_filter_icon_size">32dip</dimen>
+    <dimen name="contact_filter_list_item_height">56dp</dimen>
+    <dimen name="contact_filter_list_item_padding_start">16dp</dimen>
+    <!-- contact_filter_indicator is the arrow in expandable list view -->
+    <dimen name="contact_filter_indicator_padding_start">6dp</dimen>
+    <dimen name="contact_filter_indicator_padding_end">46dp</dimen>
+    <dimen name="contact_filter_action_button_width">72dp</dimen>
+
+    <!-- Padding to be used between a visible scrollbar and the contact list -->
+    <dimen name="list_visible_scrollbar_padding">32dip</dimen>
+
+    <dimen name="contact_browser_list_header_icon_left_margin">16dp</dimen>
+    <dimen name="contact_browser_list_header_icon_right_margin">14dp</dimen>
+    <dimen name="contact_browser_list_header_icon_left_margin_alt">18dp</dimen>
+    <dimen name="contact_browser_list_header_icon_right_margin_alt">16dp</dimen>
+    <dimen name="contact_browser_list_header_left_margin">16dip</dimen>
+    <dimen name="contact_browser_list_header_right_margin">@dimen/list_visible_scrollbar_padding</dimen>
+    <dimen name="contact_browser_list_item_text_indent">8dip</dimen>
+    <dimen name="contact_browser_list_header_height">48dp</dimen>
+    <dimen name="contact_browser_list_header_icon_size">24dp</dimen>
+    <dimen name="contact_browser_list_header_icon_size_alt">20dp</dimen>
+    <dimen name="contact_browser_list_header_text_margin">10dp</dimen>
+    <!-- Width of a contact list item section header. -->
+    <dimen name="contact_list_section_header_width">56dp</dimen>
+
+    <!-- Size of the shortcut icon. 0dip means: use the system default -->
+    <dimen name="shortcut_icon_size">0dip</dimen>
+
+    <!-- Text size of shortcut icon overlay text -->
+    <dimen name="shortcut_overlay_text_size">12dp</dimen>
+
+    <!-- Extra vertical padding for darkened background behind shortcut icon overlay text -->
+    <dimen name="shortcut_overlay_text_background_padding">1dp</dimen>
+
+    <!-- Width of height of an icon from a third-party app in the networks section of the contact card. -->
+    <dimen name="detail_network_icon_size">40dp</dimen>
+
+    <!-- Empty message margins -->
+    <dimen name="empty_message_top_margin">48dip</dimen>
+
+    <!-- contact browser list margins -->
+    <dimen name="contact_browser_list_item_text_size">16sp</dimen>
+    <dimen name="contact_browser_list_item_photo_size">40dp</dimen>
+    <dimen name="contact_browser_list_item_gap_between_image_and_text">15dp</dimen>
+    <dimen name="contact_browser_list_item_gap_between_indexer_and_image">16dp</dimen>
+    <dimen name="contact_browser_list_top_margin">12dp</dimen>
+
+    <!-- Dimensions for "No contacts" string in PhoneFavoriteFragment for the All contacts
+         with phone numbers section
+    -->
+    <dimen name="contact_phone_list_empty_description_size">20sp</dimen>
+    <dimen name="contact_phone_list_empty_description_padding">10dip</dimen>
+
+    <!-- Dimensions for contact letter tiles -->
+    <dimen name="tile_letter_font_size">40dp</dimen>
+    <dimen name="tile_letter_font_size_small">20dp</dimen>
+    <dimen name="tile_divider_width">1dp</dimen>
+    <item name="letter_to_tile_ratio" type="dimen">67%</item>
+
+    <!-- Height of the floating action button -->
+    <dimen name="floating_action_button_height">56dp</dimen>
+    <!-- Width of the floating action button -->
+    <dimen name="floating_action_button_width">56dp</dimen>
+    <!-- Corner radius of the floating action button -->
+    <dimen name="floating_action_button_radius">28dp</dimen>
+    <!-- Z translation of the floating action button -->
+    <dimen name="floating_action_button_translation_z">8dp</dimen>
+    <!-- Padding to be applied to the bottom of lists to make space for the floating action
+         button -->
+    <dimen name="floating_action_button_list_bottom_padding">88dp</dimen>
+    <!-- Right margin of the floating action button -->
+    <dimen name="floating_action_button_margin_right">16dp</dimen>
+    <!-- Bottom margin of the floating action button -->
+    <dimen name="floating_action_button_margin_bottom">16dp</dimen>
+    <!-- Offset of bottom margin of the floating action button used when dialpad is up -->
+    <dimen name="floating_action_button_dialpad_margin_bottom_offset">4dp</dimen>
+
+    <!-- Height of the selection indicator of a tab. -->
+    <dimen name="tab_selected_underline_height">2dp</dimen>
+    <!-- Size of text in tabs. -->
+    <dimen name="tab_text_size">14sp</dimen>
+    <dimen name="tab_elevation">2dp</dimen>
+    <dimen name="tab_unread_count_background_size">16dp</dimen>
+    <dimen name="tab_unread_count_background_radius">2dp</dimen>
+    <dimen name="tab_unread_count_margin_left">10dp</dimen>
+    <dimen name="tab_unread_count_margin_top">2dp</dimen>
+    <dimen name="tab_unread_count_text_size">12sp</dimen>
+    <dimen name="tab_unread_count_text_padding">2dp</dimen>
+
+    <!-- Padding around the icon in the search box. -->
+    <dimen name="search_box_icon_margin">4dp</dimen>
+    <!-- Size of the icon (voice search, back arrow) in the search box. -->
+    <dimen name="search_box_icon_size">56dp</dimen>
+    <!-- Size of the close icon.-->
+    <dimen name="search_box_close_icon_size">56dp</dimen>
+    <!-- Padding around the close button. It's visible size without padding is 24dp. -->
+    <dimen name="search_box_close_icon_padding">16dp</dimen>
+    <!-- End margin of the back arrow icon in the search box -->
+    <dimen name="search_box_navigation_icon_margin">8dp</dimen>
+    <!-- Left margin of the text field in the search box. -->
+    <dimen name="search_box_text_left_margin">8dp</dimen>
+    <!-- Search box text size -->
+    <dimen name="search_text_size">16sp</dimen>
+
+    <item name="close_icon_alpha" format="float" type="dimen">0.54</item>
+
+    <!-- Size of the close icon in selection bar.-->
+    <dimen name="selection_bar_close_icon_size">56dp</dimen>
+
+    <!-- Top margin for the Frequently Contacted section title -->
+    <dimen name="frequently_contacted_title_top_margin_when_first_row">16dp</dimen>
+    <!-- Top margin for the Frequently Contacted section title, when the title is the first
+         item in the list -->
+    <dimen name="frequently_contacted_title_top_margin">57dp</dimen>
+
+    <dimen name="frequently_contacted_title_text_size">24sp</dimen>
+
+    <!-- Size of icon for contacts number shortcuts -->
+    <dimen name="search_shortcut_radius">40dp</dimen>
+
+    <dimen name="contact_list_card_elevation">2dp</dimen>
+
+    <!-- Padding used around the periphery of the call subject dialog, as well as in between the
+         items. -->
+    <dimen name="call_subject_dialog_margin">20dp</dimen>
+    <!-- Padding used between lines of text in the call subject dialog. -->
+    <dimen name="call_subject_dialog_between_line_margin">8dp</dimen>
+    <!-- Size of the contact photo in the call subject dialog. -->
+    <dimen name="call_subject_dialog_contact_photo_size">50dp</dimen>
+    <!-- Margin above the edit text in the call subject dialog. -->
+    <dimen name="call_subject_dialog_edit_spacing">60dp</dimen>
+    <!-- Size of primary text in the call subject dialog. -->
+    <dimen name="call_subject_dialog_primary_text_size">16sp</dimen>
+    <!-- Size of secondary text in the call subject dialog. -->
+    <dimen name="call_subject_dialog_secondary_text_size">14sp</dimen>
+    <!-- Row padding for call subject history items. -->
+    <dimen name="call_subject_history_item_padding">15dp</dimen>
 </resources>
diff --git a/res/values/donottranslate_config.xml b/res/values/donottranslate_config.xml
index bfe7880..8668159 100644
--- a/res/values/donottranslate_config.xml
+++ b/res/values/donottranslate_config.xml
@@ -39,4 +39,81 @@
 
     <!-- File Authority for AOSP Contacts files -->
     <string name="contacts_file_provider_authority">com.android.contacts.files</string>
+    <!-- Flag indicating whether Contacts app is allowed to import contacts -->
+    <bool name="config_allow_import_from_vcf_file">true</bool>
+
+    <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+    <bool name="config_sort_order_user_changeable">true</bool>
+
+    <!-- If true, the default sort order is primary (i.e. by given name) -->
+    <bool name="config_default_sort_order_primary">true</bool>
+
+    <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+    <bool name="config_display_order_user_changeable">true</bool>
+
+    <!-- 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>
+
+    <!-- If true, an option is shown in Display Options UI to choose a default account -->
+    <bool name="config_default_account_user_changeable">true</bool>
+
+    <!-- Contacts preferences key for contact editor default account -->
+    <string name="contact_editor_default_account_key">ContactEditorUtils_default_account</string>
+
+    <!-- Contacts preferences key for contact editor anything saved -->
+    <string name="contact_editor_anything_saved_key">ContactEditorUtils_anything_saved</string>
+
+    <!-- The type of VCard for export. If you want to let the app emit vCard which is
+    specific to some vendor (like DoCoMo), specify this type (e.g. "docomo") -->
+    <string name="config_export_vcard_type" translatable="false">default</string>
+
+    <!-- The type of vcard for improt. If the vcard importer cannot guess the exact type
+    of a vCard type, the improter uses this type. -->
+    <string name="config_import_vcard_type" translatable="false">default</string>
+
+    <!-- Prefix of exported VCard file -->
+    <string name="config_export_file_prefix" translatable="false"></string>
+
+    <!-- Suffix of exported VCard file. Attached before an extension -->
+    <string name="config_export_file_suffix" translatable="false"></string>
+
+    <!-- Extension for exported VCard files -->
+    <string name="config_export_file_extension">vcf</string>
+
+    <!-- The filename that is suggested that users use when exporting vCards. Should include the .vcf extension. -->
+    <string name="exporting_vcard_filename" translatable="false">contacts.vcf</string>
+
+    <!-- Minimum number of exported VCard file index -->
+    <integer name="config_export_file_min_index">1</integer>
+
+    <!-- Maximum number of exported VCard file index -->
+    <integer name="config_export_file_max_index">99999</integer>
+
+    <!-- The list (separated by ',') of extensions should be checked in addition to
+     config_export_extension. e.g. If "aaa" is added to here and 00001.vcf and 00002.aaa
+     exist in a target directory, 00003.vcf becomes a next file name candidate.
+     Without this configuration, 00002.vcf becomes the candidate.-->
+    <string name="config_export_extensions_to_consider" translatable="false"></string>
+
+    <!-- If true, enable the "import contacts from SIM" feature if the device
+         has an appropriate SIM or ICC card.
+         Setting this flag to false in a resource overlay allows you to
+         entirely disable SIM import on a per-product basis. -->
+    <bool name="config_allow_sim_import">true</bool>
+
+    <!-- Flag indicating whether Contacts app is allowed to export contacts -->
+    <bool name="config_allow_export">true</bool>
+
+    <!-- Flag indicating whether Contacts app is allowed to share contacts with devices outside -->
+    <bool name="config_allow_share_contacts">true</bool>
+
+    <string name="pref_build_version_key">pref_build_version</string>
+    <string name="pref_open_source_licenses_key">pref_open_source_licenses</string>
+    <string name="pref_privacy_policy_key">pref_privacy_policy</string>
+    <string name="pref_terms_of_service_key">pref_terms_of_service</string>
+
+    <string name="star_sign">\u2605</string>
 </resources>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index f5649d9..0808496 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -52,4 +52,29 @@
 
     <!-- Menu group ID for the contact filters -->
     <item type="id" name="nav_filters_items" />
+
+    <!-- For vcard.ImportVCardActivity -->
+    <item type="id" name="dialog_cache_vcard"/>
+    <item type="id" name="dialog_error_with_message"/>
+
+    <!-- For vcard.CancelActivity -->
+    <item type="id" name="dialog_cancel_confirmation"/>
+    <item type="id" name="dialog_cancel_failed"/>
+
+    <!-- For ExportVCardActivity -->
+    <item type="id" name="dialog_exporting_vcard"/>
+    <item type="id" name="dialog_fail_to_export_with_reason"/>
+
+    <!-- For Debug Purpose -->
+    <item type="id" name="cliv_name_textview"/>
+    <item type="id" name="cliv_phoneticname_textview"/>
+    <item type="id" name="cliv_label_textview"/>
+    <item type="id" name="cliv_data_view"/>
+
+    <!-- For tag ids used by ContactPhotoManager to tag views with contact details -->
+    <item type="id" name="tag_display_name"/>
+    <item type="id" name="tag_identifier"/>
+    <item type="id" name="tag_contact_type"/>
+
+    <item type="id" name="menu_save"/>
 </resources>
diff --git a/res/values/integers.xml b/res/values/integers.xml
index 60034e5..9ef7810 100644
--- a/res/values/integers.xml
+++ b/res/values/integers.xml
@@ -36,4 +36,27 @@
 
     <!-- Top margin ratio for the image for empty contacts view-->
     <integer name="contacts_no_account_empty_image_margin_divisor">2</integer>
+
+    <!--  Determines the number of columns in a ContactTileRow in the favorites tab -->
+    <integer name="contact_tile_column_count_in_favorites">2</integer>
+    <integer name="contact_tile_column_count_in_favorites_new">3</integer>
+
+    <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+    <integer name="snippet_length_before_tokenize">30</integer>
+
+    <!-- Layout weight of space elements in contact list view.
+    Default to 0 to indicate no padding-->
+    <integer name="contact_list_space_layout_weight">0</integer>
+    <!-- Layout weight of card in contact list view.
+    Default to 0 to indicate no padding -->
+    <integer name="contact_list_card_layout_weight">0</integer>
+
+    <!-- Duration of the animations on the call subject dialog. -->
+    <integer name="call_subject_animation_duration">250</integer>
+
+    <!-- A big number to make sure "About contacts" always showing at the bottom of Settings.-->
+    <integer name="about_contacts_order_number">100</integer>
+
+    <!-- Duration of the animations when a contact list loads. -->
+    <integer name="lists_on_load_animation_duration">190</integer>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d6f8239..4fe3edc 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -751,8 +751,7 @@
 
     <!-- Toast that appears when you are copying a directory contact into your personal contacts -->
     <string name="toast_making_personal_copy">Creating a personal copy&#8230;</string>
-    <!-- Timestamp string for interactions from yesterday. [CHAR LIMIT=40] -->
-    <string name="yesterday">Yesterday</string>
+    <!-- Timestamp string for interactions from tomorrow. [CHAR LIMIT=40] -->
     <string name="tomorrow">Tomorrow</string>
     <!-- Timestamp string for interactions from today. [CHAR LIMIT=40] -->
     <string name="today">Today</string>
@@ -964,4 +963,834 @@
     <string name="hamburger_feature_highlight_header">Organize your list</string>
     <!-- The body text for hamburger promo [CHAR LIMIT=200]-->
     <string name="hamburger_feature_highlight_body">Clean up duplicates &amp; group contacts by label</string>
+
+    <!-- Toast shown when text is copied to the clipboard [CHAR LIMIT=64] -->
+    <string name="toast_text_copied">Text copied</string>
+    <!-- Option displayed in context menu to copy long pressed item to clipboard [CHAR LIMIT=64] -->
+    <string name="copy_text">Copy to clipboard</string>
+
+    <!-- Action string for calling a custom phone number -->
+    <string name="call_custom">Call
+        <xliff:g id="custom">%s</xliff:g>
+    </string>
+    <!-- Action string for calling a home phone number -->
+    <string name="call_home">Call home</string>
+    <!-- Action string for calling a mobile phone number -->
+    <string name="call_mobile">Call mobile</string>
+    <!-- Action string for calling a work phone number -->
+    <string name="call_work">Call work</string>
+    <!-- Action string for calling a work fax phone number -->
+    <string name="call_fax_work">Call work fax</string>
+    <!-- Action string for calling a home fax phone number -->
+    <string name="call_fax_home">Call home fax</string>
+    <!-- Action string for calling a pager phone number -->
+    <string name="call_pager">Call pager</string>
+    <!-- Action string for calling an other phone number -->
+    <string name="call_other">Call</string>
+    <!-- Action string for calling a callback number -->
+    <string name="call_callback">Call callback</string>
+    <!-- Action string for calling a car phone number -->
+    <string name="call_car">Call car</string>
+    <!-- Action string for calling a company main phone number -->
+    <string name="call_company_main">Call company main</string>
+    <!-- Action string for calling a ISDN phone number -->
+    <string name="call_isdn">Call ISDN</string>
+    <!-- Action string for calling a main phone number -->
+    <string name="call_main">Call main</string>
+    <!-- Action string for calling an other fax phone number -->
+    <string name="call_other_fax">Call fax</string>
+    <!-- Action string for calling a radio phone number -->
+    <string name="call_radio">Call radio</string>
+    <!-- Action string for calling a Telex phone number -->
+    <string name="call_telex">Call telex</string>
+    <!-- Action string for calling a TTY/TDD phone number -->
+    <string name="call_tty_tdd">Call TTY/TDD</string>
+    <!-- Action string for calling a work mobile phone number -->
+    <string name="call_work_mobile">Call work mobile</string>
+    <!-- Action string for calling a work pager phone number -->
+    <string name="call_work_pager">Call work pager</string>
+    <!-- Action string for calling an assistant phone number -->
+    <string name="call_assistant">Call
+        <xliff:g id="assistant">%s</xliff:g>
+    </string>
+    <!-- Action string for calling a MMS phone number -->
+    <string name="call_mms">Call MMS</string>
+    <!-- Action string for calling a contact by shortcut -->
+    <string name="call_by_shortcut"><xliff:g id="contact_name">%s</xliff:g> (Call)</string>
+
+    <!-- Action string for sending an SMS to a custom phone number -->
+    <string name="sms_custom">Text
+        <xliff:g id="custom">%s</xliff:g>
+    </string>
+    <!-- Action string for sending an SMS to a home phone number -->
+    <string name="sms_home">Text home</string>
+    <!-- Action string for sending an SMS to a mobile phone number -->
+    <string name="sms_mobile">Text mobile</string>
+    <!-- Action string for sending an SMS to a work phone number -->
+    <string name="sms_work">Text work</string>
+    <!-- Action string for sending an SMS to a work fax phone number -->
+    <string name="sms_fax_work">Text work fax</string>
+    <!-- Action string for sending an SMS to a home fax phone number -->
+    <string name="sms_fax_home">Text home fax</string>
+    <!-- Action string for sending an SMS to a pager phone number -->
+    <string name="sms_pager">Text pager</string>
+    <!-- Action string for sending an SMS to an other phone number -->
+    <string name="sms_other">Text</string>
+    <!-- Action string for sending an SMS to a callback number -->
+    <string name="sms_callback">Text callback</string>
+    <!-- Action string for sending an SMS to a car phone number -->
+    <string name="sms_car">Text car</string>
+    <!-- Action string for sending an SMS to a company main phone number -->
+    <string name="sms_company_main">Text company main</string>
+    <!-- Action string for sending an SMS to a ISDN phone number -->
+    <string name="sms_isdn">Text ISDN</string>
+    <!-- Action string for sending an SMS to a main phone number -->
+    <string name="sms_main">Text main</string>
+    <!-- Action string for sending an SMS to an other fax phone number -->
+    <string name="sms_other_fax">Text fax</string>
+    <!-- Action string for sending an SMS to a radio phone number -->
+    <string name="sms_radio">Text radio</string>
+    <!-- Action string for sending an SMS to a Telex phone number -->
+    <string name="sms_telex">Text telex</string>
+    <!-- Action string for sending an SMS to a TTY/TDD phone number -->
+    <string name="sms_tty_tdd">Text TTY/TDD</string>
+    <!-- Action string for sending an SMS to a work mobile phone number -->
+    <string name="sms_work_mobile">Text work mobile</string>
+    <!-- Action string for sending an SMS to a work pager phone number -->
+    <string name="sms_work_pager">Text work pager</string>
+    <!-- Action string for sending an SMS to an assistant phone number -->
+    <string name="sms_assistant">Text
+        <xliff:g id="assistant">%s</xliff:g>
+    </string>
+    <!-- Action string for sending an SMS to a MMS phone number -->
+    <string name="sms_mms">Text MMS</string>
+    <!-- Action string for sending an SMS to a contact by shortcut -->
+    <string name="sms_by_shortcut"><xliff:g id="contact_name">%s</xliff:g> (Message)</string>
+
+    <!-- Description string for an action button to initiate a video call. -->
+    <string name="description_video_call">Make video call</string>
+
+    <!-- Title of the confirmation dialog for clearing frequents. [CHAR LIMIT=37] -->
+    <string name="clearFrequentsConfirmation_title">Clear frequently contacted?</string>
+
+    <!-- Confirmation dialog for clearing frequents. [CHAR LIMIT=NONE] -->
+    <string name="clearFrequentsConfirmation">You\'ll clear the frequently contacted list in the
+        Contacts and Phone apps, and force email apps to learn your addressing preferences from
+        scratch.
+    </string>
+
+    <!-- Title of the "Clearing frequently contacted" progress-dialog [CHAR LIMIT=35] -->
+    <string name="clearFrequentsProgress_title">Clearing frequently contacted\u2026</string>
+
+    <!--  Used to display as default status when the contact is available for chat [CHAR LIMIT=19] -->
+    <string name="status_available">Available</string>
+
+    <!--  Used to display as default status when the contact is away or idle for chat [CHAR LIMIT=19] -->
+    <string name="status_away">Away</string>
+
+    <!--  Used to display as default status when the contact is busy or Do not disturb for chat [CHAR LIMIT=19] -->
+    <string name="status_busy">Busy</string>
+
+    <!-- Directory partition name (also exists in contacts) -->
+    <string name="contactsList">Contacts</string>
+
+    <!-- The name of the invisible local contact directory -->
+    <string name="local_invisible_directory">Other</string>
+
+    <!-- The label in section header in the contact list for a contact directory [CHAR LIMIT=128] -->
+    <string name="directory_search_label">Directory</string>
+
+    <!-- The label in section header in the contact list for a work contact directory [CHAR LIMIT=128] -->
+    <string name="directory_search_label_work">Work directory</string>
+
+    <!-- The label in section header in the contact list for a local contacts [CHAR LIMIT=128] -->
+    <string name="local_search_label">All contacts</string>
+
+    <!-- Title shown in the search result activity of contacts app while searching.  [CHAR LIMIT=20]
+         (also in contacts) -->
+    <string name="search_results_searching">Searching\u2026</string>
+
+    <!-- Displayed at the top of search results indicating that more contacts were found than shown [CHAR LIMIT=64] -->
+    <string name="foundTooManyContacts">More than <xliff:g id="count">%d</xliff:g> found.</string>
+
+    <!-- Displayed at the top of the contacts showing the zero total number of contacts found when "Only contacts with phones" not selected. [CHAR LIMIT=30]
+         (also in contacts) -->
+    <string name="listFoundAllContactsZero">No contacts</string>
+
+    <!-- Displayed at the top of the contacts showing the total number of contacts found when typing search query -->
+    <plurals name="searchFoundContacts">
+        <item quantity="one">1 found</item>
+        <item quantity="other"><xliff:g id="count">%d</xliff:g> found</item>
+    </plurals>
+
+    <!-- String describing the text for photo of a contact in a contacts list.
+
+        Note: AccessibilityServices use this attribute to announce what the view represents.
+              This is especially valuable for views without textual representation like ImageView.
+    -->
+    <string name="description_quick_contact_for">Quick contact for <xliff:g id="name">%1$s</xliff:g></string>
+
+    <!-- Shown as the display name for a person when the name is missing or unknown. [CHAR LIMIT=18]-->
+    <string name="missing_name">(No name)</string>
+
+    <!-- The text displayed on the divider for the Favorites tab in People app indicating that items below it are frequently contacted [CHAR LIMIT = 39] -->
+    <string name="favoritesFrequentContacted">Frequently contacted</string>
+
+    <!-- String describing a contact picture that introduces users to the contact detail screen.
+
+       Used by AccessibilityService to announce the purpose of the button.
+
+       [CHAR LIMIT=NONE]
+    -->
+    <string name="description_view_contact_detail" msgid="2795575601596468581">View contact</string>
+
+    <!-- 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>
+
+    <!-- Contact list filter selection indicating that the list shows all work contacts with phone numbers [CHAR LIMIT=64] -->
+    <string name="list_filter_phones_work">Work profile contacts</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. [CHAR LIMIT=20]  -->
+    <string name="account_phone">Device</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">First name</string>
+    <!-- Field title for the family name of a contact -->
+    <string name="name_family">Last 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 first 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 last 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">Special date</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">SIP</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 labels for a contact [CHAR LIMIT=20] -->
+    <string name="groupsLabel">Labels</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>
+
+    <!-- Content description for the expand or collapse phonetic name fields button. [CHAR LIMIT=100] -->
+    <string name="expand_collapse_phonetic_name_fields_description">Expand or collapse phonetic
+        name fields</string>
+
+    <!-- Contact list filter label indicating that the list is showing all available accounts [CHAR LIMIT=64] -->
+    <string name="list_filter_all_accounts">All contacts</string>
+
+    <!-- Contact list filter label indicating that the list is showing all starred contacts [CHAR LIMIT=64] -->
+    <string name="list_filter_all_starred">Starred</string>
+
+    <!-- Contact list filter selection indicating that the list shows groups chosen by the user [CHAR LIMIT=64] -->
+    <string name="list_filter_customize">Customize</string>
+
+    <!-- Contact list filter selection indicating that the list shows only the selected contact [CHAR LIMIT=64] -->
+    <string name="list_filter_single">Contact</string>
+
+    <!-- List title for a special contacts group that covers all contacts. [CHAR LIMIT=25] -->
+    <string name="display_ungrouped">All other contacts</string>
+
+    <!-- List title for a special contacts group that covers all contacts that aren't members of any other group. [CHAR LIMIT=25] -->
+    <string name="display_all_contacts">All contacts</string>
+
+    <!-- Menu item to remove a contacts sync group. [CHAR LIMIT=40] -->
+    <string name="menu_sync_remove">Remove sync group</string>
+
+    <!-- Menu item to add a contacts sync group. [CHAR LIMIT=40] -->
+    <string name="dialog_sync_add">Add sync group</string>
+
+    <!-- Text displayed in the sync groups footer view for unknown sync groups. [CHAR LIMIT=40 -->
+    <string name="display_more_groups">More groups\u2026</string>
+
+    <!-- Warning message given to users just before they remove a currently syncing
+         group that would also cause all ungrouped contacts to stop syncing.  [CHAR LIMIT=NONE] -->
+    <string name="display_warn_remove_ungrouped">Removing \"<xliff:g id="group" example="Starred">%s</xliff:g>\" from sync will also remove any ungrouped contacts from sync.</string>
+
+    <!-- Displayed in a spinner dialog as user changes to display options are saved -->
+    <string name="savingDisplayGroups">Saving display options\u2026</string>
+
+    <!-- Menu item to indicate you are done editing a contact and want to save the changes you've made -->
+    <string name="menu_done">Done</string>
+
+    <!-- Menu item to indicate you want to cancel the current editing process and NOT save the changes you've made [CHAR LIMIT=12] -->
+    <string name="menu_doNotSave">Cancel</string>
+
+    <!-- Displayed at the top of the contacts showing single contact. [CHAR LIMIT=50] -->
+    <string name="listCustomView">Customized view</string>
+
+    <!-- Message asking user to select an account to save contacts imported from vcard or SIM card [CHAR LIMIT=64] -->
+    <string name="dialog_new_contact_account">Save imported contacts to:</string>
+
+    <!-- Action string for selecting SIM for importing contacts -->
+    <string name="import_from_sim">Import from SIM card</string>
+
+    <!-- Action string for selecting a SIM subscription for importing contacts -->
+    <string name="import_from_sim_summary">Import from SIM <xliff:g id="sim_name">^1</xliff:g> - <xliff:g id="sim_number">^2</xliff:g></string>
+
+    <!-- Action string for selecting a SIM subscription for importing contacts, without a phone number -->
+    <string name="import_from_sim_summary_no_number">Import from SIM <xliff:g id="sim_name">%1$s</xliff:g></string>
+
+    <!-- Action string for selecting a .vcf file to import contacts from [CHAR LIMIT=30] -->
+    <string name="import_from_vcf_file" product="default">Import from .vcf file</string>
+
+    <!-- Message shown in a Dialog confirming a user's cancel request toward existing vCard import.
+         The argument is file name for the vCard import the user wants to cancel.
+         [CHAR LIMIT=128] -->
+    <string name="cancel_import_confirmation_message">Cancel import of <xliff:g id="filename" example="import.vcf">%s</xliff:g>?</string>
+
+    <!-- Message shown in a Dialog confirming a user's cancel request toward existing vCard export.
+         The argument is file name for the vCard export the user wants to cancel.
+         [CHAR LIMIT=128] -->
+    <string name="cancel_export_confirmation_message">Cancel export of <xliff:g id="filename" example="export.vcf">%s</xliff:g>?</string>
+
+    <!-- Title shown in a Dialog telling users cancel vCard import/export operation is failed. [CHAR LIMIT=40] -->
+    <string name="cancel_vcard_import_or_export_failed">Couldn\'t cancel vCard import/export</string>
+
+    <!-- The failed reason which should not be shown but it may in some buggy condition. [CHAR LIMIT=40] -->
+    <string name="fail_reason_unknown">Unknown error.</string>
+
+    <!-- The failed reason shown when vCard importer/exporter could not open the file
+         specified by a user. The file name should be in the message. [CHAR LIMIT=NONE] -->
+    <string name="fail_reason_could_not_open_file">Couldn\'t open \"<xliff:g id="file_name">%s</xliff:g>\": <xliff:g id="exact_reason">%s</xliff:g>.</string>
+
+    <!-- The failed reason shown when contacts exporter fails to be initialized.
+         Some exact reason must follow this. [CHAR LIMIT=NONE]-->
+    <string name="fail_reason_could_not_initialize_exporter">Couldn\'t start the exporter: \"<xliff:g id="exact_reason">%s</xliff:g>\".</string>
+
+    <!-- The failed reason shown when there's no contact which is allowed to be exported.
+         Note that user may have contacts data but all of them are probably not allowed to be
+         exported because of security/permission reasons. [CHAR LIMIT=NONE] -->
+    <string name="fail_reason_no_exportable_contact">There is no exportable contact.</string>
+
+    <!-- The user doesn't have all permissions required to use the current screen. So
+         close the current screen and show the user this message. -->
+    <string name="missing_required_permission">You have disabled a required permission.</string>
+
+    <!-- The failed reason shown when some error happend during contacts export.
+         Some exact reason must follow this. [CHAR LIMIT=NONE] -->
+    <string name="fail_reason_error_occurred_during_export">An error occurred during export: \"<xliff:g id="exact_reason">%s</xliff:g>\".</string>
+
+    <!-- The failed reason shown when the given file name is too long for the system.
+         The length limit of each file is different in each Android device, so we don't need to
+         mention it here. [CHAR LIMIT=NONE] -->
+    <string name="fail_reason_too_long_filename">Required filename is too long (\"<xliff:g id="filename">%s</xliff:g>\").</string>
+
+    <!-- The failed reason shown when Contacts app (especially vCard importer/exporter)
+         emitted some I/O error. Exact reason will be appended by the system. [CHAR LIMIT=NONE] -->
+    <string name="fail_reason_io_error">I/O error</string>
+
+    <!-- Failure reason show when Contacts app (especially vCard importer) encountered
+         low memory problem and could not proceed its import procedure. [CHAR LIMIT=NONE] -->
+    <string name="fail_reason_low_memory_during_import">Not enough memory. The file may be too large.</string>
+
+    <!-- The failed reason shown when vCard parser was not able to be parsed by the current vCard
+         implementation. This might happen even when the input vCard is completely valid, though
+         we believe it is rather rare in the actual world. [CHAR LIMIT=NONE] -->
+    <string name="fail_reason_vcard_parse_error">Couldn\'t parse vCard for an unexpected reason.</string>
+
+    <!-- The failed reason shown when vCard importer doesn't support the format.
+         This may be shown when the vCard is corrupted [CHAR LIMIT=40] -->
+    <string name="fail_reason_not_supported">The format isn\'t supported.</string>
+
+    <!-- Fail reason shown when vCard importer failed to look over meta information stored in vCard file(s). -->
+    <string name="fail_reason_failed_to_collect_vcard_meta_info">Couldn\'t collect meta information of given vCard file(s).</string>
+
+    <!-- The failed reason shown when the import of some of vCard files failed during multiple vCard
+         files import. It includes the case where all files were failed to be imported. -->
+    <string name="fail_reason_failed_to_read_files">One or more files couldn\'t be imported (%s).</string>
+
+    <!-- The title shown when exporting vCard is successfuly finished [CHAR LIMIT=40] -->
+    <string name="exporting_vcard_finished_title">Finished exporting <xliff:g id="filename" example="export.vcf">%s</xliff:g>.</string>
+
+    <!-- The title shown when exporting vCard has finished successfully but the destination filename could not be resolved. [CHAR LIMIT=NONE] -->
+    <string name="exporting_vcard_finished_title_fallback">Finished exporting contacts.</string>
+
+    <!-- The toast message shown when exporting vCard has finished and vCards are ready to be shared [CHAR LIMIT=150]-->
+    <string name="exporting_vcard_finished_toast">Finished exporting contacts, click the notification to share contacts.</string>
+
+    <!-- The message on notification shown when exporting vCard has finished and vCards are ready to be shared [CHAR LIMIT=60]-->
+    <string name="touch_to_share_contacts">Tap to share contacts.</string>
+
+    <!-- The title shown when exporting vCard is canceled (probably by a user)
+         The argument is file name the user canceled importing.
+         [CHAR LIMIT=40] -->
+    <string name="exporting_vcard_canceled_title">Exporting <xliff:g id="filename" example="export.vcf">%s</xliff:g> canceled.</string>
+
+    <!-- Dialog title shown when the application is exporting contact data outside. [CHAR LIMIT=NONE] -->
+    <string name="exporting_contact_list_title">Exporting contact data</string>
+
+    <!-- Message shown when the application is exporting contact data outside -->
+    <string name="exporting_contact_list_message">Contact data is being exported.</string>
+
+    <!-- The error reason the vCard composer "may" emit when database is corrupted or
+         something is going wrong. Usually users should not see this text. [CHAR LIMIT=NONE] -->
+    <string name="composer_failed_to_get_database_infomation">Couldn\'t get database information.</string>
+
+    <!-- This error message shown when the user actually have no contact
+         (e.g. just after data-wiping), or, data providers of the contact list prohibits their
+         contacts from being exported to outside world via vcard exporter, etc. [CHAR LIMIT=NONE] -->
+    <string name="composer_has_no_exportable_contact">There are no exportable contacts. If you do have contacts on your phone, some data providers may not allow the contacts to be exported from the phone.</string>
+
+    <!-- The error reason the vCard composer may emit when vCard composer is not initialized
+         even when needed.
+         Users should not usually see this error message. [CHAR LIMIT=NONE] -->
+    <string name="composer_not_initialized">The vCard composer didn\'t start properly.</string>
+
+    <!-- Dialog title shown when exporting Contact data failed. [CHAR LIMIT=20] -->
+    <string name="exporting_contact_failed_title">Couldn\'t export</string>
+
+    <!-- Dialog message shown when exporting Contact data failed. [CHAR LIMIT=NONE] -->
+    <string name="exporting_contact_failed_message">The contact data wasn\'t exported.\nReason: \"<xliff:g id="fail_reason">%s</xliff:g>\"</string>
+
+    <!-- Description shown when importing vCard data.
+         The argument is the name of a contact which is being read.
+         [CHAR LIMIT=20] -->
+    <string name="importing_vcard_description">Importing <xliff:g id="name" example="Joe Due">%s</xliff:g></string>
+
+    <!-- Dialog title shown when reading vCard data failed [CHAR LIMIT=40] -->
+    <string name="reading_vcard_failed_title">Couldn\'t read vCard data</string>
+
+    <!-- The title shown when reading vCard is canceled (probably by a user)
+         [CHAR LIMIT=40] -->
+    <string name="reading_vcard_canceled_title">Reading vCard data canceled</string>
+
+    <!-- The title shown when reading vCard finished
+         The argument is file name the user imported.
+         [CHAR LIMIT=40] -->
+    <string name="importing_vcard_finished_title">Finished importing vCard <xliff:g id="filename" example="import.vcf">%s</xliff:g></string>
+
+    <!-- The title shown when importing vCard is canceled (probably by a user)
+         The argument is file name the user canceled importing.
+         [CHAR LIMIT=40] -->
+    <string name="importing_vcard_canceled_title">Importing <xliff:g id="filename" example="import.vcf">%s</xliff:g> canceled</string>
+
+    <!-- The message shown when vCard import request is accepted. The system may start that work soon, or do it later
+         when there are already other import/export requests.
+         The argument is file name the user imported.
+         [CHAR LIMIT=40] -->
+    <string name="vcard_import_will_start_message"><xliff:g id="filename" example="import.vcf">%s</xliff:g> will be imported shortly.</string>
+    <!-- The message shown when vCard import request is accepted. The system may start that work soon, or do it later when there are already other import/export requests.
+         "The file" is what a user selected for importing.
+         [CHAR LIMIT=40] -->
+    <string name="vcard_import_will_start_message_with_default_name">The file will be imported shortly.</string>
+    <!-- The message shown when a given vCard import request is rejected by the system. [CHAR LIMIT=NONE] -->
+    <string name="vcard_import_request_rejected_message">vCard import request was rejected. Try again later.</string>
+    <!-- The message shown when vCard export request is accepted. The system may start that work soon, or do it later
+         when there are already other import/export requests.
+         The argument is file name the user exported.
+         [CHAR LIMIT=40] -->
+    <string name="vcard_export_will_start_message"><xliff:g id="filename" example="import.vcf">%s</xliff:g> will be exported shortly.</string>
+
+    <!-- The message shown when a vCard export request is accepted but the destination filename could not be resolved. [CHAR LIMIT=NONE] -->
+    <string name="vcard_export_will_start_message_fallback">The file will be exported shortly.</string>
+
+    <!-- The message shown when a vCard export request is accepted and contacts will be exported shortly. [CHAR LIMIT=70]-->
+    <string name="contacts_export_will_start_message">Contacts will be exported shortly.</string>
+
+    <!-- The message shown when a given vCard export request is rejected by the system. [CHAR LIMIT=NONE] -->
+    <string name="vcard_export_request_rejected_message">vCard export request was rejected. Try again later.</string>
+    <!-- Used when file name is unknown in vCard processing. It typically happens
+         when the file is given outside the Contacts app. [CHAR LIMIT=30] -->
+    <string name="vcard_unknown_filename">contact</string>
+
+    <!-- The message shown when vCard importer is caching files to be imported into local temporary
+         data storage. [CHAR LIMIT=NONE] -->
+    <string name="caching_vcard_message">Caching vCard(s) to local temporary storage. The actual import will start soon.</string>
+
+    <!-- Message used when vCard import has failed. [CHAR LIMIT=40] -->
+    <string name="vcard_import_failed">Couldn\'t import vCard.</string>
+
+    <!-- The "file name" displayed for vCards received directly via NFC [CHAR LIMIT=16] -->
+    <string name="nfc_vcard_file_name">Contact received over NFC</string>
+
+    <!-- Dialog title shown when a user confirms whether he/she export Contact data. [CHAR LIMIT=32] -->
+    <string name="confirm_export_title">Export contacts?</string>
+
+    <!-- The title shown when vCard importer is caching files to be imported into local temporary
+         data storage.  [CHAR LIMIT=40] -->
+    <string name="caching_vcard_title">Caching</string>
+
+    <!-- The message shown while importing vCard(s).
+         First argument is current index of contacts to be imported.
+         Second argument is the total number of contacts.
+         Third argument is the name of a contact which is being read.
+         [CHAR LIMIT=20] -->
+    <string name="progress_notifier_message">Importing <xliff:g id="current_number">%s</xliff:g>/<xliff:g id="total_number">%s</xliff:g>: <xliff:g id="name" example="Joe Due">%s</xliff:g></string>
+
+    <!-- Action that exports all contacts to a user selected destination. [CHAR LIMIT=25] -->
+    <string name="export_to_vcf_file" product="default">Export to .vcf file</string>
+
+    <!-- Contact preferences related strings -->
+
+    <!-- Label of the "sort by" display option -->
+    <string name="display_options_sort_list_by">Sort by</string>
+
+    <!-- An allowable value for the "sort list by" contact display option  -->
+    <string name="display_options_sort_by_given_name">First name</string>
+
+    <!-- An allowable value for the "sort list by" contact display option  -->
+    <string name="display_options_sort_by_family_name">Last name</string>
+
+    <!-- Label of the "name format" display option [CHAR LIMIT=64]-->
+    <string name="display_options_view_names_as">Name format</string>
+
+    <!-- An allowable value for the "view names as" contact display option  -->
+    <string name="display_options_view_given_name_first">First name first</string>
+
+    <!-- An allowable value for the "view names as" contact display option  -->
+    <string name="display_options_view_family_name_first">Last name first</string>
+
+    <!--Lable of the "Accounts" in settings [CHAR LIMIT=30]-->
+    <string name="settings_accounts">Accounts</string>
+
+    <!--Label of the "default account" setting option to set default editor account. [CHAR LIMIT=80]-->
+    <string name="default_editor_account">Default account for new contacts</string>
+
+    <!--Label of the "Sync contact metadata" setting option to set sync account for Lychee. [CHAR LIMIT=80]-->
+    <string name="sync_contact_metadata_title">Sync contact metadata [DOGFOOD]</string>
+
+    <!--Label of the "Sync contact metadata" setting dialog to set sync account for Lychee. [CHAR LIMIT=80]-->
+    <string name="sync_contact_metadata_dialog_title">Sync contact metadata</string>
+
+    <!-- Title of my info preference, showing the name of user's personal profile [CHAR LIMIT=30]-->
+    <string name="settings_my_info_title">My info</string>
+
+    <!-- Displayed below my info for user to set up the user's personal profile entry [CHAR LIMIT=64] -->
+    <string name="set_up_profile">Set up your profile</string>
+
+    <!-- Label of the "About" setting -->
+    <string name="setting_about">About Contacts</string>
+
+    <!-- Title of the settings activity [CHAR LIMIT=64] -->
+    <string name="activity_title_settings">Settings</string>
+
+    <!-- Action that shares visible contacts -->
+    <string name="share_visible_contacts">Share visible contacts</string>
+
+    <!-- A framework exception (ie, transaction too large) can be thrown while attempting to share all visible contacts. If so, show this toast. -->
+    <string name="share_visible_contacts_failure">Failed to share visible contacts.</string>
+
+    <!-- Action that shares favorite contacts [CHAR LIMIT=40]-->
+    <string name="share_favorite_contacts">Share favorite contacts</string>
+
+    <!-- Action that shares contacts [CHAR LIMIT=30]-->
+    <string name="share_contacts">Share all contacts</string>
+
+    <!-- A framework exception can be thrown while attempting to share all contacts. If so, show this toast. [CHAR LIMIT=40]-->
+    <string name="share_contacts_failure">Failed to share contacts.</string>
+
+    <!-- Dialog title when selecting the bulk operation to perform from a list. [CHAR LIMIT=36] -->
+    <string name="dialog_import_export">Import/export contacts</string>
+
+    <!-- Dialog title when importing contacts from an external source. [CHAR LIMIT=36] -->
+    <string name="dialog_import">Import contacts</string>
+
+    <!-- Toast indicating that sharing a contact has failed. [CHAR LIMIT=NONE]  -->
+    <string name="share_error">This contact can\'t be shared.</string>
+
+    <!-- Toast indicating that no visible contact to share [CHAR LIMIT=NONE]  -->
+    <string name="no_contact_to_share">There are no contacts to share.</string>
+
+    <!-- Menu item to search contacts -->
+    <string name="menu_search">Search</string>
+
+    <!-- The menu item to filter the list of contacts displayed -->
+    <string name="menu_contacts_filter">Contacts to display</string>
+
+    <!-- Title of the activity that allows the uesr to filter the list of contacts displayed according to account [CHAR LIMIT=25] -->
+    <string name="activity_title_contacts_filter">Contacts to display</string>
+
+    <!-- Title of the activity that allows the user to customize filtering of contact list [CHAR LIMIT=128] -->
+    <string name="custom_list_filter">Define customized view</string>
+
+    <!-- Menu item to save changes to custom filter. [CHAR LIMIT=15] -->
+    <string name="menu_custom_filter_save">Save</string>
+
+    <!-- Query hint displayed inside the search field [CHAR LIMIT=64] -->
+    <string name="hint_findContacts">Search contacts</string>
+
+    <!-- The description text for the favorites tab.
+
+         Note: AccessibilityServices use this attribute to announce what the view represents.
+         This is especially valuable for views without textual representation like ImageView.
+
+         [CHAR LIMIT=NONE] -->
+    <string name="contactsFavoritesLabel">Favorites</string>
+
+    <!-- Displayed at the top of the contacts showing the zero total number of contacts visible when "All contacts" is selected  [CHAR LIMIT=64]-->
+    <string name="listTotalAllContactsZero">No contacts.</string>
+
+    <!--  The menu item to clear frequents [CHAR LIMIT=30] -->
+    <string name="menu_clear_frequents">Clear frequents</string>
+
+    <!-- Menu item to select SIM card -->
+    <string name="menu_select_sim">Select SIM card</string>
+
+    <!-- The menu item to open the list of accounts. [CHAR LIMIT=60]-->
+    <string name="menu_accounts">Manage accounts</string>
+
+    <!-- The menu item to bulk import or bulk export contacts from SIM card or SD card.  [CHAR LIMIT=30]-->
+    <string name="menu_import_export">Import/export</string>
+
+    <!-- The menu item to open blocked numbers activity [CHAR LIMIT=60]-->
+    <string name="menu_blocked_numbers">Blocked numbers</string>
+
+    <!-- The font-family to use for tab text.
+         Do not translate. -->
+    <string name="tab_font_family">sans-serif</string>
+
+    <!-- Attribution of a contact status update, when the time of update is unknown -->
+    <string name="contact_status_update_attribution">via <xliff:g id="source" example="Google Talk">%1$s</xliff:g></string>
+
+    <!-- Attribution of a contact status update, when the time of update is known -->
+    <string name="contact_status_update_attribution_with_date"><xliff:g id="date" example="3 hours ago">%1$s</xliff:g> via <xliff:g id="source" example="Google Talk">%2$s</xliff:g></string>
+
+    <!-- Font family used when drawing letters for letter tile avatars.
+         Do not translate. -->
+    <string name="letter_tile_letter_font_family">sans-serif-medium</string>
+
+    <!-- Content description for the fake action menu up button as used
+     inside search. [CHAR LIMIT=NONE] -->
+    <string name="action_menu_back_from_search">stop searching</string>
+
+    <!--  String describing the icon used to clear the search field -->
+    <string name="description_clear_search">Clear search</string>
+
+    <!-- The font-family to use for the text inside the searchbox.
+         Do not translate. -->
+    <string name="search_font_family">sans-serif</string>
+
+    <!-- The title of the preference section that allows users to configure how they want their
+         contacts to be displayed. [CHAR LIMIT=128] -->
+    <string name="settings_contact_display_options_title">Contact display options</string>
+
+    <!-- Title for Select Account Dialog [CHAR LIMIT=30] -->
+    <string name="select_account_dialog_title">Account</string>
+
+    <!-- Label for the check box to toggle default sim card setting [CHAR LIMIT=35]-->
+    <string name="set_default_account">Always use this for calls</string>
+
+    <!-- Title for dialog to select Phone Account for outgoing call.  [CHAR LIMIT=40] -->
+    <string name="select_phone_account_for_calls">Call with</string>
+
+    <!-- String used for actions in the dialer call log and the quick contact card to initiate
+         a call to an individual.  The user is prompted to enter a note which is sent along with
+         the call (e.g. a call subject). [CHAR LIMIT=40] -->
+    <string name="call_with_a_note">Call with a note</string>
+
+    <!-- Hint text shown in the call subject dialog. [CHAR LIMIT=255] -->
+    <string name="call_subject_hint">Type a note to send with call ...</string>
+
+    <!-- Button used to start a new call with the user entered subject. [CHAR LIMIT=32] -->
+    <string name="send_and_call_button">SEND &amp; CALL</string>
+
+    <!-- String used to represent the total number of characters entered for a call subject,
+         compared to the character limit.  Example: 2 / 64 -->
+    <string name="call_subject_limit"><xliff:g id="count" example="4">%1$s</xliff:g> / <xliff:g id="limit" example="64">%2$s</xliff:g></string>
+
+    <!-- String used to build a phone number bype and phone number string.
+         Example: Mobile • 650-555-1212  -->
+    <string name="call_subject_type_and_number"><xliff:g id="type" example="Mobile">%1$s</xliff:g> • <xliff:g id="number" example="(650) 555-1212">%2$s</xliff:g></string>
+
+    <!-- String format to describe the number of unread items in a tab.
+
+        Note: AccessibilityServices use this attribute to announce what the view represents.
+              This is especially valuable for views without textual representation like ImageView.
+    -->
+    <plurals name="tab_title_with_unread_items">
+        <item quantity="one">
+            <xliff:g id="title">%1$s</xliff:g>. <xliff:g id="count">%2$d</xliff:g> unread item.
+        </item>
+        <item quantity="other">
+            <xliff:g id="title">%1$s</xliff:g>. <xliff:g id="count">%2$d</xliff:g> unread items.
+        </item>
+    </plurals>
+
+    <!-- Build version title in About preference. [CHAR LIMIT=40]-->
+    <string name="about_build_version">Build version</string>
+
+    <!-- Open source licenses title in About preference. [CHAR LIMIT=60] -->
+    <string name="about_open_source_licenses">Open source licenses</string>
+
+    <!-- Open source licenses summary in About preference. [CHAR LIMIT=NONE] -->
+    <string name="about_open_source_licenses_summary">License details for open source software</string>
+
+    <!-- Privacy policy title in About preference. [CHAR LIMIT=40]-->
+    <string name="about_privacy_policy">Privacy policy</string>
+
+    <!-- Terms of service title in about preference. [CHAR LIMIT=60]-->
+    <string name="about_terms_of_service">Terms of service</string>
+
+    <!-- Title for the activity that displays licenses for open source libraries. [CHAR LIMIT=100]-->
+    <string name="activity_title_licenses">Open source licenses</string>
+
+    <!-- Toast message showing when failed to open the url. [CHAR LIMIT=100]-->
+    <string name="url_open_error_toast">Failed to open the url.</string>
+
+    <!-- Content description of entries (including that radio button is checked) in contact
+         accounts list filter. For example: Google abc@gmail.com checked, etc [CHAR LIMIT=30]-->
+    <string name="account_filter_view_checked"><xliff:g id="account_info">%s</xliff:g> checked</string>
+
+    <!-- Content description of entries (including that the radio button is not checked) in contact
+         accounts list filter. For example: Google abc@gmail.com not checked, etc [CHAR LIMIT=30]-->
+    <string name="account_filter_view_not_checked"><xliff:g id="account_info">%s</xliff:g> not checked</string>
+
+    <!-- Description string for an action button to initiate a video call from search results.
+         Note: AccessibilityServices use this attribute to announce what the view represents.
+         This is especially valuable for views without textual representation like ImageView.
+
+         [CHAR LIMIT=NONE]-->
+    <string name="description_search_video_call">Place video call</string>
+
+    <!-- Content description of delete contact button [CHAR LIMIT=30]-->
+    <string name="description_delete_contact">Delete</string>
+
+    <!-- Content description for (...) in no name header [CHAR LIMIT=30]-->
+    <string name="description_no_name_header">Ellipsis</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 9c6ace4..9ff882d 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -434,4 +434,98 @@
         <item name="android:fontFamily">sans-serif-medium</item>
         <item name="android:textAllCaps">true</item>
     </style>
+    <style name="DirectoryHeader">
+        <item name="android:background">@android:color/transparent</item>
+    </style>
+
+    <style name="SectionHeaderStyle" parent="@android:style/TextAppearance.Large">
+        <item name="android:textSize">16sp</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textColor">@color/section_header_text_color</item>
+        <item name="android:textStyle">bold</item>
+    </style>
+
+    <style name="DirectoryHeaderStyle" parent="@android:style/TextAppearance.Small">
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">@color/dialtacts_secondary_text_color</item>
+        <item name="android:fontFamily">sans-serif-medium</item>
+    </style>
+
+    <!-- TextView style used for headers.
+
+This is similar to ?android:attr/listSeparatorTextView but uses different
+background and text color. See also android:style/Widget.Holo.TextView.ListSeparator
+(which is private, so we cannot specify it as a parent style).  -->
+    <style name="ContactListSeparatorTextViewStyle">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <!-- See comments for @dimen/list_section_divider_min_height -->
+        <item name="android:minHeight">@dimen/list_section_divider_min_height</item>
+        <item name="android:textAppearance">@style/DirectoryHeaderStyle</item>
+        <item name="android:gravity">center_vertical</item>
+        <item name="android:paddingLeft">8dip</item>
+        <item name="android:paddingStart">8dip</item>
+        <item name="android:paddingTop">4dip</item>
+        <item name="android:paddingBottom">4dip</item>
+        <item name="android:ellipsize">end</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:textAllCaps">true</item>
+    </style>
+
+    <style name="TextAppearanceMedium" parent="@android:style/TextAppearance.Medium">
+        <item name="android:textSize">16sp</item>
+        <item name="android:textColor">#000000</item>
+    </style>
+
+    <style name="TextAppearanceSmall" parent="@android:style/TextAppearance.Small">
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">#737373</item>
+    </style>
+
+    <style name="ListViewStyle" parent="@android:style/Widget.Material.Light.ListView">
+        <item name="android:overScrollMode">always</item>
+    </style>
+
+    <style name="ContactListFilterTheme" parent="@android:Theme.Holo.Light">
+        <item name="android:listViewStyle">@style/ListViewStyle</item>
+        <item name="android:actionButtonStyle">@style/FilterActionButtonStyle</item>
+    </style>
+
+    <!-- Adding padding to action button doesn't move it to left, we increase the button width to
+     make margin between the button and screen edge 16dp -->
+    <style name="FilterActionButtonStyle" parent="@android:Widget.ActionButton">
+        <item name="android:minWidth">@dimen/contact_filter_action_button_width</item>
+        <item name="android:textColor">@color/actionbar_text_color</item>
+    </style>
+
+    <style name="CustomContactListFilterView" parent="ContactListFilterTheme">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">match_parent</item>
+    </style>
+
+    <style name="BackgroundOnlyTheme" parent="@android:style/Theme.Material.Light">
+        <item name="android:windowBackground">@null</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowAnimationStyle">@null</item>
+        <item name="android:windowNoTitle">true</item>
+        <!-- Activities that use this theme are background activities without obvious displays.
+            However, some also have dialogs. Therefore, it doesn't make sense to set this true.-->
+        <item name="android:windowNoDisplay">false</item>
+        <item name="android:windowIsFloating">true</item>
+    </style>
+
+    <style name="Theme.CallSubjectDialogTheme" parent="@android:style/Theme.Material.Light.Dialog">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">match_parent</item>
+
+        <!-- No backgrounds, titles or window float -->
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowFullscreen">false</item>
+        <item name="android:windowIsFloating">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowElevation">0dp</item>
+    </style>
 </resources>
diff --git a/res/xml/preference_about.xml b/res/xml/preference_about.xml
new file mode 100644
index 0000000..a109db6
--- /dev/null
+++ b/res/xml/preference_about.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+    <Preference
+            android:icon="@null"
+            android:key="@string/pref_build_version_key"
+            android:title="@string/about_build_version"/>
+
+    <Preference
+            android:icon="@null"
+            android:key="@string/pref_open_source_licenses_key"
+            android:title="@string/about_open_source_licenses"
+            android:summary="@string/about_open_source_licenses_summary"/>
+
+    <Preference
+            android:icon="@null"
+            android:key="@string/pref_privacy_policy_key"
+            android:title="@string/about_privacy_policy">
+    </Preference>
+
+    <Preference
+            android:icon="@null"
+            android:key="@string/pref_terms_of_service_key"
+            android:title="@string/about_terms_of_service">
+    </Preference>
+</PreferenceScreen>
diff --git a/res/xml/preference_display_options.xml b/res/xml/preference_display_options.xml
new file mode 100644
index 0000000..f7a6514
--- /dev/null
+++ b/res/xml/preference_display_options.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+    <Preference
+        android:icon="@null"
+        android:key="myInfo"
+        android:title="@string/settings_my_info_title"/>
+
+    <Preference
+        android:icon="@null"
+        android:key="accounts"
+        android:title="@string/settings_accounts">
+    </Preference>
+
+    <com.android.contacts.common.preference.DefaultAccountPreference
+        android:icon="@null"
+        android:key="defaultAccount"
+        android:title="@string/default_editor_account"
+        android:dialogTitle="@string/default_editor_account" />
+
+    <Preference
+        android:icon="@null"
+        android:key="customContactsFilter"
+        android:title="@string/menu_contacts_filter"/>
+
+    <com.android.contacts.common.preference.SortOrderPreference
+        android:icon="@null"
+        android:key="sortOrder"
+        android:title="@string/display_options_sort_list_by"
+        android:dialogTitle="@string/display_options_sort_list_by" />
+
+    <com.android.contacts.common.preference.DisplayOrderPreference
+        android:icon="@null"
+        android:key="displayOrder"
+        android:title="@string/display_options_view_names_as"
+        android:dialogTitle="@string/display_options_view_names_as" />
+
+    <Preference
+        android:icon="@null"
+        android:key="importExport"
+        android:title="@string/menu_import_export"/>
+
+    <Preference
+        android:icon="@null"
+        android:key="blockedNumbers"
+        android:title="@string/menu_blocked_numbers" />
+
+    <Preference
+        android:icon="@null"
+        android:key="about"
+        android:title="@string/setting_about"
+        android:order="@integer/about_contacts_order_number"/>
+</PreferenceScreen>
diff --git a/src-bind/com/android/commonbind/ObjectFactory.java b/src-bind/com/android/commonbind/ObjectFactory.java
new file mode 100644
index 0000000..3bbd52d
--- /dev/null
+++ b/src-bind/com/android/commonbind/ObjectFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 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.commonbind;
+
+import com.android.contacts.common.logging.Logger;
+import com.android.contacts.common.preference.PreferenceManager;
+
+import android.content.Context;
+
+/**
+ * Creates default bindings for overlays.
+ */
+public class ObjectFactory {
+
+    public static Logger getLogger() {
+        return null;
+    }
+
+    public static PreferenceManager getPreferenceManager(Context context) { return null; }
+}
diff --git a/src-bind/com/android/commonbind/analytics/AnalyticsUtil.java b/src-bind/com/android/commonbind/analytics/AnalyticsUtil.java
new file mode 100644
index 0000000..84420b6
--- /dev/null
+++ b/src-bind/com/android/commonbind/analytics/AnalyticsUtil.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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.commonbind.analytics;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Fragment;
+import android.text.TextUtils;
+
+public class AnalyticsUtil {
+
+    /**
+     * Initialize this class and setup automatic activity tracking.
+     */
+    public static void initialize(Application application) { }
+
+    /**
+     * Log a screen view for {@param fragment}.
+     */
+    public static void sendScreenView(Fragment fragment) {}
+
+    public static void sendScreenView(Fragment fragment, Activity activity) {}
+
+    public static void sendScreenView(Fragment fragment, Activity activity, String tag) {}
+
+    public static void sendScreenView(String fragmentName, Activity activity, String tag) {}
+
+    /**
+     * Logs a event to the analytics server.
+     *
+     * @param application The application the tracker is stored in.
+     * @param category The category for the event.
+     * @param action The event action.
+     * @param label The event label.
+     * @param value The value associated with the event.
+     */
+    public static void sendEvent(Application application, String category, String action,
+            String label, long value) { }
+}
\ No newline at end of file
diff --git a/src-bind/com/android/commonbind/experiments/Flags.java b/src-bind/com/android/commonbind/experiments/Flags.java
new file mode 100644
index 0000000..875712f
--- /dev/null
+++ b/src-bind/com/android/commonbind/experiments/Flags.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016 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.commonbind.experiments;
+
+import android.content.Context;
+
+import com.android.contacts.common.Experiments;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides getters for experiment flags.
+ * This stub class is designed to be overwritten by an overlay.
+ */
+public final class Flags {
+
+    private static Flags sInstance;
+
+    private Map<String,Boolean> mMap;
+
+    public static Flags getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new Flags();
+        }
+        return sInstance;
+    }
+
+    private Flags() {
+        mMap = new HashMap<>();
+    }
+
+    public boolean getBoolean(String flagName) {
+        return mMap.containsKey(flagName) ? mMap.get(flagName) : false;
+    }
+}
diff --git a/src-bind/com/android/commonbind/util/UserAgentGenerator.java b/src-bind/com/android/commonbind/util/UserAgentGenerator.java
new file mode 100644
index 0000000..13bcaca
--- /dev/null
+++ b/src-bind/com/android/commonbind/util/UserAgentGenerator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 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.commonbind.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+/**
+ * Generates a user agent string for the application.
+ */
+public class UserAgentGenerator {
+    /**
+     * Builds a user agent string for the current application.  No default implementation.
+     *
+     * @param context The context.
+     * @return The user agent string.
+     */
+    public static String getUserAgent(Context context) {
+       return null;
+    }
+}
diff --git a/src/com/android/contactsbind/Assistants.java b/src-bind/com/android/contactsbind/Assistants.java
similarity index 100%
rename from src/com/android/contactsbind/Assistants.java
rename to src-bind/com/android/contactsbind/Assistants.java
diff --git a/src/com/android/contactsbind/HelpUtils.java b/src-bind/com/android/contactsbind/HelpUtils.java
similarity index 100%
rename from src/com/android/contactsbind/HelpUtils.java
rename to src-bind/com/android/contactsbind/HelpUtils.java
diff --git a/src/com/android/contacts/activities/ActionBarAdapter.java b/src/com/android/contacts/activities/ActionBarAdapter.java
index eaf027d..e78c355 100644
--- a/src/com/android/contacts/activities/ActionBarAdapter.java
+++ b/src/com/android/contacts/activities/ActionBarAdapter.java
@@ -16,6 +16,7 @@
 
 package com.android.contacts.activities;
 
+import android.animation.ArgbEvaluator;
 import android.animation.ValueAnimator;
 import android.app.Activity;
 import android.content.Context;
@@ -35,6 +36,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
+import android.view.Window;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
@@ -122,6 +124,8 @@
 
     private FeatureHighlight mHamburgerFeatureHighlight;
 
+    private ValueAnimator mStatusBarAnimator;
+
     public interface TabState {
         public static int ALL = 0;
 
@@ -451,8 +455,6 @@
     }
 
     private void update(boolean skipAnimation) {
-        updateStatusBarColor();
-
         updateOverflowButtonColor();
 
         final boolean isSelectionModeChanging
@@ -463,6 +465,8 @@
                 = (mSearchContainer.getParent() == null) == mSearchMode;
         final boolean isTabHeightChanging = isSearchModeChanging || isSelectionModeChanging;
 
+        updateStatusBarColor(isSelectionModeChanging && !isSearchModeChanging);
+
         // When skipAnimation=true, it is possible that we will switch from search mode
         // to selection mode directly. So we need to remove the undesired container in addition
         // to adding the desired container.
@@ -594,18 +598,46 @@
         textView.setText(title);
     }
 
-    private void updateStatusBarColor() {
+    private void updateStatusBarColor(boolean shouldAnimate) {
         if (!CompatUtils.isLollipopCompatible()) {
             return; // we can't change the status bar color prior to Lollipop
         }
+
         if (mSelectionMode) {
-            final int cabStatusBarColor = mActivity.getResources().getColor(
-                    R.color.contextual_selection_bar_status_bar_color);
-            mActivity.getWindow().setStatusBarColor(cabStatusBarColor);
+            final int cabStatusBarColor =ContextCompat.getColor(
+                    mActivity, R.color.contextual_selection_bar_status_bar_color);
+            runStatusBarAnimation(/* colorTo */ cabStatusBarColor);
         } else {
             final int normalStatusBarColor = ContextCompat.getColor(
                     mActivity, R.color.primary_color_dark);
-            mActivity.getWindow().setStatusBarColor(normalStatusBarColor);
+            if (shouldAnimate) {
+                runStatusBarAnimation(/* colorTo */ normalStatusBarColor);
+            } else {
+                mActivity.getWindow().setStatusBarColor(normalStatusBarColor);
+            }
+        }
+    }
+
+    private void runStatusBarAnimation(int colorTo) {
+        final Window window = mActivity.getWindow();
+        if (window.getStatusBarColor() != colorTo) {
+            // Cancel running animation.
+            if (mStatusBarAnimator != null && mStatusBarAnimator.isRunning()) {
+                mStatusBarAnimator.cancel();
+            }
+            final int from = window.getStatusBarColor();
+            // Set up mStatusBarAnimator and run animation.
+            mStatusBarAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), from, colorTo);
+            mStatusBarAnimator.addUpdateListener(
+                    new ValueAnimator.AnimatorUpdateListener() {
+                        @Override
+                        public void onAnimationUpdate(ValueAnimator animator) {
+                            window.setStatusBarColor((Integer) animator.getAnimatedValue());
+                        }
+                    });
+            mStatusBarAnimator.setDuration(mActionBarAnimationDuration);
+            mStatusBarAnimator.setStartDelay(0);
+            mStatusBarAnimator.start();
         }
     }
 
diff --git a/src/com/android/contacts/common/CallUtil.java b/src/com/android/contacts/common/CallUtil.java
new file mode 100644
index 0000000..88fca92
--- /dev/null
+++ b/src/com/android/contacts/common/CallUtil.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2015 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;
+
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.PhoneAccountSdkCompat;
+import com.android.contacts.common.util.PermissionsUtil;
+import com.android.contacts.common.util.PhoneNumberHelper;
+import com.android.phone.common.PhoneConstants;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.text.TextUtils;
+
+import java.util.List;
+
+/**
+ * Utilities related to calls that can be used by non system apps. These
+ * use {@link Intent#ACTION_CALL} instead of ACTION_CALL_PRIVILEGED.
+ *
+ * The privileged version of this util exists inside Dialer.
+ */
+public class CallUtil {
+
+    /**
+     * Indicates that the video calling is not available.
+     */
+    public static final int VIDEO_CALLING_DISABLED = 0;
+
+    /**
+     * Indicates that video calling is enabled, regardless of presence status.
+     */
+    public static final int VIDEO_CALLING_ENABLED = 1;
+
+    /**
+     * Indicates that video calling is enabled, but the availability of video call affordances is
+     * determined by the presence status associated with contacts.
+     */
+    public static final int VIDEO_CALLING_PRESENCE = 2;
+
+    /**
+     * Return an Intent for making a phone call. Scheme (e.g. tel, sip) will be determined
+     * automatically.
+     */
+    public static Intent getCallWithSubjectIntent(String number,
+            PhoneAccountHandle phoneAccountHandle, String callSubject) {
+
+        final Intent intent = getCallIntent(getCallUri(number));
+        intent.putExtra(TelecomManager.EXTRA_CALL_SUBJECT, callSubject);
+        if (phoneAccountHandle != null) {
+            intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+        }
+        return intent;
+    }
+
+    /**
+     * Return an Intent for making a phone call. Scheme (e.g. tel, sip) will be determined
+     * automatically.
+     */
+    public static Intent getCallIntent(String number) {
+        return getCallIntent(getCallUri(number));
+    }
+
+    /**
+     * Return an Intent for making a phone call. A given Uri will be used as is (without any
+     * sanity check).
+     */
+    public static Intent getCallIntent(Uri uri) {
+        return new Intent(Intent.ACTION_CALL, uri);
+    }
+
+    /**
+     * A variant of {@link #getCallIntent} for starting a video call.
+     */
+    public static Intent getVideoCallIntent(String number, String callOrigin) {
+        final Intent intent = new Intent(Intent.ACTION_CALL, getCallUri(number));
+        intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                VideoProfile.STATE_BIDIRECTIONAL);
+        if (!TextUtils.isEmpty(callOrigin)) {
+            intent.putExtra(PhoneConstants.EXTRA_CALL_ORIGIN, callOrigin);
+        }
+        return intent;
+    }
+
+    /**
+     * Return Uri with an appropriate scheme, accepting both SIP and usual phone call
+     * numbers.
+     */
+    public static Uri getCallUri(String number) {
+        if (PhoneNumberHelper.isUriNumber(number)) {
+             return Uri.fromParts(PhoneAccount.SCHEME_SIP, number, null);
+        }
+        return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null);
+    }
+
+    /**
+     * @return Uri that directly dials a user's voicemail inbox.
+     */
+    public static Uri getVoicemailUri() {
+        return Uri.fromParts(PhoneAccount.SCHEME_VOICEMAIL, "", null);
+    }
+
+    /**
+     * Determines if video calling is available, and if so whether presence checking is available
+     * as well.
+     *
+     * Returns a bitmask with {@link #VIDEO_CALLING_ENABLED} to indicate that video calling is
+     * available, and {@link #VIDEO_CALLING_PRESENCE} if presence indication is also available.
+     *
+     * @param context The context
+     * @return A bit-mask describing the current video capabilities.
+     */
+    public static int getVideoCallingAvailability(Context context) {
+        if (!PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_PHONE_STATE)
+                || !CompatUtils.isVideoCompatible()) {
+            return VIDEO_CALLING_DISABLED;
+        }
+        TelecomManager telecommMgr = (TelecomManager)
+                context.getSystemService(Context.TELECOM_SERVICE);
+        if (telecommMgr == null) {
+            return VIDEO_CALLING_DISABLED;
+        }
+
+        List<PhoneAccountHandle> accountHandles = telecommMgr.getCallCapablePhoneAccounts();
+        for (PhoneAccountHandle accountHandle : accountHandles) {
+            PhoneAccount account = telecommMgr.getPhoneAccount(accountHandle);
+            if (account != null) {
+                if (account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING)) {
+                    // Builds prior to N do not have presence support.
+                    if (!CompatUtils.isVideoPresenceCompatible()) {
+                        return VIDEO_CALLING_ENABLED;
+                    }
+
+                    int videoCapabilities = VIDEO_CALLING_ENABLED;
+                    if (account.hasCapabilities(
+                            PhoneAccountSdkCompat.CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE)) {
+                        videoCapabilities |= VIDEO_CALLING_PRESENCE;
+                    }
+                    return videoCapabilities;
+                }
+            }
+        }
+        return VIDEO_CALLING_DISABLED;
+    }
+
+    /**
+     * Determines if one of the call capable phone accounts defined supports video calling.
+     *
+     * @param context The context.
+     * @return {@code true} if one of the call capable phone accounts supports video calling,
+     *      {@code false} otherwise.
+     */
+    public static boolean isVideoEnabled(Context context) {
+        return (getVideoCallingAvailability(context) & VIDEO_CALLING_ENABLED) != 0;
+    }
+
+    /**
+     * Determines if one of the call capable phone accounts defined supports calling with a subject
+     * specified.
+     *
+     * @param context The context.
+     * @return {@code true} if one of the call capable phone accounts supports calling with a
+     *      subject specified, {@code false} otherwise.
+     */
+    public static boolean isCallWithSubjectSupported(Context context) {
+        if (!PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_PHONE_STATE)
+                || !CompatUtils.isCallSubjectCompatible()) {
+            return false;
+        }
+        TelecomManager telecommMgr = (TelecomManager)
+                context.getSystemService(Context.TELECOM_SERVICE);
+        if (telecommMgr == null) {
+            return false;
+        }
+
+        List<PhoneAccountHandle> accountHandles = telecommMgr.getCallCapablePhoneAccounts();
+        for (PhoneAccountHandle accountHandle : accountHandles) {
+            PhoneAccount account = telecommMgr.getPhoneAccount(accountHandle);
+            if (account != null && account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/contacts/common/ClipboardUtils.java b/src/com/android/contacts/common/ClipboardUtils.java
new file mode 100644
index 0000000..27af963
--- /dev/null
+++ b/src/com/android/contacts/common/ClipboardUtils.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+public class ClipboardUtils {
+    private static final String TAG = "ClipboardUtils";
+
+    private ClipboardUtils() { }
+
+    /**
+     * Copy a text to clipboard.
+     *
+     * @param context Context
+     * @param label Label to show to the user describing this clip.
+     * @param text Text to copy.
+     * @param showToast If {@code true}, a toast is shown to the user.
+     */
+    public static void copyText(Context context, CharSequence label, CharSequence text,
+            boolean showToast) {
+        if (TextUtils.isEmpty(text)) return;
+
+        ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(
+                Context.CLIPBOARD_SERVICE);
+        ClipData clipData = ClipData.newPlainText(label == null ? "" : label, text);
+        clipboardManager.setPrimaryClip(clipData);
+
+        if (showToast) {
+            String toastText = context.getString(R.string.toast_text_copied);
+            Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show();
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/Collapser.java b/src/com/android/contacts/common/Collapser.java
new file mode 100644
index 0000000..1ab63c5
--- /dev/null
+++ b/src/com/android/contacts/common/Collapser.java
@@ -0,0 +1,96 @@
+/*
+ * 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;
+
+import android.content.Context;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Class used for collapsing data items into groups of similar items. The data items that should be
+ * collapsible should implement the Collapsible interface. The class also contains a utility
+ * function that takes an ArrayList of items and returns a list of the same items collapsed into
+ * groups.
+ */
+public final class Collapser {
+
+    /*
+     * This utility class cannot be instantiated.
+     */
+    private Collapser() {}
+
+    /*
+     * The Collapser uses an n^2 algorithm so we don't want it to run on
+     * lists beyond a certain size. This specifies the maximum size to collapse.
+     */
+    private static final int MAX_LISTSIZE_TO_COLLAPSE = 20;
+
+    /*
+     * Interface implemented by data types that can be collapsed into groups of similar data. This
+     * can be used for example to collapse similar contact data items into a single item.
+     */
+    public interface Collapsible<T> {
+        public void collapseWith(T t);
+        public boolean shouldCollapseWith(T t, Context context);
+
+    }
+
+    /**
+     * Collapses a list of Collapsible items into a list of collapsed items. Items are collapsed
+     * if {@link Collapsible#shouldCollapseWith(Object)} returns true, and are collapsed
+     * through the {@Link Collapsible#collapseWith(Object)} function implemented by the data item.
+     *
+     * @param list List of Objects of type <T extends Collapsible<T>> to be collapsed.
+     */
+    public static <T extends Collapsible<T>> void collapseList(List<T> list, Context context) {
+
+        int listSize = list.size();
+        // The algorithm below is n^2 so don't run on long lists
+        if (listSize > MAX_LISTSIZE_TO_COLLAPSE) {
+            return;
+        }
+
+        for (int i = 0; i < listSize; i++) {
+            T iItem = list.get(i);
+            if (iItem != null) {
+                for (int j = i + 1; j < listSize; j++) {
+                    T jItem = list.get(j);
+                    if (jItem != null) {
+                        if (iItem.shouldCollapseWith(jItem, context)) {
+                            iItem.collapseWith(jItem);
+                            list.set(j, null);
+                        } else if (jItem.shouldCollapseWith(iItem, context)) {
+                            jItem.collapseWith(iItem);
+                            list.set(i, null);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        // Remove the null items
+        Iterator<T> itr = list.iterator();
+        while (itr.hasNext()) {
+            if (itr.next() == null) {
+                itr.remove();
+            }
+        }
+
+    }
+}
diff --git a/src/com/android/contacts/common/ContactPhotoManager.java b/src/com/android/contacts/common/ContactPhotoManager.java
new file mode 100644
index 0000000..81b6b06
--- /dev/null
+++ b/src/com/android/contacts/common/ContactPhotoManager.java
@@ -0,0 +1,1720 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.app.ActivityManager;
+import android.content.ComponentCallbacks2;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.media.ThumbnailUtils;
+import android.net.TrafficStats;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.contacts.common.util.PermissionsUtil;
+import com.android.contacts.common.util.TrafficStatsTags;
+import com.android.contacts.common.util.UriUtils;
+import com.android.contacts.commonbind.util.UserAgentGenerator;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Asynchronously loads contact photos and maintains a cache of photos.
+ */
+public abstract class ContactPhotoManager implements ComponentCallbacks2 {
+    static final String TAG = "ContactPhotoManager";
+    static final boolean DEBUG = false; // Don't submit with true
+    static final boolean DEBUG_SIZES = false; // Don't submit with true
+
+    /** Contact type constants used for default letter images */
+    public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON;
+    public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS;
+    public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL;
+    public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT;
+
+    /** Scale and offset default constants used for default letter images */
+    public static final float SCALE_DEFAULT = 1.0f;
+    public static final float OFFSET_DEFAULT = 0.0f;
+
+    public static final boolean IS_CIRCULAR_DEFAULT = false;
+
+    /** Uri-related constants used for default letter images */
+    private static final String DISPLAY_NAME_PARAM_KEY = "display_name";
+    private static final String IDENTIFIER_PARAM_KEY = "identifier";
+    private static final String CONTACT_TYPE_PARAM_KEY = "contact_type";
+    private static final String SCALE_PARAM_KEY = "scale";
+    private static final String OFFSET_PARAM_KEY = "offset";
+    private static final String IS_CIRCULAR_PARAM_KEY = "is_circular";
+    private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage";
+    private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://");
+
+    // Static field used to cache the default letter avatar drawable that is created
+    // using a null {@link DefaultImageRequest}
+    private static Drawable sDefaultLetterAvatar = null;
+
+    private static ContactPhotoManager sInstance;
+
+    /**
+     * Given a {@link DefaultImageRequest}, returns a {@link Drawable}, that when drawn, will
+     * draw a letter tile avatar based on the request parameters defined in the
+     * {@link DefaultImageRequest}.
+     */
+    public static Drawable getDefaultAvatarDrawableForContact(Resources resources, boolean hires,
+            DefaultImageRequest defaultImageRequest) {
+        if (defaultImageRequest == null) {
+            if (sDefaultLetterAvatar == null) {
+                // Cache and return the letter tile drawable that is created by a null request,
+                // so that it doesn't have to be recreated every time it is requested again.
+                sDefaultLetterAvatar = LetterTileDefaultImageProvider.getDefaultImageForContact(
+                        resources, null);
+            }
+            return sDefaultLetterAvatar;
+        }
+        return LetterTileDefaultImageProvider.getDefaultImageForContact(resources,
+                defaultImageRequest);
+    }
+
+    /**
+     * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a
+     * letter tile avatar when passed to the {@link ContactPhotoManager}. The internal
+     * implementation of this uri is not guaranteed to remain the same across application
+     * versions, so the actual uri should never be persisted in long-term storage and reused.
+     *
+     * @param request A {@link DefaultImageRequest} object with the fields configured
+     * to return a
+     * @return A Uri that when later passed to the {@link ContactPhotoManager} via
+     * {@link #loadPhoto(ImageView, Uri, int, boolean, DefaultImageRequest)}, can be
+     * used to request a default contact image, drawn as a letter tile using the
+     * parameters as configured in the provided {@link DefaultImageRequest}
+     */
+    public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) {
+        final Builder builder = DEFAULT_IMAGE_URI.buildUpon();
+        if (request != null) {
+            if (!TextUtils.isEmpty(request.displayName)) {
+                builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName);
+            }
+            if (!TextUtils.isEmpty(request.identifier)) {
+                builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier);
+            }
+            if (request.contactType != TYPE_DEFAULT) {
+                builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY,
+                        String.valueOf(request.contactType));
+            }
+            if (request.scale != SCALE_DEFAULT) {
+                builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale));
+            }
+            if (request.offset != OFFSET_DEFAULT) {
+                builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset));
+            }
+            if (request.isCircular != IS_CIRCULAR_DEFAULT) {
+                builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY,
+                        String.valueOf(request.isCircular));
+            }
+
+        }
+        return builder.build();
+    }
+
+    /**
+     * Adds a business contact type encoded fragment to the URL.  Used to ensure photo URLS
+     * from Nearby Places can be identified as business photo URLs rather than URLs for personal
+     * contact photos.
+     *
+     * @param photoUrl The photo URL to modify.
+     * @return URL with the contact type parameter added and set to TYPE_BUSINESS.
+     */
+    public static String appendBusinessContactType(String photoUrl) {
+        Uri uri = Uri.parse(photoUrl);
+        Builder builder = uri.buildUpon();
+        builder.encodedFragment(String.valueOf(TYPE_BUSINESS));
+        return builder.build().toString();
+    }
+
+    /**
+     * Removes the contact type information stored in the photo URI encoded fragment.
+     *
+     * @param photoUri The photo URI to remove the contact type from.
+     * @return The photo URI with contact type removed.
+     */
+    public static Uri removeContactType(Uri photoUri) {
+        String encodedFragment = photoUri.getEncodedFragment();
+        if (!TextUtils.isEmpty(encodedFragment)) {
+            Builder builder = photoUri.buildUpon();
+            builder.encodedFragment(null);
+            return builder.build();
+        }
+        return photoUri;
+    }
+
+    /**
+     * Inspects a photo URI to determine if the photo URI represents a business.
+     *
+     * @param photoUri The URI to inspect.
+     * @return Whether the URI represents a business photo or not.
+     */
+    public static boolean isBusinessContactUri(Uri photoUri) {
+        if (photoUri == null) {
+            return false;
+        }
+
+        String encodedFragment = photoUri.getEncodedFragment();
+        return !TextUtils.isEmpty(encodedFragment)
+                && encodedFragment.equals(String.valueOf(TYPE_BUSINESS));
+    }
+
+    protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) {
+        final DefaultImageRequest request = new DefaultImageRequest(
+                uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY),
+                uri.getQueryParameter(IDENTIFIER_PARAM_KEY), false);
+        try {
+            String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY);
+            if (!TextUtils.isEmpty(contactType)) {
+                request.contactType = Integer.valueOf(contactType);
+            }
+
+            String scale = uri.getQueryParameter(SCALE_PARAM_KEY);
+            if (!TextUtils.isEmpty(scale)) {
+                request.scale = Float.valueOf(scale);
+            }
+
+            String offset = uri.getQueryParameter(OFFSET_PARAM_KEY);
+            if (!TextUtils.isEmpty(offset)) {
+                request.offset = Float.valueOf(offset);
+            }
+
+            String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY);
+            if (!TextUtils.isEmpty(isCircular)) {
+                request.isCircular = Boolean.valueOf(isCircular);
+            }
+        } catch (NumberFormatException e) {
+            Log.w(TAG, "Invalid DefaultImageRequest image parameters provided, ignoring and using "
+                    + "defaults.");
+        }
+
+        return request;
+    }
+
+    protected boolean isDefaultImageUri(Uri uri) {
+        return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme());
+    }
+
+    /**
+     * Contains fields used to contain contact details and other user-defined settings that might
+     * be used by the ContactPhotoManager to generate a default contact image. This contact image
+     * takes the form of a letter or bitmap drawn on top of a colored tile.
+     */
+    public static class DefaultImageRequest {
+        /**
+         * The contact's display name. The display name is used to
+         */
+        public String displayName;
+
+        /**
+         * A unique and deterministic string that can be used to identify this contact. This is
+         * usually the contact's lookup key, but other contact details can be used as well,
+         * especially for non-local or temporary contacts that might not have a lookup key. This
+         * is used to determine the color of the tile.
+         */
+        public String identifier;
+
+        /**
+         * The type of this contact. This contact type may be used to decide the kind of
+         * image to use in the case where a unique letter cannot be generated from the contact's
+         * display name and identifier. See:
+         * {@link #TYPE_PERSON}
+         * {@link #TYPE_BUSINESS}
+         * {@link #TYPE_PERSON}
+         * {@link #TYPE_DEFAULT}
+         */
+        public int contactType = TYPE_DEFAULT;
+
+        /**
+         * The amount to scale the letter or bitmap to, as a ratio of its default size (from a
+         * range of 0.0f to 2.0f). The default value is 1.0f.
+         */
+        public float scale = SCALE_DEFAULT;
+
+        /**
+         * The amount to vertically offset the letter or image to within the tile.
+         * The provided offset must be within the range of -0.5f to 0.5f.
+         * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas
+         * it is being drawn on, which means it will be drawn with the center of the letter starting
+         * at the top edge of the canvas.
+         * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the
+         * canvas it is being drawn on, which means it will be drawn with the center of the letter
+         * starting at the bottom edge of the canvas.
+         * The default is 0.0f, which means the letter is drawn in the exact vertical center of
+         * the tile.
+         */
+        public float offset = OFFSET_DEFAULT;
+
+        /**
+         * Whether or not to draw the default image as a circle, instead of as a square/rectangle.
+         */
+        public boolean isCircular = false;
+
+        /**
+         * Used to indicate that a drawable that represents a contact without any contact details
+         * should be returned.
+         */
+        public static DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest();
+
+        /**
+         * Used to indicate that a drawable that represents a business without a business photo
+         * should be returned.
+         */
+        public static DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST =
+                new DefaultImageRequest(null, null, TYPE_BUSINESS, false);
+
+        /**
+         * Used to indicate that a circular drawable that represents a contact without any contact
+         * details should be returned.
+         */
+        public static DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST =
+                new DefaultImageRequest(null, null, true);
+
+        /**
+         * Used to indicate that a circular drawable that represents a business without a business
+         * photo should be returned.
+         */
+        public static DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST =
+                new DefaultImageRequest(null, null, TYPE_BUSINESS, true);
+
+        public DefaultImageRequest() {
+        }
+
+        public DefaultImageRequest(String displayName, String identifier, boolean isCircular) {
+            this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
+        }
+
+        public DefaultImageRequest(String displayName, String identifier, int contactType,
+                boolean isCircular) {
+            this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
+        }
+
+        public DefaultImageRequest(String displayName, String identifier, int contactType,
+                float scale, float offset, boolean isCircular) {
+            this.displayName = displayName;
+            this.identifier = identifier;
+            this.contactType = contactType;
+            this.scale = scale;
+            this.offset = offset;
+            this.isCircular = isCircular;
+        }
+    }
+
+    public static abstract class DefaultImageProvider {
+        /**
+         * Applies the default avatar to the ImageView. Extent is an indicator for the size (width
+         * or height). If darkTheme is set, the avatar is one that looks better on dark background
+         *
+         * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a
+         * default letter tile avatar should be drawn.
+         */
+        public abstract void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
+                DefaultImageRequest defaultImageRequest);
+    }
+
+    /**
+     * A default image provider that applies a letter tile consisting of a colored background
+     * and a letter in the foreground as the default image for a contact. The color of the
+     * background and the type of letter is decided based on the contact's details.
+     */
+    private static class LetterTileDefaultImageProvider extends DefaultImageProvider {
+        @Override
+        public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
+                DefaultImageRequest defaultImageRequest) {
+            final Drawable drawable = getDefaultImageForContact(view.getResources(),
+                    defaultImageRequest);
+            view.setImageDrawable(drawable);
+        }
+
+        public static Drawable getDefaultImageForContact(Resources resources,
+                DefaultImageRequest defaultImageRequest) {
+            final LetterTileDrawable drawable = new LetterTileDrawable(resources);
+            if (defaultImageRequest != null) {
+                // If the contact identifier is null or empty, fallback to the
+                // displayName. In that case, use {@code null} for the contact's
+                // display name so that a default bitmap will be used instead of a
+                // letter
+                if (TextUtils.isEmpty(defaultImageRequest.identifier)) {
+                    drawable.setLetterAndColorFromContactDetails(null,
+                            defaultImageRequest.displayName);
+                } else {
+                    drawable.setLetterAndColorFromContactDetails(defaultImageRequest.displayName,
+                            defaultImageRequest.identifier);
+                }
+                drawable.setContactType(defaultImageRequest.contactType);
+                drawable.setScale(defaultImageRequest.scale);
+                drawable.setOffset(defaultImageRequest.offset);
+                drawable.setIsCircular(defaultImageRequest.isCircular);
+            }
+            return drawable;
+        }
+    }
+
+    private static class BlankDefaultImageProvider extends DefaultImageProvider {
+        private static Drawable sDrawable;
+
+        @Override
+        public void applyDefaultImage(ImageView view, int extent, boolean darkTheme,
+                DefaultImageRequest defaultImageRequest) {
+            if (sDrawable == null) {
+                Context context = view.getContext();
+                sDrawable = new ColorDrawable(context.getResources().getColor(
+                        R.color.image_placeholder));
+            }
+            view.setImageDrawable(sDrawable);
+        }
+    }
+
+    public static DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider();
+
+    public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider();
+
+    public static ContactPhotoManager getInstance(Context context) {
+        if (sInstance == null) {
+            Context applicationContext = context.getApplicationContext();
+            sInstance = createContactPhotoManager(applicationContext);
+            applicationContext.registerComponentCallbacks(sInstance);
+            if (PermissionsUtil.hasContactsPermissions(context)) {
+                sInstance.preloadPhotosInBackground();
+            }
+        }
+        return sInstance;
+    }
+
+    public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
+        return new ContactPhotoManagerImpl(context);
+    }
+
+    @VisibleForTesting
+    public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) {
+        sInstance = photoManager;
+    }
+
+    /**
+     * Load thumbnail image into the supplied image view. If the photo is already cached,
+     * it is displayed immediately.  Otherwise a request is sent to load the photo
+     * from the database.
+     */
+    public abstract void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
+            boolean isCircular, DefaultImageRequest defaultImageRequest,
+            DefaultImageProvider defaultProvider);
+
+    /**
+     * Calls {@link #loadThumbnail(ImageView, long, boolean, DefaultImageRequest,
+     * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}.
+    */
+    public final void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
+            boolean isCircular, DefaultImageRequest defaultImageRequest) {
+        loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
+    }
+
+
+    /**
+     * Load photo into the supplied image view. If the photo is already cached,
+     * it is displayed immediately. Otherwise a request is sent to load the photo
+     * from the location specified by the URI.
+     *
+     * @param view The target view
+     * @param photoUri The uri of the photo to load
+     * @param requestedExtent Specifies an approximate Max(width, height) of the targetView.
+     * This is useful if the source image can be a lot bigger that the target, so that the decoding
+     * is done using efficient sampling. If requestedExtent is specified, no sampling of the image
+     * is performed
+     * @param darkTheme Whether the background is dark. This is used for default avatars
+     * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+     * letter tile avatar should be drawn.
+     * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't
+     * refer to an existing image)
+     */
+    public abstract void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
+            boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest,
+            DefaultImageProvider defaultProvider);
+
+    /**
+     * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, DefaultImageRequest,
+     * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and
+     * lookup keys.
+     *
+     * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+     * letter tile avatar should be drawn.
+     */
+    public final void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
+            boolean darkTheme, boolean isCircular, DefaultImageRequest defaultImageRequest) {
+        loadPhoto(view, photoUri, requestedExtent, darkTheme, isCircular,
+                defaultImageRequest, DEFAULT_AVATAR);
+    }
+
+    /**
+     * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageRequest,
+     * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that
+     * the image is a thumbnail.
+     *
+     * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+     * letter tile avatar should be drawn.
+     */
+    public final void loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme,
+            boolean isCircular, DefaultImageRequest defaultImageRequest) {
+        loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
+    }
+
+    /**
+     * Remove photo from the supplied image view. This also cancels current pending load request
+     * inside this photo manager.
+     */
+    public abstract void removePhoto(ImageView view);
+
+    /**
+     * Cancels all pending requests to load photos asynchronously.
+     */
+    public abstract void cancelPendingRequests(View fragmentRootView);
+
+    /**
+     * Temporarily stops loading photos from the database.
+     */
+    public abstract void pause();
+
+    /**
+     * Resumes loading photos from the database.
+     */
+    public abstract void resume();
+
+    /**
+     * Marks all cached photos for reloading.  We can continue using cache but should
+     * also make sure the photos haven't changed in the background and notify the views
+     * if so.
+     */
+    public abstract void refreshCache();
+
+    /**
+     * Stores the given bitmap directly in the LRU bitmap cache.
+     * @param photoUri The URI of the photo (for future requests).
+     * @param bitmap The bitmap.
+     * @param photoBytes The bytes that were parsed to create the bitmap.
+     */
+    public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes);
+
+    /**
+     * Initiates a background process that over time will fill up cache with
+     * preload photos.
+     */
+    public abstract void preloadPhotosInBackground();
+
+    // ComponentCallbacks2
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+    }
+
+    // ComponentCallbacks2
+    @Override
+    public void onLowMemory() {
+    }
+
+    // ComponentCallbacks2
+    @Override
+    public void onTrimMemory(int level) {
+    }
+}
+
+class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
+    private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
+
+    private static final int FADE_TRANSITION_DURATION = 200;
+
+    /**
+     * Type of message sent by the UI thread to itself to indicate that some photos
+     * need to be loaded.
+     */
+    private static final int MESSAGE_REQUEST_LOADING = 1;
+
+    /**
+     * Type of message sent by the loader thread to indicate that some photos have
+     * been loaded.
+     */
+    private static final int MESSAGE_PHOTOS_LOADED = 2;
+
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+    private static final String[] COLUMNS = new String[] { Photo._ID, Photo.PHOTO };
+
+    /**
+     * Dummy object used to indicate that a bitmap for a given key could not be stored in the
+     * cache.
+     */
+    private static final BitmapHolder BITMAP_UNAVAILABLE;
+
+    static {
+        BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0);
+        BITMAP_UNAVAILABLE.bitmapRef = new SoftReference<Bitmap>(null);
+    }
+
+    /**
+     * Maintains the state of a particular photo.
+     */
+    private static class BitmapHolder {
+        final byte[] bytes;
+        final int originalSmallerExtent;
+
+        volatile boolean fresh;
+        Bitmap bitmap;
+        Reference<Bitmap> bitmapRef;
+        int decodedSampleSize;
+
+        public BitmapHolder(byte[] bytes, int originalSmallerExtent) {
+            this.bytes = bytes;
+            this.fresh = true;
+            this.originalSmallerExtent = originalSmallerExtent;
+        }
+    }
+
+    private final Context mContext;
+
+    /**
+     * An LRU cache for bitmap holders. The cache contains bytes for photos just
+     * as they come from the database. Each holder has a soft reference to the
+     * actual bitmap.
+     */
+    private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
+
+    /**
+     * {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh.
+     */
+    private volatile boolean mBitmapHolderCacheAllUnfresh = true;
+
+    /**
+     * Cache size threshold at which bitmaps will not be preloaded.
+     */
+    private final int mBitmapHolderCacheRedZoneBytes;
+
+    /**
+     * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
+     * the most recently used bitmaps to save time on decoding
+     * them from bytes (the bytes are stored in {@link #mBitmapHolderCache}.
+     */
+    private final LruCache<Object, Bitmap> mBitmapCache;
+
+    /**
+     * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
+     * The request may swapped out before the photo loading request is started.
+     */
+    private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
+            new ConcurrentHashMap<ImageView, Request>();
+
+    /**
+     * Handler for messages sent to the UI thread.
+     */
+    private final Handler mMainThreadHandler = new Handler(this);
+
+    /**
+     * Thread responsible for loading photos from the database. Created upon
+     * the first request.
+     */
+    private LoaderThread mLoaderThread;
+
+    /**
+     * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
+     */
+    private boolean mLoadingRequested;
+
+    /**
+     * Flag indicating if the image loading is paused.
+     */
+    private boolean mPaused;
+
+    /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
+    private static final int HOLDER_CACHE_SIZE = 2000000;
+
+    /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
+    private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
+
+    /** Height/width of a thumbnail image */
+    private static int mThumbnailSize;
+
+    /** For debug: How many times we had to reload cached photo for a stale entry */
+    private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
+
+    /** For debug: How many times we had to reload cached photo for a fresh entry.  Should be 0. */
+    private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
+
+    /**
+     * The user agent string to use when loading URI based photos.
+     */
+    private String mUserAgent;
+
+    public ContactPhotoManagerImpl(Context context) {
+        mContext = context;
+
+        final ActivityManager am = ((ActivityManager) context.getSystemService(
+                Context.ACTIVITY_SERVICE));
+
+        final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f;
+
+        final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
+        mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
+            @Override protected int sizeOf(Object key, Bitmap value) {
+                return value.getByteCount();
+            }
+
+            @Override protected void entryRemoved(
+                    boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
+                if (DEBUG) dumpStats();
+            }
+        };
+        final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
+        mBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
+            @Override protected int sizeOf(Object key, BitmapHolder value) {
+                return value.bytes != null ? value.bytes.length : 0;
+            }
+
+            @Override protected void entryRemoved(
+                    boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
+                if (DEBUG) dumpStats();
+            }
+        };
+        mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
+        Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
+        if (DEBUG) {
+            Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
+                    + " + " + btk(mBitmapCache.maxSize()));
+        }
+
+        mThumbnailSize = context.getResources().getDimensionPixelSize(
+                R.dimen.contact_browser_list_item_photo_size);
+
+        // Get a user agent string to use for URI photo requests.
+        mUserAgent = UserAgentGenerator.getUserAgent(context);
+        if (mUserAgent == null) {
+            mUserAgent = "";
+        }
+    }
+
+    /** Converts bytes to K bytes, rounding up.  Used only for debug log. */
+    private static String btk(int bytes) {
+        return ((bytes + 1023) / 1024) + "K";
+    }
+
+    private static final int safeDiv(int dividend, int divisor) {
+        return (divisor  == 0) ? 0 : (dividend / divisor);
+    }
+
+    /**
+     * Dump cache stats on logcat.
+     */
+    private void dumpStats() {
+        if (!DEBUG) return;
+        {
+            int numHolders = 0;
+            int rawBytes = 0;
+            int bitmapBytes = 0;
+            int numBitmaps = 0;
+            for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
+                numHolders++;
+                if (h.bytes != null) {
+                    rawBytes += h.bytes.length;
+                }
+                Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
+                if (b != null) {
+                    numBitmaps++;
+                    bitmapBytes += b.getByteCount();
+                }
+            }
+            Log.d(TAG, "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
+                    + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
+                    + numBitmaps + " bitmaps, avg: "
+                    + btk(safeDiv(rawBytes, numHolders))
+                    + "," + btk(safeDiv(bitmapBytes,numBitmaps)));
+            Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString()
+                    + ", overwrite: fresh=" + mFreshCacheOverwrite.get()
+                    + " stale=" + mStaleCacheOverwrite.get());
+        }
+
+        {
+            int numBitmaps = 0;
+            int bitmapBytes = 0;
+            for (Bitmap b : mBitmapCache.snapshot().values()) {
+                numBitmaps++;
+                bitmapBytes += b.getByteCount();
+            }
+            Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps"
+                    + ", avg: " + btk(safeDiv(bitmapBytes, numBitmaps)));
+            // We don't get from L2 cache, so L2 stats is meaningless.
+        }
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
+        if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
+            // Clear the caches.  Note all pending requests will be removed too.
+            clear();
+        }
+    }
+
+    @Override
+    public void preloadPhotosInBackground() {
+        ensureLoaderThread();
+        mLoaderThread.requestPreloading();
+    }
+
+    @Override
+    public void loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular,
+            DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider) {
+        if (photoId == 0) {
+            // No photo is needed
+            defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest);
+            mPendingRequests.remove(view);
+        } else {
+            if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
+            loadPhotoByIdOrUri(view, Request.createFromThumbnailId(photoId, darkTheme, isCircular,
+                    defaultProvider));
+        }
+    }
+
+    @Override
+    public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
+            boolean isCircular, DefaultImageRequest defaultImageRequest,
+            DefaultImageProvider defaultProvider) {
+        if (photoUri == null) {
+            // No photo is needed
+            defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme,
+                    defaultImageRequest);
+            mPendingRequests.remove(view);
+        } else {
+            if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
+            if (isDefaultImageUri(photoUri)) {
+                createAndApplyDefaultImageForUri(view, photoUri, requestedExtent, darkTheme,
+                        isCircular, defaultProvider);
+            } else {
+                loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, requestedExtent,
+                        darkTheme, isCircular, defaultProvider));
+            }
+        }
+    }
+
+    private void createAndApplyDefaultImageForUri(ImageView view, Uri uri, int requestedExtent,
+            boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) {
+        DefaultImageRequest request = getDefaultImageRequestFromUri(uri);
+        request.isCircular = isCircular;
+        defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request);
+    }
+
+    private void loadPhotoByIdOrUri(ImageView view, Request request) {
+        boolean loaded = loadCachedPhoto(view, request, false);
+        if (loaded) {
+            mPendingRequests.remove(view);
+        } else {
+            mPendingRequests.put(view, request);
+            if (!mPaused) {
+                // Send a request to start loading photos
+                requestLoading();
+            }
+        }
+    }
+
+    @Override
+    public void removePhoto(ImageView view) {
+        view.setImageDrawable(null);
+        mPendingRequests.remove(view);
+    }
+
+
+    /**
+     * Cancels pending requests to load photos asynchronously for views inside
+     * {@param fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests.
+     */
+    @Override
+    public void cancelPendingRequests(View fragmentRootView) {
+        if (fragmentRootView == null) {
+            mPendingRequests.clear();
+            return;
+        }
+        final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
+        while (iterator.hasNext()) {
+            final ImageView imageView = iterator.next().getKey();
+            // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then
+            // we can safely remove its request.
+            if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) {
+                iterator.remove();
+            }
+        }
+    }
+
+    private static boolean isChildView(View parent, View potentialChild) {
+        return potentialChild.getParent() != null && (potentialChild.getParent() == parent || (
+                potentialChild.getParent() instanceof ViewGroup && isChildView(parent,
+                        (ViewGroup) potentialChild.getParent())));
+    }
+
+    @Override
+    public void refreshCache() {
+        if (mBitmapHolderCacheAllUnfresh) {
+            if (DEBUG) Log.d(TAG, "refreshCache -- no fresh entries.");
+            return;
+        }
+        if (DEBUG) Log.d(TAG, "refreshCache");
+        mBitmapHolderCacheAllUnfresh = true;
+        for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
+            if (holder != BITMAP_UNAVAILABLE) {
+                holder.fresh = false;
+            }
+        }
+    }
+
+    /**
+     * Checks if the photo is present in cache.  If so, sets the photo on the view.
+     *
+     * @return false if the photo needs to be (re)loaded from the provider.
+     */
+    private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
+        BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
+        if (holder == null) {
+            // The bitmap has not been loaded ==> show default avatar
+            request.applyDefaultImage(view, request.mIsCircular);
+            return false;
+        }
+
+        if (holder.bytes == null) {
+            request.applyDefaultImage(view, request.mIsCircular);
+            return holder.fresh;
+        }
+
+        Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get();
+        if (cachedBitmap == null) {
+            if (holder.bytes.length < 8 * 1024) {
+                // Small thumbnails are usually quick to inflate. Let's do that on the UI thread
+                inflateBitmap(holder, request.getRequestedExtent());
+                cachedBitmap = holder.bitmap;
+                if (cachedBitmap == null) return false;
+            } else {
+                // This is bigger data. Let's send that back to the Loader so that we can
+                // inflate this in the background
+                request.applyDefaultImage(view, request.mIsCircular);
+                return false;
+            }
+        }
+
+        final Drawable previousDrawable = view.getDrawable();
+        if (fadeIn && previousDrawable != null) {
+            final Drawable[] layers = new Drawable[2];
+            // Prevent cascade of TransitionDrawables.
+            if (previousDrawable instanceof TransitionDrawable) {
+                final TransitionDrawable previousTransitionDrawable =
+                        (TransitionDrawable) previousDrawable;
+                layers[0] = previousTransitionDrawable.getDrawable(
+                        previousTransitionDrawable.getNumberOfLayers() - 1);
+            } else {
+                layers[0] = previousDrawable;
+            }
+            layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request);
+            TransitionDrawable drawable = new TransitionDrawable(layers);
+            view.setImageDrawable(drawable);
+            drawable.startTransition(FADE_TRANSITION_DURATION);
+        } else {
+            view.setImageDrawable(
+                    getDrawableForBitmap(mContext.getResources(), cachedBitmap, request));
+        }
+
+        // Put the bitmap in the LRU cache. But only do this for images that are small enough
+        // (we require that at least six of those can be cached at the same time)
+        if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
+            mBitmapCache.put(request.getKey(), cachedBitmap);
+        }
+
+        // Soften the reference
+        holder.bitmap = null;
+
+        return holder.fresh;
+    }
+
+    /**
+     * Given a bitmap, returns a drawable that is configured to display the bitmap based on the
+     * specified request.
+     */
+    private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) {
+        if (request.mIsCircular) {
+            final RoundedBitmapDrawable drawable =
+                    RoundedBitmapDrawableFactory.create(resources, bitmap);
+            drawable.setAntiAlias(true);
+            drawable.setCornerRadius(bitmap.getHeight() / 2);
+            return drawable;
+        } else {
+            return new BitmapDrawable(resources, bitmap);
+        }
+    }
+
+    /**
+     * If necessary, decodes bytes stored in the holder to Bitmap.  As long as the
+     * bitmap is held either by {@link #mBitmapCache} or by a soft reference in
+     * the holder, it will not be necessary to decode the bitmap.
+     */
+    private static void inflateBitmap(BitmapHolder holder, int requestedExtent) {
+        final int sampleSize =
+                BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent);
+        byte[] bytes = holder.bytes;
+        if (bytes == null || bytes.length == 0) {
+            return;
+        }
+
+        if (sampleSize == holder.decodedSampleSize) {
+            // Check the soft reference.  If will be retained if the bitmap is also
+            // in the LRU cache, so we don't need to check the LRU cache explicitly.
+            if (holder.bitmapRef != null) {
+                holder.bitmap = holder.bitmapRef.get();
+                if (holder.bitmap != null) {
+                    return;
+                }
+            }
+        }
+
+        try {
+            Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize);
+
+            // TODO: As a temporary workaround while framework support is being added to
+            // clip non-square bitmaps into a perfect circle, manually crop the bitmap into
+            // into a square if it will be displayed as a thumbnail so that it can be cropped
+            // into a circle.
+            final int height = bitmap.getHeight();
+            final int width = bitmap.getWidth();
+
+            // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just
+            // below twice the length of a thumbnail image due to the way we calculate the optimal
+            // sample size.
+            if (height != width && Math.min(height, width) <= mThumbnailSize * 2) {
+                final int dimension = Math.min(height, width);
+                bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension);
+            }
+            // make bitmap mutable and draw size onto it
+            if (DEBUG_SIZES) {
+                Bitmap original = bitmap;
+                bitmap = bitmap.copy(bitmap.getConfig(), true);
+                original.recycle();
+                Canvas canvas = new Canvas(bitmap);
+                Paint paint = new Paint();
+                paint.setTextSize(16);
+                paint.setColor(Color.BLUE);
+                paint.setStyle(Style.FILL);
+                canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint);
+                paint.setColor(Color.WHITE);
+                paint.setAntiAlias(true);
+                canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint);
+            }
+
+            holder.decodedSampleSize = sampleSize;
+            holder.bitmap = bitmap;
+            holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
+            if (DEBUG) {
+                Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
+                        + bitmap.getWidth() + "x" + bitmap.getHeight()
+                        + ", " + btk(bitmap.getByteCount()));
+            }
+        } catch (OutOfMemoryError e) {
+            // Do nothing - the photo will appear to be missing
+        }
+    }
+
+    public void clear() {
+        if (DEBUG) Log.d(TAG, "clear");
+        mPendingRequests.clear();
+        mBitmapHolderCache.evictAll();
+        mBitmapCache.evictAll();
+    }
+
+    @Override
+    public void pause() {
+        mPaused = true;
+    }
+
+    @Override
+    public void resume() {
+        mPaused = false;
+        if (DEBUG) dumpStats();
+        if (!mPendingRequests.isEmpty()) {
+            requestLoading();
+        }
+    }
+
+    /**
+     * Sends a message to this thread itself to start loading images.  If the current
+     * view contains multiple image views, all of those image views will get a chance
+     * to request their respective photos before any of those requests are executed.
+     * This allows us to load images in bulk.
+     */
+    private void requestLoading() {
+        if (!mLoadingRequested) {
+            mLoadingRequested = true;
+            mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
+        }
+    }
+
+    /**
+     * Processes requests on the main thread.
+     */
+    @Override
+    public boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MESSAGE_REQUEST_LOADING: {
+                mLoadingRequested = false;
+                if (!mPaused) {
+                    ensureLoaderThread();
+                    mLoaderThread.requestLoading();
+                }
+                return true;
+            }
+
+            case MESSAGE_PHOTOS_LOADED: {
+                if (!mPaused) {
+                    processLoadedImages();
+                }
+                if (DEBUG) dumpStats();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void ensureLoaderThread() {
+        if (mLoaderThread == null) {
+            mLoaderThread = new LoaderThread(mContext.getContentResolver());
+            mLoaderThread.start();
+        }
+    }
+
+    /**
+     * Goes over pending loading requests and displays loaded photos.  If some of the
+     * photos still haven't been loaded, sends another request for image loading.
+     */
+    private void processLoadedImages() {
+        final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
+        while (iterator.hasNext()) {
+            final Entry<ImageView, Request> entry = iterator.next();
+            // TODO: Temporarily disable contact photo fading in, until issues with
+            // RoundedBitmapDrawables overlapping the default image drawables are resolved.
+            final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false);
+            if (loaded) {
+                iterator.remove();
+            }
+        }
+
+        softenCache();
+
+        if (!mPendingRequests.isEmpty()) {
+            requestLoading();
+        }
+    }
+
+    /**
+     * Removes strong references to loaded bitmaps to allow them to be garbage collected
+     * if needed.  Some of the bitmaps will still be retained by {@link #mBitmapCache}.
+     */
+    private void softenCache() {
+        for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
+            holder.bitmap = null;
+        }
+    }
+
+    /**
+     * Stores the supplied bitmap in cache.
+     */
+    private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
+        if (DEBUG) {
+            BitmapHolder prev = mBitmapHolderCache.get(key);
+            if (prev != null && prev.bytes != null) {
+                Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
+                if (prev.fresh) {
+                    mFreshCacheOverwrite.incrementAndGet();
+                } else {
+                    mStaleCacheOverwrite.incrementAndGet();
+                }
+            }
+            Log.d(TAG, "Caching data: key=" + key + ", " +
+                    (bytes == null ? "<null>" : btk(bytes.length)));
+        }
+        BitmapHolder holder = new BitmapHolder(bytes,
+                bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
+
+        // Unless this image is being preloaded, decode it right away while
+        // we are still on the background thread.
+        if (!preloading) {
+            inflateBitmap(holder, requestedExtent);
+        }
+
+        if (bytes != null) {
+            mBitmapHolderCache.put(key, holder);
+            if (mBitmapHolderCache.get(key) != holder) {
+                Log.w(TAG, "Bitmap too big to fit in cache.");
+                mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
+            }
+        } else {
+            mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
+        }
+
+        mBitmapHolderCacheAllUnfresh = false;
+    }
+
+    @Override
+    public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
+        final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight());
+        // We can pretend here that the extent of the photo was the size that we originally
+        // requested
+        Request request = Request.createFromUri(photoUri, smallerExtent, false /* darkTheme */,
+                false /* isCircular */ , DEFAULT_AVATAR);
+        BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent);
+        holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
+        mBitmapHolderCache.put(request.getKey(), holder);
+        mBitmapHolderCacheAllUnfresh = false;
+        mBitmapCache.put(request.getKey(), bitmap);
+    }
+
+    /**
+     * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
+     * already loaded
+     */
+    private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
+            Set<String> photoIdsAsStrings, Set<Request> uris) {
+        photoIds.clear();
+        photoIdsAsStrings.clear();
+        uris.clear();
+
+        boolean jpegsDecoded = false;
+
+        /*
+         * Since the call is made from the loader thread, the map could be
+         * changing during the iteration. That's not really a problem:
+         * ConcurrentHashMap will allow those changes to happen without throwing
+         * exceptions. Since we may miss some requests in the situation of
+         * concurrent change, we will need to check the map again once loading
+         * is complete.
+         */
+        Iterator<Request> iterator = mPendingRequests.values().iterator();
+        while (iterator.hasNext()) {
+            Request request = iterator.next();
+            final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
+            if (holder == BITMAP_UNAVAILABLE) {
+                continue;
+            }
+            if (holder != null && holder.bytes != null && holder.fresh &&
+                    (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
+                // This was previously loaded but we don't currently have the inflated Bitmap
+                inflateBitmap(holder, request.getRequestedExtent());
+                jpegsDecoded = true;
+            } else {
+                if (holder == null || !holder.fresh) {
+                    if (request.isUriRequest()) {
+                        uris.add(request);
+                    } else {
+                        photoIds.add(request.getId());
+                        photoIdsAsStrings.add(String.valueOf(request.mId));
+                    }
+                }
+            }
+        }
+
+        if (jpegsDecoded) mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
+    }
+
+    /**
+     * The thread that performs loading of photos from the database.
+     */
+    private class LoaderThread extends HandlerThread implements Callback {
+        private static final int BUFFER_SIZE = 1024*16;
+        private static final int MESSAGE_PRELOAD_PHOTOS = 0;
+        private static final int MESSAGE_LOAD_PHOTOS = 1;
+
+        /**
+         * A pause between preload batches that yields to the UI thread.
+         */
+        private static final int PHOTO_PRELOAD_DELAY = 1000;
+
+        /**
+         * Number of photos to preload per batch.
+         */
+        private static final int PRELOAD_BATCH = 25;
+
+        /**
+         * Maximum number of photos to preload.  If the cache size is 2Mb and
+         * the expected average size of a photo is 4kb, then this number should be 2Mb/4kb = 500.
+         */
+        private static final int MAX_PHOTOS_TO_PRELOAD = 100;
+
+        private final ContentResolver mResolver;
+        private final StringBuilder mStringBuilder = new StringBuilder();
+        private final Set<Long> mPhotoIds = Sets.newHashSet();
+        private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
+        private final Set<Request> mPhotoUris = Sets.newHashSet();
+        private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
+
+        private Handler mLoaderThreadHandler;
+        private byte mBuffer[];
+
+        private static final int PRELOAD_STATUS_NOT_STARTED = 0;
+        private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
+        private static final int PRELOAD_STATUS_DONE = 2;
+
+        private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
+
+        public LoaderThread(ContentResolver resolver) {
+            super(LOADER_THREAD_NAME);
+            mResolver = resolver;
+        }
+
+        public void ensureHandler() {
+            if (mLoaderThreadHandler == null) {
+                mLoaderThreadHandler = new Handler(getLooper(), this);
+            }
+        }
+
+        /**
+         * Kicks off preloading of the next batch of photos on the background thread.
+         * Preloading will happen after a delay: we want to yield to the UI thread
+         * as much as possible.
+         * <p>
+         * If preloading is already complete, does nothing.
+         */
+        public void requestPreloading() {
+            if (mPreloadStatus == PRELOAD_STATUS_DONE) {
+                return;
+            }
+
+            ensureHandler();
+            if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
+                return;
+            }
+
+            mLoaderThreadHandler.sendEmptyMessageDelayed(
+                    MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
+        }
+
+        /**
+         * Sends a message to this thread to load requested photos.  Cancels a preloading
+         * request, if any: we don't want preloading to impede loading of the photos
+         * we need to display now.
+         */
+        public void requestLoading() {
+            ensureHandler();
+            mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
+            mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
+        }
+
+        /**
+         * Receives the above message, loads photos and then sends a message
+         * to the main thread to process them.
+         */
+        @Override
+        public boolean handleMessage(Message msg) {
+            switch (msg.what) {
+                case MESSAGE_PRELOAD_PHOTOS:
+                    preloadPhotosInBackground();
+                    break;
+                case MESSAGE_LOAD_PHOTOS:
+                    loadPhotosInBackground();
+                    break;
+            }
+            return true;
+        }
+
+        /**
+         * The first time it is called, figures out which photos need to be preloaded.
+         * Each subsequent call preloads the next batch of photos and requests
+         * another cycle of preloading after a delay.  The whole process ends when
+         * we either run out of photos to preload or fill up cache.
+         */
+        private void preloadPhotosInBackground() {
+            if (mPreloadStatus == PRELOAD_STATUS_DONE) {
+                return;
+            }
+
+            if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
+                queryPhotosForPreload();
+                if (mPreloadPhotoIds.isEmpty()) {
+                    mPreloadStatus = PRELOAD_STATUS_DONE;
+                } else {
+                    mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
+                }
+                requestPreloading();
+                return;
+            }
+
+            if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
+                mPreloadStatus = PRELOAD_STATUS_DONE;
+                return;
+            }
+
+            mPhotoIds.clear();
+            mPhotoIdsAsStrings.clear();
+
+            int count = 0;
+            int preloadSize = mPreloadPhotoIds.size();
+            while(preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
+                preloadSize--;
+                count++;
+                Long photoId = mPreloadPhotoIds.get(preloadSize);
+                mPhotoIds.add(photoId);
+                mPhotoIdsAsStrings.add(photoId.toString());
+                mPreloadPhotoIds.remove(preloadSize);
+            }
+
+            loadThumbnails(true);
+
+            if (preloadSize == 0) {
+                mPreloadStatus = PRELOAD_STATUS_DONE;
+            }
+
+            Log.v(TAG, "Preloaded " + count + " photos.  Cached bytes: "
+                    + mBitmapHolderCache.size());
+
+            requestPreloading();
+        }
+
+        private void queryPhotosForPreload() {
+            Cursor cursor = null;
+            try {
+                Uri uri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
+                        ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
+                        .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
+                                String.valueOf(MAX_PHOTOS_TO_PRELOAD))
+                        .build();
+                cursor = mResolver.query(uri, new String[] { Contacts.PHOTO_ID },
+                        Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
+                        null,
+                        Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
+
+                if (cursor != null) {
+                    while (cursor.moveToNext()) {
+                        // Insert them in reverse order, because we will be taking
+                        // them from the end of the list for loading.
+                        mPreloadPhotoIds.add(0, cursor.getLong(0));
+                    }
+                }
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+        }
+
+        private void loadPhotosInBackground() {
+            if (!PermissionsUtil.hasPermission(mContext,
+                    android.Manifest.permission.READ_CONTACTS)) {
+                return;
+            }
+            obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
+            loadThumbnails(false);
+            loadUriBasedPhotos();
+            requestPreloading();
+        }
+
+        /** Loads thumbnail photos with ids */
+        private void loadThumbnails(boolean preloading) {
+            if (mPhotoIds.isEmpty()) {
+                return;
+            }
+
+            // Remove loaded photos from the preload queue: we don't want
+            // the preloading process to load them again.
+            if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
+                for (Long id : mPhotoIds) {
+                    mPreloadPhotoIds.remove(id);
+                }
+                if (mPreloadPhotoIds.isEmpty()) {
+                    mPreloadStatus = PRELOAD_STATUS_DONE;
+                }
+            }
+
+            mStringBuilder.setLength(0);
+            mStringBuilder.append(Photo._ID + " IN(");
+            for (int i = 0; i < mPhotoIds.size(); i++) {
+                if (i != 0) {
+                    mStringBuilder.append(',');
+                }
+                mStringBuilder.append('?');
+            }
+            mStringBuilder.append(')');
+
+            Cursor cursor = null;
+            try {
+                if (DEBUG) Log.d(TAG, "Loading " + TextUtils.join(",", mPhotoIdsAsStrings));
+                cursor = mResolver.query(Data.CONTENT_URI,
+                        COLUMNS,
+                        mStringBuilder.toString(),
+                        mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
+                        null);
+
+                if (cursor != null) {
+                    while (cursor.moveToNext()) {
+                        Long id = cursor.getLong(0);
+                        byte[] bytes = cursor.getBlob(1);
+                        cacheBitmap(id, bytes, preloading, -1);
+                        mPhotoIds.remove(id);
+                    }
+                }
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+
+            // Remaining photos were not found in the contacts database (but might be in profile).
+            for (Long id : mPhotoIds) {
+                if (ContactsContract.isProfileId(id)) {
+                    Cursor profileCursor = null;
+                    try {
+                        profileCursor = mResolver.query(
+                                ContentUris.withAppendedId(Data.CONTENT_URI, id),
+                                COLUMNS, null, null, null);
+                        if (profileCursor != null && profileCursor.moveToFirst()) {
+                            cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
+                                    preloading, -1);
+                        } else {
+                            // Couldn't load a photo this way either.
+                            cacheBitmap(id, null, preloading, -1);
+                        }
+                    } finally {
+                        if (profileCursor != null) {
+                            profileCursor.close();
+                        }
+                    }
+                } else {
+                    // Not a profile photo and not found - mark the cache accordingly
+                    cacheBitmap(id, null, preloading, -1);
+                }
+            }
+
+            mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
+        }
+
+        /**
+         * Loads photos referenced with Uris. Those can be remote thumbnails
+         * (from directory searches), display photos etc
+         */
+        private void loadUriBasedPhotos() {
+            for (Request uriRequest : mPhotoUris) {
+                // Keep the original URI and use this to key into the cache.  Failure to do so will
+                // result in an image being continually reloaded into cache if the original URI
+                // has a contact type encodedFragment (eg nearby places business photo URLs).
+                Uri originalUri = uriRequest.getUri();
+
+                // Strip off the "contact type" we added to the URI to ensure it was identifiable as
+                // a business photo -- there is no need to pass this on to the server.
+                Uri uri = ContactPhotoManager.removeContactType(originalUri);
+
+                if (mBuffer == null) {
+                    mBuffer = new byte[BUFFER_SIZE];
+                }
+                try {
+                    if (DEBUG) Log.d(TAG, "Loading " + uri);
+                    final String scheme = uri.getScheme();
+                    InputStream is = null;
+                    if (scheme.equals("http") || scheme.equals("https")) {
+                        TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG);
+                        final HttpURLConnection connection =
+                                (HttpURLConnection) new URL(uri.toString()).openConnection();
+
+                        // Include the user agent if it is specified.
+                        if (!TextUtils.isEmpty(mUserAgent)) {
+                            connection.setRequestProperty("User-Agent", mUserAgent);
+                        }
+                        try {
+                            is = connection.getInputStream();
+                        } catch (IOException e) {
+                            connection.disconnect();
+                            is = null;
+                        }
+                        TrafficStats.clearThreadStatsTag();
+                    } else {
+                        is = mResolver.openInputStream(uri);
+                    }
+                    if (is != null) {
+                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                        try {
+                            int size;
+                            while ((size = is.read(mBuffer)) != -1) {
+                                baos.write(mBuffer, 0, size);
+                            }
+                        } finally {
+                            is.close();
+                        }
+                        cacheBitmap(originalUri, baos.toByteArray(), false,
+                                uriRequest.getRequestedExtent());
+                        mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
+                    } else {
+                        Log.v(TAG, "Cannot load photo " + uri);
+                        cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
+                    }
+                } catch (final Exception | OutOfMemoryError ex) {
+                    Log.v(TAG, "Cannot load photo " + uri, ex);
+                    cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
+                }
+            }
+        }
+    }
+
+    /**
+     * A holder for either a Uri or an id and a flag whether this was requested for the dark or
+     * light theme
+     */
+    private static final class Request {
+        private final long mId;
+        private final Uri mUri;
+        private final boolean mDarkTheme;
+        private final int mRequestedExtent;
+        private final DefaultImageProvider mDefaultProvider;
+        /**
+         * Whether or not the contact photo is to be displayed as a circle
+         */
+        private final boolean mIsCircular;
+
+        private Request(long id, Uri uri, int requestedExtent, boolean darkTheme,
+                boolean isCircular, DefaultImageProvider defaultProvider) {
+            mId = id;
+            mUri = uri;
+            mDarkTheme = darkTheme;
+            mIsCircular = isCircular;
+            mRequestedExtent = requestedExtent;
+            mDefaultProvider = defaultProvider;
+        }
+
+        public static Request createFromThumbnailId(long id, boolean darkTheme, boolean isCircular,
+                DefaultImageProvider defaultProvider) {
+            return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider);
+        }
+
+        public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
+                boolean isCircular, DefaultImageProvider defaultProvider) {
+            return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, isCircular,
+                    defaultProvider);
+        }
+
+        public boolean isUriRequest() {
+            return mUri != null;
+        }
+
+        public Uri getUri() {
+            return mUri;
+        }
+
+        public long getId() {
+            return mId;
+        }
+
+        public int getRequestedExtent() {
+            return mRequestedExtent;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + (int) (mId ^ (mId >>> 32));
+            result = prime * result + mRequestedExtent;
+            result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+            final Request that = (Request) obj;
+            if (mId != that.mId) return false;
+            if (mRequestedExtent != that.mRequestedExtent) return false;
+            if (!UriUtils.areEqual(mUri, that.mUri)) return false;
+            // Don't compare equality of mDarkTheme because it is only used in the default contact
+            // photo case. When the contact does have a photo, the contact photo is the same
+            // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
+            // twice.
+            return true;
+        }
+
+        public Object getKey() {
+            return mUri == null ? mId : mUri;
+        }
+
+        /**
+         * Applies the default image to the current view. If the request is URI-based, looks for
+         * the contact type encoded fragment to determine if this is a request for a business photo,
+         * in which case we will load the default business photo.
+         *
+         * @param view The current image view to apply the image to.
+         * @param isCircular Whether the image is circular or not.
+         */
+        public void applyDefaultImage(ImageView view, boolean isCircular) {
+            final DefaultImageRequest request;
+
+            if (isCircular) {
+                request = ContactPhotoManager.isBusinessContactUri(mUri)
+                        ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST
+                        : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST;
+            } else {
+                request = ContactPhotoManager.isBusinessContactUri(mUri)
+                        ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST
+                        : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST;
+            }
+            mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request);
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/ContactPresenceIconUtil.java b/src/com/android/contacts/common/ContactPresenceIconUtil.java
new file mode 100644
index 0000000..2f4c9ee
--- /dev/null
+++ b/src/com/android/contacts/common/ContactPresenceIconUtil.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.StatusUpdates;
+
+/**
+ * Define the contact present show policy in Contacts
+ */
+public class ContactPresenceIconUtil {
+    /**
+     * Get the presence icon resource according the status.
+     *
+     * @return null means don't show the status icon.
+     */
+    public static Drawable getPresenceIcon (Context context, int status) {
+        // We don't show the offline status in Contacts
+        switch(status) {
+            case StatusUpdates.AVAILABLE:
+            case StatusUpdates.IDLE:
+            case StatusUpdates.AWAY:
+            case StatusUpdates.DO_NOT_DISTURB:
+            case StatusUpdates.INVISIBLE:
+                return context.getResources().getDrawable(
+                        StatusUpdates.getPresenceIconResourceId(status));
+            case StatusUpdates.OFFLINE:
+            // The undefined status is treated as OFFLINE in getPresenceIconResourceId();
+            default:
+                return null;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/ContactStatusUtil.java b/src/com/android/contacts/common/ContactStatusUtil.java
new file mode 100644
index 0000000..a7d1925
--- /dev/null
+++ b/src/com/android/contacts/common/ContactStatusUtil.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;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.ContactsContract.StatusUpdates;
+
+/**
+ * Provides static function to get default contact status message.
+ */
+public class ContactStatusUtil {
+
+    private static final String TAG = "ContactStatusUtil";
+
+    public static String getStatusString(Context context, int presence) {
+        Resources resources = context.getResources();
+        switch (presence) {
+            case StatusUpdates.AVAILABLE:
+                return resources.getString(R.string.status_available);
+            case StatusUpdates.IDLE:
+            case StatusUpdates.AWAY:
+                return resources.getString(R.string.status_away);
+            case StatusUpdates.DO_NOT_DISTURB:
+                return resources.getString(R.string.status_busy);
+            case StatusUpdates.OFFLINE:
+            case StatusUpdates.INVISIBLE:
+            default:
+                return null;
+        }
+    }
+
+}
diff --git a/src/com/android/contacts/common/ContactTileLoaderFactory.java b/src/com/android/contacts/common/ContactTileLoaderFactory.java
new file mode 100644
index 0000000..f75950e
--- /dev/null
+++ b/src/com/android/contacts/common/ContactTileLoaderFactory.java
@@ -0,0 +1,108 @@
+/*
+ * 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;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+
+/**
+ * Used to create {@link CursorLoader}s to load different groups of
+ * {@link com.android.contacts.list.ContactTileView}.
+ */
+public final class ContactTileLoaderFactory {
+
+    public final static int CONTACT_ID = 0;
+    public final static int DISPLAY_NAME = 1;
+    public final static int STARRED = 2;
+    public final static int PHOTO_URI = 3;
+    public final static int LOOKUP_KEY = 4;
+    public final static int CONTACT_PRESENCE = 5;
+    public final static int CONTACT_STATUS = 6;
+
+    // Only used for StrequentPhoneOnlyLoader
+    public final static int PHONE_NUMBER = 5;
+    public final static int PHONE_NUMBER_TYPE = 6;
+    public final static int PHONE_NUMBER_LABEL = 7;
+    public final static int IS_DEFAULT_NUMBER = 8;
+    public final static int PINNED = 9;
+    // The _ID field returned for strequent items actually contains data._id instead of
+    // contacts._id because the query is performed on the data table. In order to obtain the
+    // contact id for strequent items, we thus have to use Phone.contact_id instead.
+    public final static int CONTACT_ID_FOR_DATA = 10;
+    public final static int DISPLAY_NAME_ALTERNATIVE = 11;
+
+    private static final String[] COLUMNS = new String[] {
+        Contacts._ID, // ..........................................0
+        Contacts.DISPLAY_NAME, // .................................1
+        Contacts.STARRED, // ......................................2
+        Contacts.PHOTO_URI, // ....................................3
+        Contacts.LOOKUP_KEY, // ...................................4
+        Contacts.CONTACT_PRESENCE, // .............................5
+        Contacts.CONTACT_STATUS, // ...............................6
+    };
+
+    /**
+     * Projection used for the {@link Contacts#CONTENT_STREQUENT_URI}
+     * query when {@link ContactsContract#STREQUENT_PHONE_ONLY} flag
+     * is set to true. The main difference is the lack of presence
+     * and status data and the addition of phone number and label.
+     */
+    @VisibleForTesting
+    public static final String[] COLUMNS_PHONE_ONLY = new String[] {
+        Contacts._ID, // ..........................................0
+        Contacts.DISPLAY_NAME_PRIMARY, // .........................1
+        Contacts.STARRED, // ......................................2
+        Contacts.PHOTO_URI, // ....................................3
+        Contacts.LOOKUP_KEY, // ...................................4
+        Phone.NUMBER, // ..........................................5
+        Phone.TYPE, // ............................................6
+        Phone.LABEL, // ...........................................7
+        Phone.IS_SUPER_PRIMARY, //.................................8
+        Contacts.PINNED, // .......................................9
+        Phone.CONTACT_ID, //.......................................10
+        Contacts.DISPLAY_NAME_ALTERNATIVE, // .....................11
+    };
+
+    private static final String STARRED_ORDER = Contacts.DISPLAY_NAME+" COLLATE NOCASE ASC";
+
+    public static CursorLoader createStrequentLoader(Context context) {
+        return new CursorLoader(context, Contacts.CONTENT_STREQUENT_URI, COLUMNS, null, null,
+                STARRED_ORDER);
+    }
+
+    public static CursorLoader createStrequentPhoneOnlyLoader(Context context) {
+        Uri uri = Contacts.CONTENT_STREQUENT_URI.buildUpon()
+                .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true").build();
+
+        return new CursorLoader(context, uri, COLUMNS_PHONE_ONLY, null, null, null);
+    }
+
+    public static CursorLoader createStarredLoader(Context context) {
+        return new CursorLoader(context, Contacts.CONTENT_URI, COLUMNS, Contacts.STARRED + "=?",
+                new String[]{"1"},  STARRED_ORDER);
+    }
+
+    public static CursorLoader createFrequentLoader(Context context) {
+        return new CursorLoader(context, Contacts.CONTENT_FREQUENT_URI, COLUMNS,
+                 Contacts.STARRED + "=?", new String[]{"0"}, null);
+    }
+}
diff --git a/src/com/android/contacts/common/ContactsUtils.java b/src/com/android/contacts/common/ContactsUtils.java
new file mode 100644
index 0000000..38fdbf2
--- /dev/null
+++ b/src/com/android/contacts/common/ContactsUtils.java
@@ -0,0 +1,280 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.support.annotation.IntDef;
+import android.provider.ContactsContract.DisplayPhoto;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.ImDataItem;
+import com.android.contacts.common.testing.NeededForTesting;
+import com.android.contacts.common.compat.ContactsCompat;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.model.AccountTypeManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+public class ContactsUtils {
+    private static final String TAG = "ContactsUtils";
+
+    // Telecomm related schemes are in CallUtil
+    public static final String SCHEME_IMTO = "imto";
+    public static final String SCHEME_MAILTO = "mailto";
+    public static final String SCHEME_SMSTO = "smsto";
+
+    private static final int DEFAULT_THUMBNAIL_SIZE = 96;
+
+    private static int sThumbnailSize = -1;
+
+    public static final boolean FLAG_N_FEATURE = Build.VERSION.SDK_INT >= 24;
+
+    // TODO find a proper place for the canonical version of these
+    public interface ProviderNames {
+        String YAHOO = "Yahoo";
+        String GTALK = "GTalk";
+        String MSN = "MSN";
+        String ICQ = "ICQ";
+        String AIM = "AIM";
+        String XMPP = "XMPP";
+        String JABBER = "JABBER";
+        String SKYPE = "SKYPE";
+        String QQ = "QQ";
+    }
+
+    /**
+     * This looks up the provider name defined in
+     * ProviderNames from the predefined IM protocol id.
+     * This is used for interacting with the IM application.
+     *
+     * @param protocol the protocol ID
+     * @return the provider name the IM app uses for the given protocol, or null if no
+     * provider is defined for the given protocol
+     * @hide
+     */
+    public static String lookupProviderNameFromId(int protocol) {
+        switch (protocol) {
+            case Im.PROTOCOL_GOOGLE_TALK:
+                return ProviderNames.GTALK;
+            case Im.PROTOCOL_AIM:
+                return ProviderNames.AIM;
+            case Im.PROTOCOL_MSN:
+                return ProviderNames.MSN;
+            case Im.PROTOCOL_YAHOO:
+                return ProviderNames.YAHOO;
+            case Im.PROTOCOL_ICQ:
+                return ProviderNames.ICQ;
+            case Im.PROTOCOL_JABBER:
+                return ProviderNames.JABBER;
+            case Im.PROTOCOL_SKYPE:
+                return ProviderNames.SKYPE;
+            case Im.PROTOCOL_QQ:
+                return ProviderNames.QQ;
+        }
+        return null;
+    }
+
+
+    public static final long USER_TYPE_CURRENT = 0;
+    public static final long USER_TYPE_WORK = 1;
+
+    /**
+     * UserType indicates the user type of the contact. If the contact is from Work User (Work
+     * Profile in Android Multi-User System), it's {@link #USER_TYPE_WORK}, otherwise,
+     * {@link #USER_TYPE_CURRENT}. Please note that current user can be in work profile, where the
+     * dialer is running inside Work Profile.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({USER_TYPE_CURRENT, USER_TYPE_WORK})
+    public @interface UserType {}
+
+    /**
+     * Test if the given {@link CharSequence} contains any graphic characters,
+     * first checking {@link TextUtils#isEmpty(CharSequence)} to handle null.
+     */
+    public static boolean isGraphic(CharSequence str) {
+        return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str);
+    }
+
+    /**
+     * Returns true if two objects are considered equal.  Two null references are equal here.
+     */
+    @NeededForTesting
+    public static boolean areObjectsEqual(Object a, Object b) {
+        return a == b || (a != null && a.equals(b));
+    }
+
+    /**
+     * Returns true if two {@link Intent}s are both null, or have the same action.
+     */
+    public static final boolean areIntentActionEqual(Intent a, Intent b) {
+        if (a == b) {
+            return true;
+        }
+        if (a == null || b == null) {
+            return false;
+        }
+        return TextUtils.equals(a.getAction(), b.getAction());
+    }
+
+    public static boolean areGroupWritableAccountsAvailable(Context context) {
+        final List<AccountWithDataSet> accounts =
+                AccountTypeManager.getInstance(context).getGroupWritableAccounts();
+        return !accounts.isEmpty();
+    }
+
+    /**
+     * Returns the size (width and height) of thumbnail pictures as configured in the provider. This
+     * can safely be called from the UI thread, as the provider can serve this without performing
+     * a database access
+     */
+    public static int getThumbnailSize(Context context) {
+        if (sThumbnailSize == -1) {
+            final Cursor c = context.getContentResolver().query(
+                    DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
+                    new String[] { DisplayPhoto.THUMBNAIL_MAX_DIM }, null, null, null);
+            if (c != null) {
+                try {
+                    if (c.moveToFirst()) {
+                        sThumbnailSize = c.getInt(0);
+                    }
+                } finally {
+                    c.close();
+                }
+            }
+        }
+        return sThumbnailSize != -1 ? sThumbnailSize : DEFAULT_THUMBNAIL_SIZE;
+    }
+
+    private static Intent getCustomImIntent(ImDataItem im, int protocol) {
+        String host = im.getCustomProtocol();
+        final String data = im.getData();
+        if (TextUtils.isEmpty(data)) {
+            return null;
+        }
+        if (protocol != Im.PROTOCOL_CUSTOM) {
+            // Try bringing in a well-known host for specific protocols
+            host = ContactsUtils.lookupProviderNameFromId(protocol);
+        }
+        if (TextUtils.isEmpty(host)) {
+            return null;
+        }
+        final String authority = host.toLowerCase();
+        final Uri imUri = new Uri.Builder().scheme(SCHEME_IMTO).authority(
+                authority).appendPath(data).build();
+        final Intent intent = new Intent(Intent.ACTION_SENDTO, imUri);
+        return intent;
+    }
+
+    /**
+     * Returns the proper Intent for an ImDatItem. If available, a secondary intent is stored
+     * in the second Pair slot
+     */
+    public static Pair<Intent, Intent> buildImIntent(Context context, ImDataItem im) {
+        Intent intent = null;
+        Intent secondaryIntent = null;
+        final boolean isEmail = im.isCreatedFromEmail();
+
+        if (!isEmail && !im.isProtocolValid()) {
+            return new Pair<>(null, null);
+        }
+
+        final String data = im.getData();
+        if (TextUtils.isEmpty(data)) {
+            return new Pair<>(null, null);
+        }
+
+        final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
+
+        if (protocol == Im.PROTOCOL_GOOGLE_TALK) {
+            final int chatCapability = im.getChatCapability();
+            if ((chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) {
+                intent = new Intent(Intent.ACTION_SENDTO,
+                                Uri.parse("xmpp:" + data + "?message"));
+                secondaryIntent = new Intent(Intent.ACTION_SENDTO,
+                        Uri.parse("xmpp:" + data + "?call"));
+            } else if ((chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) {
+                // Allow Talking and Texting
+                intent =
+                    new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
+                secondaryIntent =
+                    new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call"));
+            } else {
+                intent =
+                    new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
+            }
+        } else {
+            // Build an IM Intent
+            intent = getCustomImIntent(im, protocol);
+        }
+        return new Pair<>(intent, secondaryIntent);
+    }
+
+    /**
+     * Determine UserType from directory id and contact id.
+     *
+     * 3 types of query
+     *
+     * 1. 2 profile query: content://com.android.contacts/phone_lookup_enterprise/1234567890
+     * personal and work contact are mixed into one cursor. no directory id. contact_id indicates if
+     * it's work contact
+     *
+     * 2. work local query:
+     * content://com.android.contacts/phone_lookup_enterprise/1234567890?directory=1000000000
+     * either directory_id or contact_id is enough to identify work contact
+     *
+     * 3. work remote query:
+     * content://com.android.contacts/phone_lookup_enterprise/1234567890?directory=1000000003
+     * contact_id is random. only directory_id is available
+     *
+     * Summary: If directory_id is not null, always use directory_id to identify work contact.
+     * (which is the case here) Otherwise, use contact_id.
+     *
+     * @param directoryId directory id of ContactsProvider query
+     * @param contactId contact id
+     * @return UserType indicates the user type of the contact. A directory id or contact id larger
+     *         than a thredshold indicates that the contact is stored in Work Profile, but not in
+     *         current user. It's a contract by ContactsProvider and check by
+     *         Contacts.isEnterpriseDirectoryId and Contacts.isEnterpriseContactId. Currently, only
+     *         2 kinds of users can be detected from the directoryId and contactId as
+     *         ContactsProvider can only access current and work user's contacts
+     */
+    public static @UserType long determineUserType(Long directoryId, Long contactId) {
+        // First check directory id
+        if (directoryId != null) {
+            return DirectoryCompat.isEnterpriseDirectoryId(directoryId) ? USER_TYPE_WORK
+                    : USER_TYPE_CURRENT;
+        }
+        // Only check contact id if directory id is null
+        if (contactId != null && contactId != 0L
+                && ContactsCompat.isEnterpriseContactId(contactId)) {
+            return USER_TYPE_WORK;
+        } else {
+            return USER_TYPE_CURRENT;
+        }
+
+    }
+}
diff --git a/src/com/android/contacts/common/Experiments.java b/src/com/android/contacts/common/Experiments.java
new file mode 100644
index 0000000..c811e27
--- /dev/null
+++ b/src/com/android/contacts/common/Experiments.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2016 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;
+
+/**
+ * Experiment flag names.
+ */
+public final class Experiments {
+
+    private Experiments() {
+    }
+}
diff --git a/src/com/android/contacts/common/GeoUtil.java b/src/com/android/contacts/common/GeoUtil.java
new file mode 100644
index 0000000..cd0139b
--- /dev/null
+++ b/src/com/android/contacts/common/GeoUtil.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+import android.app.Application;
+import android.content.Context;
+
+import com.android.contacts.common.location.CountryDetector;
+
+import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber;
+
+import java.util.Locale;
+
+/**
+ * Static methods related to Geo.
+ */
+public class GeoUtil {
+
+    /**
+     * Returns the country code of the country the user is currently in. Before calling this
+     * method, make sure that {@link CountryDetector#initialize(Context)} has already been called
+     * in {@link Application#onCreate()}.
+     * @return The ISO 3166-1 two letters country code of the country the user
+     *         is in.
+     */
+    public static String getCurrentCountryIso(Context context) {
+        // The {@link CountryDetector} should never return null so this is safe to return as-is.
+        return CountryDetector.getInstance(context).getCurrentCountryIso();
+    }
+
+    public static String getGeocodedLocationFor(Context context,  String phoneNumber) {
+        final PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance();
+        final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
+        try {
+            final Phonenumber.PhoneNumber structuredPhoneNumber =
+                    phoneNumberUtil.parse(phoneNumber, getCurrentCountryIso(context));
+            final Locale locale = context.getResources().getConfiguration().locale;
+            return geocoder.getDescriptionForNumber(structuredPhoneNumber, locale);
+        } catch (NumberParseException e) {
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/GroupMetaData.java b/src/com/android/contacts/common/GroupMetaData.java
new file mode 100644
index 0000000..fa86ae2
--- /dev/null
+++ b/src/com/android/contacts/common/GroupMetaData.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.contacts.common;
+
+/**
+ * Meta-data for a contact group.  We load all groups associated with the contact's
+ * constituent accounts.
+ */
+public final class GroupMetaData {
+    private String mAccountName;
+    private String mAccountType;
+    private String mDataSet;
+    private long mGroupId;
+    private String mTitle;
+    private boolean mDefaultGroup;
+    private boolean mFavorites;
+
+    public GroupMetaData(String accountName, String accountType, String dataSet, long groupId,
+            String title, boolean defaultGroup, boolean favorites) {
+        this.mAccountName = accountName;
+        this.mAccountType = accountType;
+        this.mDataSet = dataSet;
+        this.mGroupId = groupId;
+        this.mTitle = title;
+        this.mDefaultGroup = defaultGroup;
+        this.mFavorites = favorites;
+    }
+
+    public String getAccountName() {
+        return mAccountName;
+    }
+
+    public String getAccountType() {
+        return mAccountType;
+    }
+
+    public String getDataSet() {
+        return mDataSet;
+    }
+
+    public long getGroupId() {
+        return mGroupId;
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public boolean isDefaultGroup() {
+        return mDefaultGroup;
+    }
+
+    public boolean isFavorites() {
+        return mFavorites;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/common/MoreContactUtils.java b/src/com/android/contacts/common/MoreContactUtils.java
new file mode 100644
index 0000000..9b9f800
--- /dev/null
+++ b/src/com/android/contacts/common/MoreContactUtils.java
@@ -0,0 +1,243 @@
+/*
+ * 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;
+
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.contacts.common.model.account.AccountType;
+
+/**
+ * Shared static contact utility methods.
+ */
+public class MoreContactUtils {
+
+    private static final String WAIT_SYMBOL_AS_STRING = String.valueOf(PhoneNumberUtils.WAIT);
+
+    /**
+     * Returns true if two data with mimetypes which represent values in contact entries are
+     * considered equal for collapsing in the GUI. For caller-id, use
+     * {@link android.telephony.PhoneNumberUtils#compare(android.content.Context, String, String)}
+     * instead
+     */
+    public static boolean shouldCollapse(CharSequence mimetype1, CharSequence data1,
+              CharSequence mimetype2, CharSequence data2) {
+        // different mimetypes? don't collapse
+        if (!TextUtils.equals(mimetype1, mimetype2)) return false;
+
+        // exact same string? good, bail out early
+        if (TextUtils.equals(data1, data2)) return true;
+
+        // so if either is null, these two must be different
+        if (data1 == null || data2 == null) return false;
+
+        // if this is not about phone numbers, we know this is not a match (of course, some
+        // mimetypes could have more sophisticated matching is the future, e.g. addresses)
+        if (!TextUtils.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
+                mimetype1)) {
+            return false;
+        }
+
+        return shouldCollapsePhoneNumbers(data1.toString(), data2.toString());
+    }
+
+    // TODO: Move this to PhoneDataItem.shouldCollapse override
+    private static boolean shouldCollapsePhoneNumbers(String number1, String number2) {
+        // Work around to address b/20724444. We want to distinguish between #555, *555 and 555.
+        // This makes no attempt to distinguish between 555 and 55*5, since 55*5 is an improbable
+        // number. PhoneNumberUtil already distinguishes between 555 and 55#5.
+        if (number1.contains("#") != number2.contains("#")
+                || number1.contains("*") != number2.contains("*")) {
+            return false;
+        }
+
+        // Now do the full phone number thing. split into parts, separated by waiting symbol
+        // and compare them individually
+        final String[] dataParts1 = number1.split(WAIT_SYMBOL_AS_STRING);
+        final String[] dataParts2 = number2.split(WAIT_SYMBOL_AS_STRING);
+        if (dataParts1.length != dataParts2.length) return false;
+        final PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+        for (int i = 0; i < dataParts1.length; i++) {
+            // Match phone numbers represented by keypad letters, in which case prefer the
+            // phone number with letters.
+            final String dataPart1 = PhoneNumberUtils.convertKeypadLettersToDigits(dataParts1[i]);
+            final String dataPart2 = dataParts2[i];
+
+            // substrings equal? shortcut, don't parse
+            if (TextUtils.equals(dataPart1, dataPart2)) continue;
+
+            // do a full parse of the numbers
+            final PhoneNumberUtil.MatchType result = util.isNumberMatch(dataPart1, dataPart2);
+            switch (result) {
+                case NOT_A_NUMBER:
+                    // don't understand the numbers? let's play it safe
+                    return false;
+                case NO_MATCH:
+                    return false;
+                case EXACT_MATCH:
+                    break;
+                case NSN_MATCH:
+                    try {
+                        // For NANP phone numbers, match when one has +1 and the other does not.
+                        // In this case, prefer the +1 version.
+                        if (util.parse(dataPart1, null).getCountryCode() == 1) {
+                            // At this point, the numbers can be either case 1 or 2 below....
+                            //
+                            // case 1)
+                            // +14155551212    <--- country code 1
+                            //  14155551212    <--- 1 is trunk prefix, not country code
+                            //
+                            // and
+                            //
+                            // case 2)
+                            // +14155551212
+                            //   4155551212
+                            //
+                            // From b/7519057, case 2 needs to be equal.  But also that bug, case 3
+                            // below should not be equal.
+                            //
+                            // case 3)
+                            // 14155551212
+                            //  4155551212
+                            //
+                            // So in order to make sure transitive equality is valid, case 1 cannot
+                            // be equal.  Otherwise, transitive equality breaks and the following
+                            // would all be collapsed:
+                            //   4155551212  |
+                            //  14155551212  |---->   +14155551212
+                            // +14155551212  |
+                            //
+                            // With transitive equality, the collapsed values should be:
+                            //   4155551212  |         14155551212
+                            //  14155551212  |---->   +14155551212
+                            // +14155551212  |
+
+                            // Distinguish between case 1 and 2 by checking for trunk prefix '1'
+                            // at the start of number 2.
+                            if (dataPart2.trim().charAt(0) == '1') {
+                                // case 1
+                                return false;
+                            }
+                            break;
+                        }
+                    } catch (NumberParseException e) {
+                        // This is the case where the first number does not have a country code.
+                        // examples:
+                        // (123) 456-7890   &   123-456-7890  (collapse)
+                        // 0049 (8092) 1234   &   +49/80921234  (unit test says do not collapse)
+
+                        // Check the second number.  If it also does not have a country code, then
+                        // we should collapse.  If it has a country code, then it's a different
+                        // number and we should not collapse (this conclusion is based on an
+                        // existing unit test).
+                        try {
+                            util.parse(dataPart2, null);
+                        } catch (NumberParseException e2) {
+                            // Number 2 also does not have a country.  Collapse.
+                            break;
+                        }
+                    }
+                    return false;
+                case SHORT_NSN_MATCH:
+                    return false;
+                default:
+                    throw new IllegalStateException("Unknown result value from phone number " +
+                            "library");
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns the {@link android.graphics.Rect} with left, top, right, and bottom coordinates
+     * that are equivalent to the given {@link android.view.View}'s bounds. This is equivalent to
+     * how the target {@link android.graphics.Rect} is calculated in
+     * {@link android.provider.ContactsContract.QuickContact#showQuickContact}.
+     */
+    public static Rect getTargetRectFromView(View view) {
+        final int[] pos = new int[2];
+        view.getLocationOnScreen(pos);
+
+        final Rect rect = new Rect();
+        rect.left = pos[0];
+        rect.top = pos[1];
+        rect.right = pos[0] + view.getWidth();
+        rect.bottom = pos[1] + view.getHeight();
+        return rect;
+    }
+
+    /**
+     * Returns a header view based on the R.layout.list_separator, where the
+     * containing {@link android.widget.TextView} is set using the given textResourceId.
+     */
+    public static TextView createHeaderView(Context context, int textResourceId) {
+        final TextView textView = (TextView) View.inflate(context, R.layout.list_separator, null);
+        textView.setText(context.getString(textResourceId));
+        return textView;
+    }
+
+    /**
+     * Set the top padding on the header view dynamically, based on whether the header is in
+     * the first row or not.
+     */
+    public static void setHeaderViewBottomPadding(Context context, TextView textView,
+            boolean isFirstRow) {
+        final int topPadding;
+        if (isFirstRow) {
+            topPadding = (int) context.getResources().getDimension(
+                    R.dimen.frequently_contacted_title_top_margin_when_first_row);
+        } else {
+            topPadding = (int) context.getResources().getDimension(
+                    R.dimen.frequently_contacted_title_top_margin);
+        }
+        textView.setPaddingRelative(textView.getPaddingStart(), topPadding,
+                textView.getPaddingEnd(), textView.getPaddingBottom());
+    }
+
+
+    /**
+     * Returns the intent to launch for the given invitable account type and contact lookup URI.
+     * This will return null if the account type is not invitable (i.e. there is no
+     * {@link AccountType#getInviteContactActivityClassName()} or
+     * {@link AccountType#syncAdapterPackageName}).
+     */
+    public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) {
+        String syncAdapterPackageName = accountType.syncAdapterPackageName;
+        String className = accountType.getInviteContactActivityClassName();
+        if (TextUtils.isEmpty(syncAdapterPackageName) || TextUtils.isEmpty(className)) {
+            return null;
+        }
+        Intent intent = new Intent();
+        intent.setClassName(syncAdapterPackageName, className);
+
+        intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
+
+        // Data is the lookup URI.
+        intent.setData(lookupUri);
+        return intent;
+    }
+}
diff --git a/src/com/android/contacts/common/activity/AppCompatTransactionSafeActivity.java b/src/com/android/contacts/common/activity/AppCompatTransactionSafeActivity.java
new file mode 100644
index 0000000..e70a9fd
--- /dev/null
+++ b/src/com/android/contacts/common/activity/AppCompatTransactionSafeActivity.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 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.activity;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+/**
+ * A common superclass that keeps track of whether an {@link AppCompatActivity} has saved its state
+ * yet or not, copied from {@link com.android.contacts.common.activity.TransactionSafeActivity},
+ * which will be deprecated after Kitkat backporting is done.
+ */
+public abstract class AppCompatTransactionSafeActivity extends AppCompatActivity {
+
+    private boolean mIsSafeToCommitTransactions;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mIsSafeToCommitTransactions = true;
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        mIsSafeToCommitTransactions = true;
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mIsSafeToCommitTransactions = true;
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        mIsSafeToCommitTransactions = false;
+    }
+
+    /**
+     * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
+     * whether {@link FragmentActivity#onSaveInstanceState} has been called or not.
+     *
+     * Make sure that the current activity calls into
+     * {@link super.onSaveInstanceState(Bundle outState)} (if that method is overridden),
+     * so the flag is properly set.
+     */
+    public boolean isSafeToCommitTransactions() {
+        return mIsSafeToCommitTransactions;
+    }
+}
diff --git a/src/com/android/contacts/common/activity/LicenseActivity.java b/src/com/android/contacts/common/activity/LicenseActivity.java
new file mode 100644
index 0000000..71bcd84
--- /dev/null
+++ b/src/com/android/contacts/common/activity/LicenseActivity.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 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.activity;
+
+import com.android.contacts.common.R;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.webkit.WebView;
+
+/**
+ * Displays the licenses for all open source libraries.
+ */
+public class LicenseActivity extends Activity {
+    private static final String LICENSE_FILE = "file:///android_asset/licenses.html";
+    private WebView mWebView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.licenses);
+        mWebView = (WebView) findViewById(R.id.webview);
+        mWebView.loadUrl(LICENSE_FILE);
+        final ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        mWebView.destroy();
+        super.onDestroy();
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            finish();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/src/com/android/contacts/common/activity/RequestDesiredPermissionsActivity.java b/src/com/android/contacts/common/activity/RequestDesiredPermissionsActivity.java
new file mode 100644
index 0000000..8098fdf
--- /dev/null
+++ b/src/com/android/contacts/common/activity/RequestDesiredPermissionsActivity.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 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.activity;
+
+import android.Manifest.permission;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Requests permissions that are not absolutely required by the calling Activity;
+ * if permissions are denied, the calling Activity is still restarted.
+ *
+ * Activities that have a set of permissions that must be granted in order for the Activity to
+ * function propertly should call
+ * {@link RequestPermissionsActivity#startPermissionActivity(Activity, String[], Class)}
+ * before calling {@link RequestDesiredPermissionsActivity#startPermissionActivity(Activity)}.
+ */
+public class RequestDesiredPermissionsActivity extends RequestPermissionsActivityBase {
+
+    private static String[] sDesiredPermissions;
+
+    @Override
+    protected String[] getPermissions() {
+        return getPermissions(getPackageManager());
+    }
+
+    /**
+     * If any desired permission that Contacts app needs are missing, open an Activity
+     * to prompt user for these permissions. After that calling activity is restarted
+     * and in the second run permission check is skipped.
+     *
+     * This is designed to be called inside {@link android.app.Activity#onCreate}
+     */
+    public static boolean startPermissionActivity(Activity activity) {
+        final Bundle extras = activity.getIntent().getExtras();
+        if (extras != null && extras.getBoolean(EXTRA_STARTED_PERMISSIONS_ACTIVITY, false)) {
+            return false;
+        }
+        return startPermissionActivity(activity,
+                getPermissions(activity.getPackageManager()),
+                RequestDesiredPermissionsActivity.class);
+    }
+
+    private static String[] getPermissions(PackageManager packageManager) {
+        if (sDesiredPermissions == null) {
+            final List<String> permissions = new ArrayList<>();
+            // Calendar group
+            permissions.add(permission.READ_CALENDAR);
+
+            if (packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+                // SMS group
+                permissions.add(permission.READ_SMS);
+            }
+            sDesiredPermissions = permissions.toArray(new String[0]);
+        }
+        return sDesiredPermissions;
+    }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, String permissions[], int[] grantResults) {
+        mPreviousActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+        startActivity(mPreviousActivityIntent);
+        overridePendingTransition(0, 0);
+
+        finish();
+        overridePendingTransition(0, 0);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java b/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java
new file mode 100644
index 0000000..df98d12
--- /dev/null
+++ b/src/com/android/contacts/common/activity/RequestImportVCardPermissionsActivity.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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.activity;
+
+import android.Manifest.permission;
+import android.app.Activity;
+
+/**
+ * Activity that requests permissions needed for ImportVCardActivity.
+ */
+public class RequestImportVCardPermissionsActivity extends RequestPermissionsActivity {
+
+    private static final String[] REQUIRED_PERMISSIONS = new String[] {
+            // Contacts group
+            permission.GET_ACCOUNTS,
+            permission.READ_CONTACTS,
+            permission.WRITE_CONTACTS,
+            // Storage group
+            permission.READ_EXTERNAL_STORAGE,
+    };
+
+    @Override
+    protected String[] getPermissions() {
+        return REQUIRED_PERMISSIONS;
+    }
+
+    /**
+     * If any permissions the Contacts app needs are missing, open an Activity
+     * to prompt the user for these permissions. Moreover, finish the current activity.
+     *
+     * This is designed to be called inside {@link android.app.Activity#onCreate}
+     */
+    public static boolean startPermissionActivity(Activity activity) {
+        return startPermissionActivity(activity, REQUIRED_PERMISSIONS,
+                RequestImportVCardPermissionsActivity.class);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/common/activity/RequestPermissionsActivity.java b/src/com/android/contacts/common/activity/RequestPermissionsActivity.java
new file mode 100644
index 0000000..126cd64
--- /dev/null
+++ b/src/com/android/contacts/common/activity/RequestPermissionsActivity.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2015 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.activity;
+
+import com.android.contacts.common.R;
+
+import android.Manifest.permission;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Activity that requests permissions needed for activities exported from Contacts.
+ */
+public class RequestPermissionsActivity extends RequestPermissionsActivityBase {
+
+    private static String[] sRequiredPermissions;
+
+    @Override
+    protected String[] getPermissions() {
+        return getPermissions(getPackageManager());
+    }
+
+    public static boolean startPermissionActivity(Activity activity) {
+        return startPermissionActivity(activity,
+                getPermissions(activity.getPackageManager()),
+                RequestPermissionsActivity.class);
+    }
+
+    private static String[] getPermissions(PackageManager packageManager) {
+        if (sRequiredPermissions == null) {
+            final List<String> permissions = new ArrayList<>();
+            // Contacts group
+            permissions.add(permission.GET_ACCOUNTS);
+            permissions.add(permission.READ_CONTACTS);
+            permissions.add(permission.WRITE_CONTACTS);
+
+            if (packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+                // Phone group
+                // These are only used in a few places such as QuickContactActivity and
+                // ImportExportDialogFragment.  We work around missing this permission when
+                // telephony is not available on the device (i.e. on tablets).
+                permissions.add(permission.CALL_PHONE);
+                permissions.add(permission.READ_CALL_LOG);
+                permissions.add(permission.READ_PHONE_STATE);
+            }
+            sRequiredPermissions = permissions.toArray(new String[0]);
+        }
+        return sRequiredPermissions;
+    }
+
+    @Override
+    public void onRequestPermissionsResult(
+            int requestCode, String permissions[], int[] grantResults) {
+        if (permissions != null && permissions.length > 0
+                && isAllGranted(permissions, grantResults)) {
+            mPreviousActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+            startActivity(mPreviousActivityIntent);
+            finish();
+            overridePendingTransition(0, 0);
+        } else {
+            Toast.makeText(this, R.string.missing_required_permission, Toast.LENGTH_SHORT).show();
+            finish();
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/activity/RequestPermissionsActivityBase.java b/src/com/android/contacts/common/activity/RequestPermissionsActivityBase.java
new file mode 100644
index 0000000..999c545
--- /dev/null
+++ b/src/com/android/contacts/common/activity/RequestPermissionsActivityBase.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2015 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.activity;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.util.PermissionsUtil;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Trace;
+import android.support.v4.app.ActivityCompat;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Activity that asks the user for all {@link #getPermissions} if any are missing.
+ *
+ * NOTE: As a result of b/22095159, this can behave oddly in the case where the final permission
+ * you are requesting causes an application restart.
+ */
+public abstract class RequestPermissionsActivityBase extends Activity
+        implements ActivityCompat.OnRequestPermissionsResultCallback {
+
+    public static final String PREVIOUS_ACTIVITY_INTENT = "previous_intent";
+
+    /** Whether the permissions activity was already started. */
+    protected static final String EXTRA_STARTED_PERMISSIONS_ACTIVITY =
+            "started_permissions_activity";
+
+    private static final int PERMISSIONS_REQUEST_ALL_PERMISSIONS = 1;
+
+    /**
+     * @return list of permissions that are needed in order for {@link #PREVIOUS_ACTIVITY_INTENT}
+     * to operate. You only need to return a single permission per permission group you care about.
+     */
+    protected abstract String[] getPermissions();
+
+    protected Intent mPreviousActivityIntent;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mPreviousActivityIntent = (Intent) getIntent().getExtras().get(PREVIOUS_ACTIVITY_INTENT);
+
+        // Only start a requestPermissions() flow when first starting this activity the first time.
+        // The process is likely to be restarted during the permission flow (necessary to enable
+        // permissions) so this is important to track.
+        if (savedInstanceState == null) {
+            requestPermissions();
+        }
+    }
+
+    /**
+     * If any permissions the Contacts app needs are missing, open an Activity
+     * to prompt the user for these permissions. Moreover, finish the current activity.
+     *
+     * This is designed to be called inside {@link android.app.Activity#onCreate}
+     */
+    protected static boolean startPermissionActivity(Activity activity,
+            String[] requiredPermissions, Class<?> newActivityClass) {
+        if (!hasPermissions(activity, requiredPermissions)) {
+            final Intent intent = new Intent(activity,  newActivityClass);
+            activity.getIntent().putExtra(EXTRA_STARTED_PERMISSIONS_ACTIVITY, true);
+            intent.putExtra(PREVIOUS_ACTIVITY_INTENT, activity.getIntent());
+            activity.startActivity(intent);
+            activity.finish();
+            return true;
+        }
+
+        // Account type initialization must be delayed until the Contacts permission group
+        // has been granted (since GET_ACCOUNTS) falls under that groups.  Previously it
+        // was initialized in ContactApplication which would cause problems as
+        // AccountManager.getAccounts would return an empty array. See b/22690336
+        AccountTypeManager.getInstance(activity);
+
+        return false;
+    }
+
+    protected boolean isAllGranted(String permissions[], int[] grantResult) {
+        for (int i = 0; i < permissions.length; i++) {
+            if (grantResult[i] != PackageManager.PERMISSION_GRANTED
+                    && isPermissionRequired(permissions[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isPermissionRequired(String p) {
+        return Arrays.asList(getPermissions()).contains(p);
+    }
+
+    private void requestPermissions() {
+        Trace.beginSection("requestPermissions");
+        try {
+            // Construct a list of missing permissions
+            final ArrayList<String> unsatisfiedPermissions = new ArrayList<>();
+            for (String permission : getPermissions()) {
+                if (!PermissionsUtil.hasPermission(this, permission)) {
+                    unsatisfiedPermissions.add(permission);
+                }
+            }
+            if (unsatisfiedPermissions.size() == 0) {
+                throw new RuntimeException("Request permission activity was called even"
+                        + " though all permissions are satisfied.");
+            }
+            ActivityCompat.requestPermissions(
+                    this,
+                    unsatisfiedPermissions.toArray(new String[unsatisfiedPermissions.size()]),
+                    PERMISSIONS_REQUEST_ALL_PERMISSIONS);
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    protected static boolean hasPermissions(Context context, String[] permissions) {
+        Trace.beginSection("hasPermission");
+        try {
+            for (String permission : permissions) {
+                if (!PermissionsUtil.hasPermission(context, permission)) {
+                    return false;
+                }
+            }
+            return true;
+        } finally {
+            Trace.endSection();
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/activity/TransactionSafeActivity.java b/src/com/android/contacts/common/activity/TransactionSafeActivity.java
new file mode 100644
index 0000000..6c2e4fe
--- /dev/null
+++ b/src/com/android/contacts/common/activity/TransactionSafeActivity.java
@@ -0,0 +1,65 @@
+/*
+ * 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.activity;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * A common superclass that keeps track of whether an {@link Activity} has saved its state yet or
+ * not.
+ */
+public abstract class TransactionSafeActivity extends Activity {
+
+    private boolean mIsSafeToCommitTransactions;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mIsSafeToCommitTransactions = true;
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        mIsSafeToCommitTransactions = true;
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mIsSafeToCommitTransactions = true;
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        mIsSafeToCommitTransactions = false;
+    }
+
+    /**
+     * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
+     * whether {@link Activity#onSaveInstanceState} has been called or not.
+     *
+     * Make sure that the current activity calls into
+     * {@link super.onSaveInstanceState(Bundle outState)} (if that method is overridden),
+     * so the flag is properly set.
+     */
+    public boolean isSafeToCommitTransactions() {
+        return mIsSafeToCommitTransactions;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/CallSdkCompat.java b/src/com/android/contacts/common/compat/CallSdkCompat.java
new file mode 100644
index 0000000..fd06d85
--- /dev/null
+++ b/src/com/android/contacts/common/compat/CallSdkCompat.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 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.compat;
+
+import android.telecom.Call;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class CallSdkCompat {
+    public static class Details {
+        // TODO: This property is hidden in the N release; replace with actual when the API is
+        // made public.
+        public static final int PROPERTY_IS_EXTERNAL_CALL = 0x00000040;
+        public static final int PROPERTY_ENTERPRISE_CALL = Call.Details.PROPERTY_ENTERPRISE_CALL;
+        // TODO: This capability is hidden in the N release; replace with actual when the API is
+        // made public.
+        public static final int CAPABILITY_CAN_PULL_CALL = 0x00800000;
+        public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO =
+                Call.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO;
+    }
+
+    /**
+     * TODO: This API is hidden in the N release; replace the implementation with a call to the
+     * actual once it is made public.
+     */
+    public static void pullExternalCall(Call call) {
+        if (!CompatUtils.isNCompatible()) {
+            return;
+        }
+        Class<?> callClass = Call.class;
+        try {
+            Method pullExternalCallMethod = callClass.getDeclaredMethod("pullExternalCall");
+            pullExternalCallMethod.invoke(call);
+        } catch (NoSuchMethodException e) {
+            // Ignore requests to pull call if there is a problem.
+        } catch (InvocationTargetException e) {
+            // Ignore requests to pull call if there is a problem.
+        } catch (IllegalAccessException e) {
+            // Ignore requests to pull call if there is a problem.
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/compat/CallableCompat.java b/src/com/android/contacts/common/compat/CallableCompat.java
new file mode 100644
index 0000000..d25d4be
--- /dev/null
+++ b/src/com/android/contacts/common/compat/CallableCompat.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
+
+import com.android.contacts.common.ContactsUtils;
+
+public class CallableCompat {
+
+    // TODO: Use N APIs
+    private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
+            Uri.withAppendedPath(Callable.CONTENT_URI, "filter_enterprise");
+
+    public static Uri getContentFilterUri() {
+        if (ContactsUtils.FLAG_N_FEATURE) {
+            return ENTERPRISE_CONTENT_FILTER_URI;
+        }
+        return Callable.CONTENT_FILTER_URI;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/CompatUtils.java b/src/com/android/contacts/common/compat/CompatUtils.java
new file mode 100644
index 0000000..567f183
--- /dev/null
+++ b/src/com/android/contacts/common/compat/CompatUtils.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.model.CPOWrapper;
+
+import java.lang.reflect.InvocationTargetException;
+
+public final class CompatUtils {
+
+    private static final String TAG = CompatUtils.class.getSimpleName();
+
+    /**
+     * These 4 variables are copied from ContentProviderOperation for compatibility.
+     */
+    public final static int TYPE_INSERT = 1;
+
+    public final static int TYPE_UPDATE = 2;
+
+    public final static int TYPE_DELETE = 3;
+
+    public final static int TYPE_ASSERT = 4;
+
+    /**
+     * Returns whether the operation in CPOWrapper is of TYPE_INSERT;
+     */
+    public static boolean isInsertCompat(CPOWrapper cpoWrapper) {
+        if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
+            return cpoWrapper.getOperation().isInsert();
+        }
+        return (cpoWrapper.getType() == TYPE_INSERT);
+    }
+
+    /**
+     * Returns whether the operation in CPOWrapper is of TYPE_UPDATE;
+     */
+    public static boolean isUpdateCompat(CPOWrapper cpoWrapper) {
+        if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
+            return cpoWrapper.getOperation().isUpdate();
+        }
+        return (cpoWrapper.getType() == TYPE_UPDATE);
+    }
+
+    /**
+     * Returns whether the operation in CPOWrapper is of TYPE_DELETE;
+     */
+    public static boolean isDeleteCompat(CPOWrapper cpoWrapper) {
+        if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
+            return cpoWrapper.getOperation().isDelete();
+        }
+        return (cpoWrapper.getType() == TYPE_DELETE);
+    }
+    /**
+     * Returns whether the operation in CPOWrapper is of TYPE_ASSERT;
+     */
+    public static boolean isAssertQueryCompat(CPOWrapper cpoWrapper) {
+        if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
+            return cpoWrapper.getOperation().isAssertQuery();
+        }
+        return (cpoWrapper.getType() == TYPE_ASSERT);
+    }
+
+    /**
+     * PrioritizedMimeType is added in API level 23.
+     */
+    public static boolean hasPrioritizedMimeType() {
+        return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M)
+                >= Build.VERSION_CODES.M;
+    }
+
+    /**
+     * Determines if this version is compatible with multi-SIM and the phone account APIs. Can also
+     * force the version to be lower through SdkVersionOverride.
+     *
+     * @return {@code true} if multi-SIM capability is available, {@code false} otherwise.
+     */
+    public static boolean isMSIMCompatible() {
+        return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP)
+                >= Build.VERSION_CODES.LOLLIPOP_MR1;
+    }
+
+    /**
+     * Determines if this version is compatible with video calling. Can also force the version to be
+     * lower through SdkVersionOverride.
+     *
+     * @return {@code true} if video calling is allowed, {@code false} otherwise.
+     */
+    public static boolean isVideoCompatible() {
+        return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP)
+                >= Build.VERSION_CODES.M;
+    }
+
+    /**
+     * Determines if this version is capable of using presence checking for video calling. Support
+     * for video call presence indication is added in SDK 24.
+     *
+     * @return {@code true} if video presence checking is allowed, {@code false} otherwise.
+     */
+    public static boolean isVideoPresenceCompatible() {
+        return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M)
+                > Build.VERSION_CODES.M;
+    }
+
+    /**
+     * Determines if this version is compatible with call subject. Can also force the version to be
+     * lower through SdkVersionOverride.
+     *
+     * @return {@code true} if call subject is a feature on this device, {@code false} otherwise.
+     */
+    public static boolean isCallSubjectCompatible() {
+        return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP)
+                >= Build.VERSION_CODES.M;
+    }
+
+    /**
+     * Determines if this version is compatible with a default dialer. Can also force the version to
+     * be lower through {@link SdkVersionOverride}.
+     *
+     * @return {@code true} if default dialer is a feature on this device, {@code false} otherwise.
+     */
+    public static boolean isDefaultDialerCompatible() {
+        return isMarshmallowCompatible();
+    }
+
+    /**
+     * Determines if this version is compatible with Lollipop Mr1-specific APIs. Can also force the
+     * version to be lower through SdkVersionOverride.
+     *
+     * @return {@code true} if runtime sdk is compatible with Lollipop MR1, {@code false} otherwise.
+     */
+    public static boolean isLollipopMr1Compatible() {
+        return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP_MR1)
+                >= Build.VERSION_CODES.LOLLIPOP_MR1;
+    }
+
+    /**
+     * Determines if this version is compatible with Marshmallow-specific APIs. Can also force the
+     * version to be lower through SdkVersionOverride.
+     *
+     * @return {@code true} if runtime sdk is compatible with Marshmallow, {@code false} otherwise.
+     */
+    public static boolean isMarshmallowCompatible() {
+        return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP)
+                >= Build.VERSION_CODES.M;
+    }
+
+    /**
+     * Determines if this version is compatible with N-specific APIs.
+     *
+     * @return {@code true} if runtime sdk is compatible with N and the app is built with N, {@code
+     * false} otherwise.
+     */
+    public static boolean isNCompatible() {
+        return VERSION.SDK_INT >= 24;
+    }
+
+    /**
+     * Determines if the given class is available. Can be used to check if system apis exist at
+     * runtime.
+     *
+     * @param className the name of the class to look for.
+     * @return {@code true} if the given class is available, {@code false} otherwise or if className
+     * is empty.
+     */
+    public static boolean isClassAvailable(@Nullable String className) {
+        if (TextUtils.isEmpty(className)) {
+            return false;
+        }
+        try {
+            Class.forName(className);
+            return true;
+        } catch (ClassNotFoundException e) {
+            return false;
+        } catch (Throwable t) {
+            Log.e(TAG, "Unexpected exception when checking if class:" + className + " exists at "
+                    + "runtime", t);
+            return false;
+        }
+    }
+
+    /**
+     * Determines if the given class's method is available to call. Can be used to check if system
+     * apis exist at runtime.
+     *
+     * @param className the name of the class to look for
+     * @param methodName the name of the method to look for
+     * @param parameterTypes the needed parameter types for the method to look for
+     * @return {@code true} if the given class is available, {@code false} otherwise or if className
+     * or methodName are empty.
+     */
+    public static boolean isMethodAvailable(@Nullable String className, @Nullable String methodName,
+            Class<?>... parameterTypes) {
+        if (TextUtils.isEmpty(className) || TextUtils.isEmpty(methodName)) {
+            return false;
+        }
+
+        try {
+            Class.forName(className).getMethod(methodName, parameterTypes);
+            return true;
+        } catch (ClassNotFoundException | NoSuchMethodException e) {
+            Log.v(TAG, "Could not find method: " + className + "#" + methodName);
+            return false;
+        } catch (Throwable t) {
+            Log.e(TAG, "Unexpected exception when checking if method: " + className + "#"
+                    + methodName + " exists at runtime", t);
+            return false;
+        }
+    }
+
+    /**
+     * Invokes a given class's method using reflection. Can be used to call system apis that exist
+     * at runtime but not in the SDK.
+     *
+     * @param instance The instance of the class to invoke the method on.
+     * @param methodName The name of the method to invoke.
+     * @param parameterTypes The needed parameter types for the method.
+     * @param parameters The parameter values to pass into the method.
+     * @return The result of the invocation or {@code null} if instance or methodName are empty, or
+     * if the reflection fails.
+     */
+    @Nullable
+    public static Object invokeMethod(@Nullable Object instance, @Nullable String methodName,
+            Class<?>[] parameterTypes, Object[] parameters) {
+        if (instance == null || TextUtils.isEmpty(methodName)) {
+            return null;
+        }
+
+        String className = instance.getClass().getName();
+        try {
+            return Class.forName(className).getMethod(methodName, parameterTypes)
+                    .invoke(instance, parameters);
+        } catch (ClassNotFoundException | NoSuchMethodException | IllegalArgumentException
+                | IllegalAccessException | InvocationTargetException e) {
+            Log.v(TAG, "Could not invoke method: " + className + "#" + methodName);
+            return null;
+        } catch (Throwable t) {
+            Log.e(TAG, "Unexpected exception when invoking method: " + className
+                    + "#" + methodName + " at runtime", t);
+            return null;
+        }
+    }
+
+    /**
+     * Determines if this version is compatible with Lollipop-specific APIs. Can also force the
+     * version to be lower through SdkVersionOverride.
+     *
+     * @return {@code true} if call subject is a feature on this device, {@code false} otherwise.
+     */
+    public static boolean isLollipopCompatible() {
+        return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP)
+                >= Build.VERSION_CODES.LOLLIPOP;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/ContactsCompat.java b/src/com/android/contacts/common/compat/ContactsCompat.java
new file mode 100644
index 0000000..5a5e46a
--- /dev/null
+++ b/src/com/android/contacts/common/compat/ContactsCompat.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+
+import com.android.contacts.common.ContactsUtils;
+
+/**
+ * Compatibility class for {@link ContactsContract.Contacts}
+ */
+public class ContactsCompat {
+    /**
+     * Not instantiable.
+     */
+    private ContactsCompat() {
+    }
+
+    // TODO: Use N APIs
+    private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
+            Uri.withAppendedPath(Contacts.CONTENT_URI, "filter_enterprise");
+
+    // Copied from ContactsContract.Contacts#ENTERPRISE_CONTACT_ID_BASE, which is hidden.
+    private static final long ENTERPRISE_CONTACT_ID_BASE = 1000000000;
+
+    public static Uri getContentUri() {
+        if (ContactsUtils.FLAG_N_FEATURE) {
+            return ENTERPRISE_CONTENT_FILTER_URI;
+        }
+        return Contacts.CONTENT_FILTER_URI;
+    }
+
+    /**
+     * Return {@code true} if a contact ID is from the contacts provider on the enterprise profile.
+     */
+    public static boolean isEnterpriseContactId(long contactId) {
+        if (CompatUtils.isLollipopCompatible()) {
+            return Contacts.isEnterpriseContactId(contactId);
+        } else {
+            // copied from ContactsContract.Contacts.isEnterpriseContactId
+            return (contactId >= ENTERPRISE_CONTACT_ID_BASE) &&
+                    (contactId < ContactsContract.Profile.MIN_ID);
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/compat/DirectoryCompat.java b/src/com/android/contacts/common/compat/DirectoryCompat.java
new file mode 100644
index 0000000..682e14c
--- /dev/null
+++ b/src/com/android/contacts/common/compat/DirectoryCompat.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Directory;
+
+import com.android.contacts.common.ContactsUtils;
+
+public class DirectoryCompat {
+
+    public static Uri getContentUri() {
+        if (ContactsUtils.FLAG_N_FEATURE) {
+            return DirectorySdkCompat.ENTERPRISE_CONTENT_URI;
+        }
+        return Directory.CONTENT_URI;
+    }
+
+    public static boolean isInvisibleDirectory(long directoryId) {
+        if (ContactsUtils.FLAG_N_FEATURE) {
+            return (directoryId == Directory.LOCAL_INVISIBLE
+                    || directoryId == DirectorySdkCompat.ENTERPRISE_LOCAL_INVISIBLE);
+        }
+        return directoryId == Directory.LOCAL_INVISIBLE;
+    }
+
+    public static boolean isRemoteDirectoryId(long directoryId) {
+        if (ContactsUtils.FLAG_N_FEATURE) {
+            return DirectorySdkCompat.isRemoteDirectoryId(directoryId);
+        }
+        return !(directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE);
+    }
+
+    public static boolean isEnterpriseDirectoryId(long directoryId) {
+        return ContactsUtils.FLAG_N_FEATURE
+                ? DirectorySdkCompat.isEnterpriseDirectoryId(directoryId)
+                : false;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/DirectorySdkCompat.java b/src/com/android/contacts/common/compat/DirectorySdkCompat.java
new file mode 100644
index 0000000..b919466
--- /dev/null
+++ b/src/com/android/contacts/common/compat/DirectorySdkCompat.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.net.Uri;
+import android.provider.ContactsContract.Directory;
+
+public class DirectorySdkCompat {
+
+    private static final String TAG = "DirectorySdkCompat";
+
+    public static final Uri ENTERPRISE_CONTENT_URI = Directory.ENTERPRISE_CONTENT_URI;
+    public static final long ENTERPRISE_LOCAL_DEFAULT = Directory.ENTERPRISE_DEFAULT;
+    public static final long ENTERPRISE_LOCAL_INVISIBLE = Directory.ENTERPRISE_LOCAL_INVISIBLE;
+
+    public static boolean isRemoteDirectoryId(long directoryId) {
+        return CompatUtils.isNCompatible() ? Directory.isRemoteDirectoryId(directoryId) : false;
+    }
+
+    public static boolean isEnterpriseDirectoryId(long directoryId) {
+        return CompatUtils.isNCompatible() ? Directory.isEnterpriseDirectoryId(directoryId) : false;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/EventCompat.java b/src/com/android/contacts/common/compat/EventCompat.java
new file mode 100644
index 0000000..f37aeff
--- /dev/null
+++ b/src/com/android/contacts/common/compat/EventCompat.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.content.res.Resources;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.text.TextUtils;
+
+/**
+ * Compatibility class for {@link Event}
+ */
+public class EventCompat {
+    /**
+     * Not instantiable.
+     */
+    private EventCompat() {
+    }
+
+    /**
+     * Return a {@link CharSequence} that best describes the given type, possibly substituting
+     * the given label value for TYPE_CUSTOM.
+     */
+    public static CharSequence getTypeLabel(Resources res, int type, CharSequence label) {
+        if (CompatUtils.isLollipopCompatible()) {
+            return Event.getTypeLabel(res, type, label);
+        } else {
+            return getTypeLabelInternal(res, type, label);
+        }
+    }
+
+    /**
+     * The method was added in API level 21, and below is the implementation copied from
+     * {@link Event#getTypeLabel(Resources, int, CharSequence)}
+     */
+    private static CharSequence getTypeLabelInternal(Resources res, int type, CharSequence label) {
+        if (type == BaseTypes.TYPE_CUSTOM && !TextUtils.isEmpty(label)) {
+            return label;
+        } else {
+            return res.getText(Event.getTypeResource(type));
+        }
+    }
+
+}
diff --git a/src/com/android/contacts/common/compat/MetadataSyncEnabledCompat.java b/src/com/android/contacts/common/compat/MetadataSyncEnabledCompat.java
new file mode 100644
index 0000000..4a9650f
--- /dev/null
+++ b/src/com/android/contacts/common/compat/MetadataSyncEnabledCompat.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 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.compat;
+
+import android.content.Context;
+import android.provider.Settings;
+
+public class MetadataSyncEnabledCompat {
+    public static boolean isMetadataSyncEnabled(Context context) {
+        return CompatUtils.isNCompatible()
+                ? (Settings.Global.getInt(context.getContentResolver(),
+                        Settings.Global.CONTACT_METADATA_SYNC_ENABLED, 0) == 1)
+                : false;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/common/compat/MultiWindowCompat.java b/src/com/android/contacts/common/compat/MultiWindowCompat.java
new file mode 100644
index 0000000..6641279
--- /dev/null
+++ b/src/com/android/contacts/common/compat/MultiWindowCompat.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 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.compat;
+
+import android.app.Activity;
+
+public class MultiWindowCompat {
+    /**
+     * Returns true if the activity is currently in multi-window mode.
+     */
+    public static boolean isInMultiWindowMode(Activity activity) {
+        return CompatUtils.isNCompatible() ? activity.isInMultiWindowMode() : false;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/common/compat/PhoneAccountCompat.java b/src/com/android/contacts/common/compat/PhoneAccountCompat.java
new file mode 100644
index 0000000..00f4211
--- /dev/null
+++ b/src/com/android/contacts/common/compat/PhoneAccountCompat.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.util.Log;
+
+/**
+ * Compatiblity class for {@link android.telecom.PhoneAccount}
+ */
+public class PhoneAccountCompat {
+
+    private static final String TAG = PhoneAccountCompat.class.getSimpleName();
+
+    /**
+     * Gets the {@link Icon} associated with the given {@link PhoneAccount}
+     *
+     * @param phoneAccount the PhoneAccount from which to retrieve the Icon
+     * @return the Icon, or null
+     */
+    @Nullable
+    public static Icon getIcon(@Nullable PhoneAccount phoneAccount) {
+        if (phoneAccount == null) {
+            return null;
+        }
+
+        if (CompatUtils.isMarshmallowCompatible()) {
+            return phoneAccount.getIcon();
+        }
+
+        return null;
+    }
+
+    /**
+     * Builds and returns an icon {@code Drawable} to represent this {@code PhoneAccount} in a user
+     * interface.
+     *
+     * @param phoneAccount the PhoneAccount from which to build the icon.
+     * @param context A {@code Context} to use for loading Drawables.
+     *
+     * @return An icon for this PhoneAccount, or null
+     */
+    @Nullable
+    public static Drawable createIconDrawable(@Nullable PhoneAccount phoneAccount,
+            @Nullable Context context) {
+        if (phoneAccount == null || context == null) {
+            return null;
+        }
+
+        if (CompatUtils.isMarshmallowCompatible()) {
+            return createIconDrawableMarshmallow(phoneAccount, context);
+        }
+
+        if (CompatUtils.isLollipopMr1Compatible()) {
+            return createIconDrawableLollipopMr1(phoneAccount, context);
+        }
+        return null;
+    }
+
+    @Nullable
+    private static Drawable createIconDrawableMarshmallow(PhoneAccount phoneAccount,
+            Context context) {
+        Icon accountIcon = getIcon(phoneAccount);
+        if (accountIcon == null) {
+            return null;
+        }
+        return accountIcon.loadDrawable(context);
+    }
+
+    @Nullable
+    private static Drawable createIconDrawableLollipopMr1(PhoneAccount phoneAccount,
+            Context context) {
+        try {
+            return (Drawable) PhoneAccount.class.getMethod("createIconDrawable", Context.class)
+                    .invoke(phoneAccount, context);
+        } catch (ReflectiveOperationException e) {
+            return null;
+        } catch (Throwable t) {
+            Log.e(TAG, "Unexpected exception when attempting to call "
+                    + "android.telecom.PhoneAccount#createIconDrawable", t);
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/compat/PhoneAccountSdkCompat.java b/src/com/android/contacts/common/compat/PhoneAccountSdkCompat.java
new file mode 100644
index 0000000..5cbf617
--- /dev/null
+++ b/src/com/android/contacts/common/compat/PhoneAccountSdkCompat.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.os.Bundle;
+import android.telecom.PhoneAccount;
+
+public class PhoneAccountSdkCompat {
+
+    private static final String TAG = "PhoneAccountSdkCompat";
+
+    public static final String EXTRA_CALL_SUBJECT_MAX_LENGTH =
+            PhoneAccount.EXTRA_CALL_SUBJECT_MAX_LENGTH;
+    public static final String EXTRA_CALL_SUBJECT_CHARACTER_ENCODING =
+            PhoneAccount.EXTRA_CALL_SUBJECT_CHARACTER_ENCODING;
+
+    public static final int CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE =
+            PhoneAccount.CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE;
+
+    public static Bundle getExtras(PhoneAccount account) {
+        return CompatUtils.isNCompatible() ? account.getExtras() : null;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/PhoneCompat.java b/src/com/android/contacts/common/compat/PhoneCompat.java
new file mode 100644
index 0000000..5277761
--- /dev/null
+++ b/src/com/android/contacts/common/compat/PhoneCompat.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+import com.android.contacts.common.ContactsUtils;
+
+public class PhoneCompat {
+
+    // TODO: Use N APIs
+    private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
+            Uri.withAppendedPath(Phone.CONTENT_URI, "filter_enterprise");
+
+    public static Uri getContentFilterUri() {
+        if (ContactsUtils.FLAG_N_FEATURE) {
+            return ENTERPRISE_CONTENT_FILTER_URI;
+        }
+        return Phone.CONTENT_FILTER_URI;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/PhoneLookupSdkCompat.java b/src/com/android/contacts/common/compat/PhoneLookupSdkCompat.java
new file mode 100644
index 0000000..0c0a898
--- /dev/null
+++ b/src/com/android/contacts/common/compat/PhoneLookupSdkCompat.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 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.compat;
+
+import android.provider.ContactsContract;
+
+public class PhoneLookupSdkCompat {
+    public static final String CONTACT_ID = ContactsContract.PhoneLookup.CONTACT_ID;
+}
diff --git a/src/com/android/contacts/common/compat/PhoneNumberFormattingTextWatcherCompat.java b/src/com/android/contacts/common/compat/PhoneNumberFormattingTextWatcherCompat.java
new file mode 100644
index 0000000..42b604e
--- /dev/null
+++ b/src/com/android/contacts/common/compat/PhoneNumberFormattingTextWatcherCompat.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.telephony.PhoneNumberFormattingTextWatcher;
+
+public class PhoneNumberFormattingTextWatcherCompat {
+    public static PhoneNumberFormattingTextWatcher newInstance(String countryCode) {
+        if (CompatUtils.isLollipopCompatible()) {
+            return new PhoneNumberFormattingTextWatcher(countryCode);
+        }
+        return new PhoneNumberFormattingTextWatcher();
+    }
+}
diff --git a/src/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java b/src/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java
new file mode 100644
index 0000000..9a8fa41
--- /dev/null
+++ b/src/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
+
+import android.telephony.PhoneNumberUtils;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.style.TtsSpan;
+
+/**
+ * This class contains static utility methods extracted from PhoneNumberUtils, and the
+ * methods were added in API level 23. In this way, we could enable the corresponding functionality
+ * for pre-M devices. We need maintain this class and keep it synced with PhoneNumberUtils.
+ * Another thing to keep in mind is that we use com.google.i18n rather than com.android.i18n in
+ * here, so we need make sure the application behavior is preserved.
+ */
+public class PhoneNumberUtilsCompat {
+    /**
+     * Not instantiable.
+     */
+    private PhoneNumberUtilsCompat() {}
+
+    public static String normalizeNumber(String phoneNumber) {
+        if (CompatUtils.isLollipopCompatible()) {
+            return PhoneNumberUtils.normalizeNumber(phoneNumber);
+        } else {
+            return normalizeNumberInternal(phoneNumber);
+        }
+    }
+
+    /**
+     * Implementation copied from {@link PhoneNumberUtils#normalizeNumber}
+     */
+    private static String normalizeNumberInternal(String phoneNumber) {
+        if (TextUtils.isEmpty(phoneNumber)) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        int len = phoneNumber.length();
+        for (int i = 0; i < len; i++) {
+            char c = phoneNumber.charAt(i);
+            // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.)
+            int digit = Character.digit(c, 10);
+            if (digit != -1) {
+                sb.append(digit);
+            } else if (sb.length() == 0 && c == '+') {
+                sb.append(c);
+            } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
+                return normalizeNumber(PhoneNumberUtils.convertKeypadLettersToDigits(phoneNumber));
+            }
+        }
+        return sb.toString();
+    }
+
+    public static String formatNumber(
+            String phoneNumber, String phoneNumberE164, String defaultCountryIso) {
+        if (CompatUtils.isLollipopCompatible()) {
+            return PhoneNumberUtils.formatNumber(phoneNumber, phoneNumberE164, defaultCountryIso);
+        } else {
+            // This method was deprecated in API level 21, so it's only used on pre-L SDKs.
+            return PhoneNumberUtils.formatNumber(phoneNumber);
+        }
+    }
+
+    public static CharSequence createTtsSpannable(CharSequence phoneNumber) {
+        if (CompatUtils.isMarshmallowCompatible()) {
+            return PhoneNumberUtils.createTtsSpannable(phoneNumber);
+        } else {
+            return createTtsSpannableInternal(phoneNumber);
+        }
+    }
+
+    public static TtsSpan createTtsSpan(String phoneNumber) {
+        if (CompatUtils.isMarshmallowCompatible()) {
+            return PhoneNumberUtils.createTtsSpan(phoneNumber);
+        } else if (CompatUtils.isLollipopCompatible()) {
+            return createTtsSpanLollipop(phoneNumber);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Copied from {@link PhoneNumberUtils#createTtsSpannable}
+     */
+    private static CharSequence createTtsSpannableInternal(CharSequence phoneNumber) {
+        if (phoneNumber == null) {
+            return null;
+        }
+        Spannable spannable = Spannable.Factory.getInstance().newSpannable(phoneNumber);
+        addTtsSpanInternal(spannable, 0, spannable.length());
+        return spannable;
+    }
+
+    /**
+     * Compat method for addTtsSpan, see {@link PhoneNumberUtils#addTtsSpan}
+     */
+    public static void addTtsSpan(Spannable s, int start, int endExclusive) {
+        if (CompatUtils.isMarshmallowCompatible()) {
+            PhoneNumberUtils.addTtsSpan(s, start, endExclusive);
+        } else {
+            addTtsSpanInternal(s, start, endExclusive);
+        }
+    }
+
+    /**
+     * Copied from {@link PhoneNumberUtils#addTtsSpan}
+     */
+    private static void addTtsSpanInternal(Spannable s, int start, int endExclusive) {
+        s.setSpan(createTtsSpan(s.subSequence(start, endExclusive).toString()),
+                start,
+                endExclusive,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+    }
+
+    /**
+     * Copied from {@link PhoneNumberUtils#createTtsSpan}
+     */
+    private static TtsSpan createTtsSpanLollipop(String phoneNumberString) {
+        if (phoneNumberString == null) {
+            return null;
+        }
+
+        // Parse the phone number
+        final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
+        PhoneNumber phoneNumber = null;
+        try {
+            // Don't supply a defaultRegion so this fails for non-international numbers because
+            // we don't want to TalkBalk to read a country code (e.g. +1) if it is not already
+            // present
+            phoneNumber = phoneNumberUtil.parse(phoneNumberString, /* defaultRegion */ null);
+        } catch (NumberParseException ignored) {
+        }
+
+        // Build a telephone tts span
+        final TtsSpan.TelephoneBuilder builder = new TtsSpan.TelephoneBuilder();
+        if (phoneNumber == null) {
+            // Strip separators otherwise TalkBack will be silent
+            // (this behavior was observed with TalkBalk 4.0.2 from their alpha channel)
+            builder.setNumberParts(splitAtNonNumerics(phoneNumberString));
+        } else {
+            if (phoneNumber.hasCountryCode()) {
+                builder.setCountryCode(Integer.toString(phoneNumber.getCountryCode()));
+            }
+            builder.setNumberParts(Long.toString(phoneNumber.getNationalNumber()));
+        }
+        return builder.build();
+    }
+
+
+
+    /**
+     * Split a phone number using spaces, ignoring anything that is not a digit
+     * @param number A {@code CharSequence} before splitting, e.g., "+20(123)-456#"
+     * @return A {@code String} after splitting, e.g., "20 123 456".
+     */
+    private static String splitAtNonNumerics(CharSequence number) {
+        StringBuilder sb = new StringBuilder(number.length());
+        for (int i = 0; i < number.length(); i++) {
+            sb.append(PhoneNumberUtils.isISODigit(number.charAt(i))
+                    ? number.charAt(i)
+                    : " ");
+        }
+        // It is very important to remove extra spaces. At time of writing, any leading or trailing
+        // spaces, or any sequence of more than one space, will confuse TalkBack and cause the TTS
+        // span to be non-functional!
+        return sb.toString().replaceAll(" +", " ").trim();
+    }
+
+}
diff --git a/src/com/android/contacts/common/compat/ProviderStatusCompat.java b/src/com/android/contacts/common/compat/ProviderStatusCompat.java
new file mode 100644
index 0000000..84338de
--- /dev/null
+++ b/src/com/android/contacts/common/compat/ProviderStatusCompat.java
@@ -0,0 +1,70 @@
+
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.os.Build;
+import android.provider.ContactsContract.ProviderStatus;
+
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.SdkVersionOverride;
+
+/**
+ * This class contains constants from the pre-M version of ContactsContract.ProviderStatus class
+ * and also the mappings between pre-M constants and M constants for compatibility purpose,
+ * because ProviderStatus class constant names and values changed and the class became visible in
+ * API level 23.
+ */
+public class ProviderStatusCompat {
+    /**
+     * Not instantiable.
+     */
+    private ProviderStatusCompat() {
+    }
+
+    public static final boolean USE_CURRENT_VERSION = CompatUtils.isMarshmallowCompatible();
+
+    public static final int STATUS_EMPTY = USE_CURRENT_VERSION ?
+            ProviderStatus.STATUS_EMPTY : ProviderStatusCompat.STATUS_NO_ACCOUNTS_NO_CONTACTS;
+
+    public static final int STATUS_BUSY = USE_CURRENT_VERSION ?
+            ProviderStatus.STATUS_BUSY : ProviderStatusCompat.STATUS_UPGRADING;
+
+    /**
+     * Default status of the provider, using the actual constant to guard against errors
+     */
+    public static final int STATUS_NORMAL = ProviderStatus.STATUS_NORMAL;
+
+    /**
+     * The following three constants are from pre-M.
+     *
+     * The status used when the provider is in the process of upgrading.  Contacts
+     * are temporarily unaccessible.
+     */
+    private static final int STATUS_UPGRADING = 1;
+
+    /**
+     * The status used during a locale change.
+     */
+    public static final int STATUS_CHANGING_LOCALE = 3;
+
+    /**
+     * The status that indicates that there are no accounts and no contacts
+     * on the device.
+     */
+    private static final int STATUS_NO_ACCOUNTS_NO_CONTACTS = 4;
+}
diff --git a/src/com/android/contacts/common/compat/SdkVersionOverride.java b/src/com/android/contacts/common/compat/SdkVersionOverride.java
new file mode 100644
index 0000000..ebde623
--- /dev/null
+++ b/src/com/android/contacts/common/compat/SdkVersionOverride.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.os.Build.VERSION;
+
+/**
+ * Class used to override the current sdk version to test specific branches of compatibility
+ * logic. When such branching occurs, use {@link #getSdkVersion(int)} rather than explicitly
+ * calling {@link VERSION#SDK_INT}. This allows the sdk version to be forced to a specific value.
+ */
+public class SdkVersionOverride {
+
+    /**
+     * Flag used to determine if override sdk versions are returned.
+     */
+    private static final boolean ALLOW_OVERRIDE_VERSION = false;
+
+    private SdkVersionOverride() {}
+
+    /**
+     * Gets the sdk version
+     *
+     * @param overrideVersion the version to attempt using
+     * @return overrideVersion if the {@link #ALLOW_OVERRIDE_VERSION} flag is set to {@code true},
+     * otherwise the current version
+     */
+    public static int getSdkVersion(int overrideVersion) {
+        return ALLOW_OVERRIDE_VERSION ? overrideVersion : VERSION.SDK_INT;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/TelecomManagerUtil.java b/src/com/android/contacts/common/compat/TelecomManagerUtil.java
new file mode 100644
index 0000000..30c541c
--- /dev/null
+++ b/src/com/android/contacts/common/compat/TelecomManagerUtil.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 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.compat;
+
+import android.content.Intent;
+import android.telecom.TelecomManager;
+
+/**
+ * Utility class for TelecomManager
+ */
+public class TelecomManagerUtil {
+    /**
+     * Creates {@link Intent} to launch the activity to manage blocked numbers.
+     */
+    public static Intent createManageBlockedNumbersIntent(TelecomManager tm) {
+        return CompatUtils.isNCompatible() ? tm.createManageBlockedNumbersIntent() : null;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/TelephonyManagerCompat.java b/src/com/android/contacts/common/compat/TelephonyManagerCompat.java
new file mode 100644
index 0000000..ec7907f
--- /dev/null
+++ b/src/com/android/contacts/common/compat/TelephonyManagerCompat.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+
+import com.android.contacts.common.ContactsUtils;
+
+public class TelephonyManagerCompat {
+    public static final String TELEPHONY_MANAGER_CLASS = "android.telephony.TelephonyManager";
+
+    /**
+     * @param telephonyManager The telephony manager instance to use for method calls.
+     * @return true if the current device is "voice capable".
+     * <p>
+     * "Voice capable" means that this device supports circuit-switched
+     * (i.e. voice) phone calls over the telephony network, and is allowed
+     * to display the in-call UI while a cellular voice call is active.
+     * This will be false on "data only" devices which can't make voice
+     * calls and don't support any in-call UI.
+     * <p>
+     * Note: the meaning of this flag is subtly different from the
+     * PackageManager.FEATURE_TELEPHONY system feature, which is available
+     * on any device with a telephony radio, even if the device is
+     * data-only.
+     */
+    public static boolean isVoiceCapable(@Nullable TelephonyManager telephonyManager) {
+        if (telephonyManager == null) {
+            return false;
+        }
+        if (CompatUtils.isLollipopMr1Compatible()
+                || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isVoiceCapable")) {
+            // isVoiceCapable was unhidden in L-MR1
+            return telephonyManager.isVoiceCapable();
+        }
+        final int phoneType = telephonyManager.getPhoneType();
+        return phoneType == TelephonyManager.PHONE_TYPE_CDMA ||
+                phoneType == TelephonyManager.PHONE_TYPE_GSM;
+    }
+
+    /**
+     * Returns the number of phones available.
+     * Returns 1 for Single standby mode (Single SIM functionality)
+     * Returns 2 for Dual standby mode.(Dual SIM functionality)
+     *
+     * Returns 1 if the method or telephonyManager is not available.
+     *
+     * @param telephonyManager The telephony manager instance to use for method calls.
+     */
+    public static int getPhoneCount(@Nullable TelephonyManager telephonyManager) {
+        if (telephonyManager == null) {
+            return 1;
+        }
+        if (CompatUtils.isMarshmallowCompatible() || CompatUtils
+                .isMethodAvailable(TELEPHONY_MANAGER_CLASS, "getPhoneCount")) {
+            return telephonyManager.getPhoneCount();
+        }
+        return 1;
+    }
+
+    /**
+     * Returns the unique device ID of a subscription, for example, the IMEI for
+     * GSM and the MEID for CDMA phones. Return null if device ID is not available.
+     *
+     * <p>Requires Permission:
+     *   {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
+     *
+     * @param telephonyManager The telephony manager instance to use for method calls.
+     * @param slotId of which deviceID is returned
+     */
+    public static String getDeviceId(@Nullable TelephonyManager telephonyManager, int slotId) {
+        if (telephonyManager == null) {
+            return null;
+        }
+        if (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS,
+                        "getDeviceId", Integer.class)) {
+            return telephonyManager.getDeviceId(slotId);
+        }
+        return null;
+    }
+
+    /**
+     * Whether the phone supports TTY mode.
+     *
+     * @param telephonyManager The telephony manager instance to use for method calls.
+     * @return {@code true} if the device supports TTY mode, and {@code false} otherwise.
+     */
+
+    public static boolean isTtyModeSupported(@Nullable TelephonyManager telephonyManager) {
+        if (telephonyManager == null) {
+            return false;
+        }
+        if (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isTtyModeSupported")) {
+            return telephonyManager.isTtyModeSupported();
+        }
+        return false;
+    }
+
+    /**
+     * Whether the phone supports hearing aid compatibility.
+     *
+     * @param telephonyManager The telephony manager instance to use for method calls.
+     * @return {@code true} if the device supports hearing aid compatibility, and {@code false}
+     * otherwise.
+     */
+    public static boolean isHearingAidCompatibilitySupported(
+            @Nullable TelephonyManager telephonyManager) {
+        if (telephonyManager == null) {
+            return false;
+        }
+        if (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS,
+                        "isHearingAidCompatibilitySupported")) {
+            return telephonyManager.isHearingAidCompatibilitySupported();
+        }
+        return false;
+    }
+
+    /**
+     * Returns the URI for the per-account voicemail ringtone set in Phone settings.
+     *
+     * @param telephonyManager The telephony manager instance to use for method calls.
+     * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to
+     * retrieve the voicemail ringtone.
+     * @return The URI for the ringtone to play when receiving a voicemail from a specific
+     * PhoneAccount.
+     */
+    @Nullable
+    public static Uri getVoicemailRingtoneUri(TelephonyManager telephonyManager,
+            PhoneAccountHandle accountHandle) {
+        if (!CompatUtils.isNCompatible()) {
+            return null;
+        }
+        return TelephonyManagerSdkCompat
+                .getVoicemailRingtoneUri(telephonyManager, accountHandle);
+    }
+
+    /**
+     * Returns whether vibration is set for voicemail notification in Phone settings.
+     *
+     * @param telephonyManager The telephony manager instance to use for method calls.
+     * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to
+     * retrieve the voicemail vibration setting.
+     * @return {@code true} if the vibration is set for this PhoneAccount, {@code false} otherwise.
+     */
+    public static boolean isVoicemailVibrationEnabled(TelephonyManager telephonyManager,
+            PhoneAccountHandle accountHandle) {
+        if (!CompatUtils.isNCompatible()) {
+            return true;
+        }
+        return TelephonyManagerSdkCompat
+                .isVoicemailVibrationEnabled(telephonyManager, accountHandle);
+    }
+}
diff --git a/src/com/android/contacts/common/compat/TelephonyManagerSdkCompat.java b/src/com/android/contacts/common/compat/TelephonyManagerSdkCompat.java
new file mode 100644
index 0000000..acabfdf
--- /dev/null
+++ b/src/com/android/contacts/common/compat/TelephonyManagerSdkCompat.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 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.compat;
+
+import android.net.Uri;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+
+/**
+ * On N and above, this will look up voicemail notification settings from Telephony.
+ */
+public class TelephonyManagerSdkCompat {
+    public static Uri getVoicemailRingtoneUri(TelephonyManager telephonyManager,
+            PhoneAccountHandle accountHandle) {
+        return CompatUtils.isNCompatible()
+                ? telephonyManager.getVoicemailRingtoneUri(accountHandle) : null;
+    }
+
+    public static boolean isVoicemailVibrationEnabled(TelephonyManager telephonyManager,
+            PhoneAccountHandle accountHandle) {
+        return CompatUtils.isNCompatible()
+                ? telephonyManager.isVoicemailVibrationEnabled(accountHandle) : false;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/TelephonyThreadsCompat.java b/src/com/android/contacts/common/compat/TelephonyThreadsCompat.java
new file mode 100644
index 0000000..d9642c7
--- /dev/null
+++ b/src/com/android/contacts/common/compat/TelephonyThreadsCompat.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.BaseColumns;
+import android.provider.Telephony;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Patterns;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class contains static utility methods and variables extracted from Telephony and
+ * SqliteWrapper, and the methods were made visible in API level 23. In this way, we could
+ * enable the corresponding functionality for pre-M devices. We need maintain this class and keep
+ * it synced with Telephony and SqliteWrapper.
+ */
+public class TelephonyThreadsCompat {
+    /**
+     * Not instantiable.
+     */
+    private TelephonyThreadsCompat() {}
+
+    private static final String TAG = "TelephonyThreadsCompat";
+
+    public static long getOrCreateThreadId(Context context, String recipient) {
+        if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
+            return Telephony.Threads.getOrCreateThreadId(context, recipient);
+        } else {
+            return getOrCreateThreadIdInternal(context, recipient);
+        }
+    }
+
+    // Below is code copied from Telephony and SqliteWrapper
+    /**
+     * Private {@code content://} style URL for this table. Used by
+     * {@link #getOrCreateThreadId(Context, Set)}.
+     */
+    private static final Uri THREAD_ID_CONTENT_URI = Uri.parse("content://mms-sms/threadID");
+
+    private static final String[] ID_PROJECTION = { BaseColumns._ID };
+
+    /**
+     * Regex pattern for names and email addresses.
+     * <ul>
+     *     <li><em>mailbox</em> = {@code name-addr}</li>
+     *     <li><em>name-addr</em> = {@code [display-name] angle-addr}</li>
+     *     <li><em>angle-addr</em> = {@code [CFWS] "<" addr-spec ">" [CFWS]}</li>
+     * </ul>
+     */
+    private static final Pattern NAME_ADDR_EMAIL_PATTERN =
+            Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*");
+
+    /**
+     * Copied from {@link Telephony.Threads#getOrCreateThreadId(Context, String)}
+     */
+    private static long getOrCreateThreadIdInternal(Context context, String recipient) {
+        Set<String> recipients = new HashSet<String>();
+
+        recipients.add(recipient);
+        return getOrCreateThreadIdInternal(context, recipients);
+    }
+
+    /**
+     * Given the recipients list and subject of an unsaved message,
+     * return its thread ID.  If the message starts a new thread,
+     * allocate a new thread ID.  Otherwise, use the appropriate
+     * existing thread ID.
+     *
+     * <p>Find the thread ID of the same set of recipients (in any order,
+     * without any additions). If one is found, return it. Otherwise,
+     * return a unique thread ID.</p>
+     */
+    private static long getOrCreateThreadIdInternal(Context context, Set<String> recipients) {
+        Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon();
+
+        for (String recipient : recipients) {
+            if (isEmailAddress(recipient)) {
+                recipient = extractAddrSpec(recipient);
+            }
+
+            uriBuilder.appendQueryParameter("recipient", recipient);
+        }
+
+        Uri uri = uriBuilder.build();
+
+        Cursor cursor = query(
+                context.getContentResolver(), uri, ID_PROJECTION, null, null, null);
+        if (cursor != null) {
+            try {
+                if (cursor.moveToFirst()) {
+                    return cursor.getLong(0);
+                } else {
+                    Log.e(TAG, "getOrCreateThreadId returned no rows!");
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+
+        Log.e(TAG, "getOrCreateThreadId failed with uri " + uri.toString());
+        throw new IllegalArgumentException("Unable to find or allocate a thread ID.");
+    }
+
+    /**
+     * Copied from {@link SqliteWrapper#query}
+     */
+    private static Cursor query(ContentResolver resolver, Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        try {
+            return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+        } catch (Exception e) {
+            Log.e(TAG, "Catch an exception when query: ", e);
+            return null;
+        }
+    }
+
+    /**
+     * Is the specified address an email address?
+     *
+     * @param address the input address to test
+     * @return true if address is an email address; false otherwise.
+     */
+    private static boolean isEmailAddress(String address) {
+        if (TextUtils.isEmpty(address)) {
+            return false;
+        }
+
+        String s = extractAddrSpec(address);
+        Matcher match = Patterns.EMAIL_ADDRESS.matcher(s);
+        return match.matches();
+    }
+
+    /**
+     * Helper method to extract email address from address string.
+     */
+    private static String extractAddrSpec(String address) {
+        Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address);
+
+        if (match.matches()) {
+            return match.group(2);
+        }
+        return address;
+    }
+}
diff --git a/src/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java b/src/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
new file mode 100644
index 0000000..6292b7f
--- /dev/null
+++ b/src/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2015 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.compat.telecom;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.contacts.common.compat.CompatUtils;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Compatibility class for {@link android.telecom.TelecomManager}.
+ */
+public class TelecomManagerCompat {
+    public static final String TELECOM_MANAGER_CLASS = "android.telecom.TelecomManager";
+    /**
+     * Places a new outgoing call to the provided address using the system telecom service with
+     * the specified intent.
+     *
+     * @param activity {@link Activity} used to start another activity for the given intent
+     * @param telecomManager the {@link TelecomManager} used to place a call, if possible
+     * @param intent the intent for the call
+     */
+    public static void placeCall(@Nullable Activity activity,
+            @Nullable TelecomManager telecomManager, @Nullable Intent intent) {
+        if (activity == null || telecomManager == null || intent == null) {
+            return;
+        }
+        if (CompatUtils.isMarshmallowCompatible()) {
+            telecomManager.placeCall(intent.getData(), intent.getExtras());
+            return;
+        }
+        activity.startActivityForResult(intent, 0);
+    }
+
+    /**
+     * Get the URI for running an adn query.
+     *
+     * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+     * @param accountHandle The handle for the account to derive an adn query URI for or
+     * {@code null} to return a URI which will use the default account.
+     * @return The URI (with the content:// scheme) specific to the specified {@link PhoneAccount}
+     * for the the content retrieve.
+     */
+    public static Uri getAdnUriForPhoneAccount(@Nullable TelecomManager telecomManager,
+            PhoneAccountHandle accountHandle) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "getAdnUriForPhoneAccount",
+                        PhoneAccountHandle.class))) {
+            return telecomManager.getAdnUriForPhoneAccount(accountHandle);
+        }
+        return Uri.parse("content://icc/adn");
+    }
+
+    /**
+     * Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone
+     * calls. The returned list includes only those accounts which have been explicitly enabled
+     * by the user.
+     *
+     * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+     * @return A list of PhoneAccountHandle objects.
+     */
+    public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(
+            @Nullable TelecomManager telecomManager) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS,
+                        "getCallCapablePhoneAccounts"))) {
+            return telecomManager.getCallCapablePhoneAccounts();
+        }
+        return new ArrayList<>();
+    }
+
+    /**
+     * Used to determine the currently selected default dialer package.
+     *
+     * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+     * @return package name for the default dialer package or null if no package has been
+     *         selected as the default dialer.
+     */
+    @Nullable
+    public static String getDefaultDialerPackage(@Nullable TelecomManager telecomManager) {
+        if (telecomManager != null && CompatUtils.isDefaultDialerCompatible()) {
+            return telecomManager.getDefaultDialerPackage();
+        }
+        return null;
+    }
+
+    /**
+     * Return the {@link PhoneAccount} which will be used to place outgoing calls to addresses with
+     * the specified {@code uriScheme}. This PhoneAccount will always be a member of the
+     * list which is returned from invoking {@link TelecomManager#getCallCapablePhoneAccounts()}.
+     * The specific account returned depends on the following priorities:
+     *
+     * 1. If the user-selected default PhoneAccount supports the specified scheme, it will
+     * be returned.
+     * 2. If there exists only one PhoneAccount that supports the specified scheme, it
+     * will be returned.
+     *
+     * If no PhoneAccount fits the criteria above, this method will return {@code null}.
+     *
+     * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+     * @param uriScheme The URI scheme.
+     * @return The {@link PhoneAccountHandle} corresponding to the account to be used.
+     */
+    @Nullable
+    public static PhoneAccountHandle getDefaultOutgoingPhoneAccount(
+            @Nullable TelecomManager telecomManager, @Nullable String uriScheme) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS,
+                        "getDefaultOutgoingPhoneAccount", String.class))) {
+            return telecomManager.getDefaultOutgoingPhoneAccount(uriScheme);
+        }
+        return null;
+    }
+
+    /**
+     * Return the line 1 phone number for given phone account.
+     *
+     * @param telecomManager the {@link TelecomManager} to use in the event that
+     *    {@link TelecomManager#getLine1Number(PhoneAccountHandle)} is available
+     * @param telephonyManager the {@link TelephonyManager} to use if TelecomManager#getLine1Number
+     *    is unavailable
+     * @param phoneAccountHandle the phoneAccountHandle upon which to check the line one number
+     * @return the line one number
+     */
+    @Nullable
+    public static String getLine1Number(@Nullable TelecomManager telecomManager,
+            @Nullable TelephonyManager telephonyManager,
+            @Nullable PhoneAccountHandle phoneAccountHandle) {
+        if (telecomManager != null && CompatUtils.isMarshmallowCompatible()) {
+            return telecomManager.getLine1Number(phoneAccountHandle);
+        }
+        if (telephonyManager != null) {
+            return telephonyManager.getLine1Number();
+        }
+        return null;
+    }
+
+    /**
+     * Return whether a given phone number is the configured voicemail number for a
+     * particular phone account.
+     *
+     * @param telecomManager the {@link TelecomManager} to use for checking the number.
+     * @param accountHandle The handle for the account to check the voicemail number against
+     * @param number The number to look up.
+     */
+    public static boolean isVoiceMailNumber(@Nullable TelecomManager telecomManager,
+            @Nullable PhoneAccountHandle accountHandle, @Nullable String number) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "isVoiceMailNumber",
+                        PhoneAccountHandle.class, String.class))) {
+            return telecomManager.isVoiceMailNumber(accountHandle, number);
+        }
+        return PhoneNumberUtils.isVoiceMailNumber(number);
+    }
+
+    /**
+     * Return the {@link PhoneAccount} for a specified {@link PhoneAccountHandle}. Object includes
+     * resources which can be used in a user interface.
+     *
+     * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+     * @param account The {@link PhoneAccountHandle}.
+     * @return The {@link PhoneAccount} object or null if it doesn't exist.
+     */
+    @Nullable
+    public static PhoneAccount getPhoneAccount(@Nullable TelecomManager telecomManager,
+            @Nullable PhoneAccountHandle accountHandle) {
+        if (telecomManager != null && (CompatUtils.isMethodAvailable(
+                TELECOM_MANAGER_CLASS, "getPhoneAccount", PhoneAccountHandle.class))) {
+            return telecomManager.getPhoneAccount(accountHandle);
+        }
+        return null;
+    }
+
+    /**
+     * Return the voicemail number for a given phone account.
+     *
+     * @param telecomManager The {@link TelecomManager} object to use for retrieving the voicemail
+     * number if accountHandle is specified.
+     * @param telephonyManager The {@link TelephonyManager} object to use for retrieving the
+     * voicemail number if accountHandle is null.
+     * @param accountHandle The handle for the phone account.
+     * @return The voicemail number for the phone account, and {@code null} if one has not been
+     *         configured.
+     */
+    @Nullable
+    public static String getVoiceMailNumber(@Nullable TelecomManager telecomManager,
+            @Nullable TelephonyManager telephonyManager,
+            @Nullable PhoneAccountHandle accountHandle) {
+        if (telecomManager != null && (CompatUtils.isMethodAvailable(
+                TELECOM_MANAGER_CLASS, "getVoiceMailNumber", PhoneAccountHandle.class))) {
+            return telecomManager.getVoiceMailNumber(accountHandle);
+        } else if (telephonyManager != null){
+            return telephonyManager.getVoiceMailNumber();
+        }
+        return null;
+    }
+
+    /**
+     * Processes the specified dial string as an MMI code.
+     * MMI codes are any sequence of characters entered into the dialpad that contain a "*" or "#".
+     * Some of these sequences launch special behavior through handled by Telephony.
+     *
+     * @param telecomManager The {@link TelecomManager} object to use for handling MMI.
+     * @param dialString The digits to dial.
+     * @return {@code true} if the digits were processed as an MMI code, {@code false} otherwise.
+     */
+    public static boolean handleMmi(@Nullable TelecomManager telecomManager,
+            @Nullable String dialString, @Nullable PhoneAccountHandle accountHandle) {
+        if (telecomManager == null || TextUtils.isEmpty(dialString)) {
+            return false;
+        }
+        if (CompatUtils.isMarshmallowCompatible()) {
+            return telecomManager.handleMmi(dialString, accountHandle);
+        }
+
+        Object handleMmiResult = CompatUtils.invokeMethod(
+                telecomManager,
+                "handleMmi",
+                new Class<?>[] {PhoneAccountHandle.class, String.class},
+                new Object[] {accountHandle, dialString});
+        if (handleMmiResult != null) {
+            return (boolean) handleMmiResult;
+        }
+
+        return telecomManager.handleMmi(dialString);
+    }
+
+    /**
+     * Silences the ringer if a ringing call exists. Noop if {@link TelecomManager#silenceRinger()}
+     * is unavailable.
+     *
+     * @param telecomManager the TelecomManager to use to silence the ringer.
+     */
+    public static void silenceRinger(@Nullable TelecomManager telecomManager) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible() || CompatUtils
+                .isMethodAvailable(TELECOM_MANAGER_CLASS, "silenceRinger"))) {
+            telecomManager.silenceRinger();
+        }
+    }
+
+    /**
+     * Returns the current SIM call manager. Apps must be prepared for this method to return null,
+     * indicating that there currently exists no registered SIM call manager.
+     *
+     * @param telecomManager the {@link TelecomManager} to use to fetch the SIM call manager.
+     * @return The phone account handle of the current sim call manager.
+     */
+    @Nullable
+    public static PhoneAccountHandle getSimCallManager(TelecomManager telecomManager) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible() || CompatUtils
+                .isMethodAvailable(TELECOM_MANAGER_CLASS, "getSimCallManager"))) {
+            return telecomManager.getSimCallManager();
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/contacts/common/database/ContactUpdateUtils.java b/src/com/android/contacts/common/database/ContactUpdateUtils.java
new file mode 100644
index 0000000..1bd08aa
--- /dev/null
+++ b/src/com/android/contacts/common/database/ContactUpdateUtils.java
@@ -0,0 +1,47 @@
+/*
+ * 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.database;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+/**
+ * Static methods to update contact information.
+ */
+public class ContactUpdateUtils {
+
+    private static final String TAG = ContactUpdateUtils.class.getSimpleName();
+
+    public static void setSuperPrimary(Context context, long dataId) {
+        if (dataId == -1) {
+            Log.e(TAG, "Invalid arguments for setSuperPrimary request");
+            return;
+        }
+
+        // Update the primary values in the data record.
+        ContentValues values = new ContentValues(2);
+        values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
+        values.put(ContactsContract.Data.IS_PRIMARY, 1);
+
+        context.getContentResolver().update(
+                ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId),
+                values, null, null);
+    }
+}
diff --git a/src/com/android/contacts/common/database/EmptyCursor.java b/src/com/android/contacts/common/database/EmptyCursor.java
new file mode 100644
index 0000000..ad00eff
--- /dev/null
+++ b/src/com/android/contacts/common/database/EmptyCursor.java
@@ -0,0 +1,84 @@
+/*
+* 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.database;
+
+import android.database.AbstractCursor;
+import android.database.CursorIndexOutOfBoundsException;
+
+/**
+ * A cursor that is empty.
+ * <p>
+ * If you want an empty cursor, this class is better than a MatrixCursor because it has less
+ * overhead.
+ */
+final public class EmptyCursor extends AbstractCursor {
+
+    private String[] mColumns;
+
+    public EmptyCursor(String[] columns) {
+        this.mColumns = columns;
+    }
+
+    @Override
+    public int getCount() {
+        return 0;
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        return mColumns;
+    }
+
+    @Override
+    public String getString(int column) {
+        throw cursorException();
+    }
+
+    @Override
+    public short getShort(int column) {
+        throw cursorException();
+    }
+
+    @Override
+    public int getInt(int column) {
+        throw cursorException();
+    }
+
+    @Override
+    public long getLong(int column) {
+        throw cursorException();
+    }
+
+    @Override
+    public float getFloat(int column) {
+        throw cursorException();
+    }
+
+    @Override
+    public double getDouble(int column) {
+        throw cursorException();
+    }
+
+    @Override
+    public boolean isNull(int column) {
+        throw cursorException();
+    }
+
+    private CursorIndexOutOfBoundsException cursorException() {
+        return new CursorIndexOutOfBoundsException("Operation not permitted on an empty cursor.");
+    }
+}
diff --git a/src/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java b/src/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java
new file mode 100644
index 0000000..aefc0fd
--- /dev/null
+++ b/src/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java
@@ -0,0 +1,69 @@
+/*
+ * 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.database;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * An {@AsyncQueryHandler} that will never return a null cursor.
+ * <p>
+ * Instead, will return a {@link Cursor} with 0 records.
+ */
+public abstract class NoNullCursorAsyncQueryHandler extends AsyncQueryHandler {
+
+    public NoNullCursorAsyncQueryHandler(ContentResolver cr) {
+        super(cr);
+    }
+
+    @Override
+    public void startQuery(int token, Object cookie, Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String orderBy) {
+        final CookieWithProjection projectionCookie = new CookieWithProjection(cookie, projection);
+        super.startQuery(token, projectionCookie, uri, projection, selection, selectionArgs,
+                orderBy);
+    }
+
+    @Override
+    protected final void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        CookieWithProjection projectionCookie = (CookieWithProjection) cookie;
+
+        super.onQueryComplete(token, projectionCookie.originalCookie, cursor);
+
+        if (cursor == null) {
+            cursor = new EmptyCursor(projectionCookie.projection);
+        }
+        onNotNullableQueryComplete(token, projectionCookie.originalCookie, cursor);
+    }
+
+    protected abstract void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor);
+
+    /**
+     * Class to add projection to an existing cookie.
+     */
+    private static class CookieWithProjection {
+        public final Object originalCookie;
+        public final String[] projection;
+
+        public CookieWithProjection(Object cookie, String[] projection) {
+            this.originalCookie = cookie;
+            this.projection = projection;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/dialog/CallSubjectDialog.java b/src/com/android/contacts/common/dialog/CallSubjectDialog.java
new file mode 100644
index 0000000..a17c4fc
--- /dev/null
+++ b/src/com/android/contacts/common/dialog/CallSubjectDialog.java
@@ -0,0 +1,623 @@
+/*
+ * Copyright (C) 2015 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.dialog;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.PhoneAccountSdkCompat;
+import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
+import com.android.contacts.common.util.UriUtils;
+import com.android.phone.common.animation.AnimUtils;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements a dialog which prompts for a call subject for an outgoing call.  The dialog includes
+ * a pop up list of historical call subjects.
+ */
+public class CallSubjectDialog extends Activity {
+    private static final String TAG = "CallSubjectDialog";
+    private static final int CALL_SUBJECT_LIMIT = 16;
+    private static final int CALL_SUBJECT_HISTORY_SIZE = 5;
+
+    private static final int REQUEST_SUBJECT = 1001;
+
+    public static final String PREF_KEY_SUBJECT_HISTORY_COUNT = "subject_history_count";
+    public static final String PREF_KEY_SUBJECT_HISTORY_ITEM = "subject_history_item";
+
+    /**
+     * Activity intent argument bundle keys:
+     */
+    public static final String ARG_PHOTO_ID = "PHOTO_ID";
+    public static final String ARG_PHOTO_URI = "PHOTO_URI";
+    public static final String ARG_CONTACT_URI = "CONTACT_URI";
+    public static final String ARG_NAME_OR_NUMBER = "NAME_OR_NUMBER";
+    public static final String ARG_IS_BUSINESS = "IS_BUSINESS";
+    public static final String ARG_NUMBER = "NUMBER";
+    public static final String ARG_DISPLAY_NUMBER = "DISPLAY_NUMBER";
+    public static final String ARG_NUMBER_LABEL = "NUMBER_LABEL";
+    public static final String ARG_PHONE_ACCOUNT_HANDLE = "PHONE_ACCOUNT_HANDLE";
+
+    private int mAnimationDuration;
+    private Charset mMessageEncoding;
+    private View mBackgroundView;
+    private View mDialogView;
+    private QuickContactBadge mContactPhoto;
+    private TextView mNameView;
+    private TextView mNumberView;
+    private EditText mCallSubjectView;
+    private TextView mCharacterLimitView;
+    private View mHistoryButton;
+    private View mSendAndCallButton;
+    private ListView mSubjectList;
+
+    private int mLimit = CALL_SUBJECT_LIMIT;
+    private int mPhotoSize;
+    private SharedPreferences mPrefs;
+    private List<String> mSubjectHistory;
+
+    private long mPhotoID;
+    private Uri mPhotoUri;
+    private Uri mContactUri;
+    private String mNameOrNumber;
+    private boolean mIsBusiness;
+    private String mNumber;
+    private String mDisplayNumber;
+    private String mNumberLabel;
+    private PhoneAccountHandle mPhoneAccountHandle;
+
+    /**
+     * Handles changes to the text in the subject box.  Ensures the character limit is updated.
+     */
+    private final TextWatcher mTextWatcher = new TextWatcher() {
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            // no-op
+        }
+
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+            updateCharacterLimit();
+        }
+
+        @Override
+        public void afterTextChanged(Editable s) {
+            // no-op
+        }
+    };
+
+    /**
+     * Click listener which handles user clicks outside of the dialog.
+     */
+    private View.OnClickListener mBackgroundListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            finish();
+        }
+    };
+
+    /**
+     * Handles displaying the list of past call subjects.
+     */
+    private final View.OnClickListener mHistoryOnClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            hideSoftKeyboard(CallSubjectDialog.this, mCallSubjectView);
+            showCallHistory(mSubjectList.getVisibility() == View.GONE);
+        }
+    };
+
+    /**
+     * Handles starting a call with a call subject specified.
+     */
+    private final View.OnClickListener mSendAndCallOnClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            String subject = mCallSubjectView.getText().toString();
+            Intent intent = CallUtil.getCallWithSubjectIntent(mNumber, mPhoneAccountHandle,
+                    subject);
+
+            TelecomManagerCompat.placeCall(
+                    CallSubjectDialog.this,
+                    (TelecomManager) getSystemService(Context.TELECOM_SERVICE),
+                    intent);
+
+            mSubjectHistory.add(subject);
+            saveSubjectHistory(mSubjectHistory);
+            finish();
+        }
+    };
+
+    /**
+     * Handles auto-hiding the call history when user clicks in the call subject field to give it
+     * focus.
+     */
+    private final View.OnClickListener mCallSubjectClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            if (mSubjectList.getVisibility() == View.VISIBLE) {
+                showCallHistory(false);
+            }
+        }
+    };
+
+    /**
+     * Item click listener which handles user clicks on the items in the list view.  Dismisses
+     * the activity, returning the subject to the caller and closing the activity with the
+     * {@link Activity#RESULT_OK} result code.
+     */
+    private AdapterView.OnItemClickListener mItemClickListener =
+            new AdapterView.OnItemClickListener() {
+                @Override
+                public void onItemClick(AdapterView<?> arg0, View view, int position, long arg3) {
+                    mCallSubjectView.setText(mSubjectHistory.get(position));
+                    showCallHistory(false);
+                }
+            };
+
+    /**
+     * Show the call subject dialog given a phone number to dial (e.g. from the dialpad).
+     *
+     * @param activity The activity.
+     * @param number The number to dial.
+     */
+    public static void start(Activity activity, String number) {
+        start(activity,
+                -1 /* photoId */,
+                null /* photoUri */,
+                null /* contactUri */,
+                number /* nameOrNumber */,
+                false /* isBusiness */,
+                number /* number */,
+                null /* displayNumber */,
+                null /* numberLabel */,
+                null /* phoneAccountHandle */);
+    }
+
+    /**
+     * Creates a call subject dialog.
+     *
+     * @param activity The current activity.
+     * @param photoId The photo ID (used to populate contact photo).
+     * @param photoUri The photo Uri (used to populate contact photo).
+     * @param contactUri The Contact URI (used so quick contact can be invoked from contact photo).
+     * @param nameOrNumber The name or number of the callee.
+     * @param isBusiness {@code true} if a business is being called (used for contact photo).
+     * @param number The raw number to dial.
+     * @param displayNumber The number to dial, formatted for display.
+     * @param numberLabel The label for the number (if from a contact).
+     * @param phoneAccountHandle The phone account handle.
+     */
+    public static void start(Activity activity, long photoId, Uri photoUri, Uri contactUri,
+            String nameOrNumber, boolean isBusiness, String number, String displayNumber,
+            String numberLabel, PhoneAccountHandle phoneAccountHandle) {
+        Bundle arguments = new Bundle();
+        arguments.putLong(ARG_PHOTO_ID, photoId);
+        arguments.putParcelable(ARG_PHOTO_URI, photoUri);
+        arguments.putParcelable(ARG_CONTACT_URI, contactUri);
+        arguments.putString(ARG_NAME_OR_NUMBER, nameOrNumber);
+        arguments.putBoolean(ARG_IS_BUSINESS, isBusiness);
+        arguments.putString(ARG_NUMBER, number);
+        arguments.putString(ARG_DISPLAY_NUMBER, displayNumber);
+        arguments.putString(ARG_NUMBER_LABEL, numberLabel);
+        arguments.putParcelable(ARG_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+        start(activity, arguments);
+    }
+
+    /**
+     * Shows the call subject dialog given a Bundle containing all the arguments required to
+     * display the dialog (e.g. from Quick Contacts).
+     *
+     * @param activity The activity.
+     * @param arguments The arguments bundle.
+     */
+    public static void start(Activity activity, Bundle arguments) {
+        Intent intent = new Intent(activity, CallSubjectDialog.class);
+        intent.putExtras(arguments);
+        activity.startActivity(intent);
+    }
+
+    /**
+     * Creates the dialog, inflating the layout and populating it with the name and phone number.
+     *
+     * @param savedInstanceState The last saved instance state of the Fragment,
+     * or null if this is a freshly created Fragment.
+     *
+     * @return Dialog instance.
+     */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mAnimationDuration = getResources().getInteger(R.integer.call_subject_animation_duration);
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+        mPhotoSize = getResources().getDimensionPixelSize(
+                R.dimen.call_subject_dialog_contact_photo_size);
+        readArguments();
+        loadConfiguration();
+        mSubjectHistory = loadSubjectHistory(mPrefs);
+
+        setContentView(R.layout.dialog_call_subject);
+        getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT);
+        mBackgroundView = findViewById(R.id.call_subject_dialog);
+        mBackgroundView.setOnClickListener(mBackgroundListener);
+        mDialogView = findViewById(R.id.dialog_view);
+        mContactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo);
+        mNameView = (TextView) findViewById(R.id.name);
+        mNumberView = (TextView) findViewById(R.id.number);
+        mCallSubjectView = (EditText) findViewById(R.id.call_subject);
+        mCallSubjectView.addTextChangedListener(mTextWatcher);
+        mCallSubjectView.setOnClickListener(mCallSubjectClickListener);
+        InputFilter[] filters = new InputFilter[1];
+        filters[0] = new InputFilter.LengthFilter(mLimit);
+        mCallSubjectView.setFilters(filters);
+        mCharacterLimitView = (TextView) findViewById(R.id.character_limit);
+        mHistoryButton = findViewById(R.id.history_button);
+        mHistoryButton.setOnClickListener(mHistoryOnClickListener);
+        mHistoryButton.setVisibility(mSubjectHistory.isEmpty() ? View.GONE : View.VISIBLE);
+        mSendAndCallButton = findViewById(R.id.send_and_call_button);
+        mSendAndCallButton.setOnClickListener(mSendAndCallOnClickListener);
+        mSubjectList = (ListView) findViewById(R.id.subject_list);
+        mSubjectList.setOnItemClickListener(mItemClickListener);
+        mSubjectList.setVisibility(View.GONE);
+
+        updateContactInfo();
+        updateCharacterLimit();
+    }
+
+    /**
+     * Populates the contact info fields based on the current contact information.
+     */
+    private void updateContactInfo() {
+        if (mContactUri != null) {
+            setPhoto(mPhotoID, mPhotoUri, mContactUri, mNameOrNumber, mIsBusiness);
+        } else {
+            mContactPhoto.setVisibility(View.GONE);
+        }
+        mNameView.setText(mNameOrNumber);
+        if (!TextUtils.isEmpty(mNumberLabel) && !TextUtils.isEmpty(mDisplayNumber)) {
+            mNumberView.setVisibility(View.VISIBLE);
+            mNumberView.setText(getString(R.string.call_subject_type_and_number,
+                    mNumberLabel, mDisplayNumber));
+        } else {
+            mNumberView.setVisibility(View.GONE);
+            mNumberView.setText(null);
+        }
+    }
+
+    /**
+     * Reads arguments from the fragment arguments and populates the necessary instance variables.
+     */
+    private void readArguments() {
+        Bundle arguments = getIntent().getExtras();
+        if (arguments == null) {
+            Log.e(TAG, "Arguments cannot be null.");
+            return;
+        }
+        mPhotoID = arguments.getLong(ARG_PHOTO_ID);
+        mPhotoUri = arguments.getParcelable(ARG_PHOTO_URI);
+        mContactUri = arguments.getParcelable(ARG_CONTACT_URI);
+        mNameOrNumber = arguments.getString(ARG_NAME_OR_NUMBER);
+        mIsBusiness = arguments.getBoolean(ARG_IS_BUSINESS);
+        mNumber = arguments.getString(ARG_NUMBER);
+        mDisplayNumber = arguments.getString(ARG_DISPLAY_NUMBER);
+        mNumberLabel = arguments.getString(ARG_NUMBER_LABEL);
+        mPhoneAccountHandle = arguments.getParcelable(ARG_PHONE_ACCOUNT_HANDLE);
+    }
+
+    /**
+     * Updates the character limit display, coloring the text RED when the limit is reached or
+     * exceeded.
+     */
+    private void updateCharacterLimit() {
+        String subjectText = mCallSubjectView.getText().toString();
+        final int length;
+
+        // If a message encoding is specified, use that to count bytes in the message.
+        if (mMessageEncoding != null) {
+            length = subjectText.getBytes(mMessageEncoding).length;
+        } else {
+            // No message encoding specified, so just count characters entered.
+            length = subjectText.length();
+        }
+
+        mCharacterLimitView.setText(
+                getString(R.string.call_subject_limit, length, mLimit));
+        if (length >= mLimit) {
+            mCharacterLimitView.setTextColor(getResources().getColor(
+                    R.color.call_subject_limit_exceeded));
+        } else {
+            mCharacterLimitView.setTextColor(getResources().getColor(
+                    R.color.dialtacts_secondary_text_color));
+        }
+    }
+
+    /**
+     * Sets the photo on the quick contact photo.
+     *
+     * @param photoId
+     * @param photoUri
+     * @param contactUri
+     * @param displayName
+     * @param isBusiness
+     */
+    private void setPhoto(long photoId, Uri photoUri, Uri contactUri, String displayName,
+            boolean isBusiness) {
+        mContactPhoto.assignContactUri(contactUri);
+        if (CompatUtils.isLollipopCompatible()) {
+            mContactPhoto.setOverlay(null);
+        }
+
+        int contactType;
+        if (isBusiness) {
+            contactType = ContactPhotoManager.TYPE_BUSINESS;
+        } else {
+            contactType = ContactPhotoManager.TYPE_DEFAULT;
+        }
+
+        String lookupKey = null;
+        if (contactUri != null) {
+            lookupKey = UriUtils.getLookupKeyFromUri(contactUri);
+        }
+
+        ContactPhotoManager.DefaultImageRequest
+                request = new ContactPhotoManager.DefaultImageRequest(
+                displayName, lookupKey, contactType, true /* isCircular */);
+
+        if (photoId == 0 && photoUri != null) {
+            ContactPhotoManager.getInstance(this).loadPhoto(mContactPhoto, photoUri,
+                    mPhotoSize, false /* darkTheme */, true /* isCircular */, request);
+        } else {
+            ContactPhotoManager.getInstance(this).loadThumbnail(mContactPhoto, photoId,
+                    false /* darkTheme */, true /* isCircular */, request);
+        }
+    }
+
+    /**
+     * Loads the subject history from shared preferences.
+     *
+     * @param prefs Shared preferences.
+     * @return List of subject history strings.
+     */
+    public static List<String> loadSubjectHistory(SharedPreferences prefs) {
+        int historySize = prefs.getInt(PREF_KEY_SUBJECT_HISTORY_COUNT, 0);
+        List<String> subjects = new ArrayList(historySize);
+
+        for (int ix = 0 ; ix < historySize; ix++) {
+            String historyItem = prefs.getString(PREF_KEY_SUBJECT_HISTORY_ITEM + ix, null);
+            if (!TextUtils.isEmpty(historyItem)) {
+                subjects.add(historyItem);
+            }
+        }
+
+        return subjects;
+    }
+
+    /**
+     * Saves the subject history list to shared prefs, removing older items so that there are only
+     * {@link #CALL_SUBJECT_HISTORY_SIZE} items at most.
+     *
+     * @param history The history.
+     */
+    private void saveSubjectHistory(List<String> history) {
+        // Remove oldest subject(s).
+        while (history.size() > CALL_SUBJECT_HISTORY_SIZE) {
+            history.remove(0);
+        }
+
+        SharedPreferences.Editor editor = mPrefs.edit();
+        int historyCount = 0;
+        for (String subject : history) {
+            if (!TextUtils.isEmpty(subject)) {
+                editor.putString(PREF_KEY_SUBJECT_HISTORY_ITEM + historyCount,
+                        subject);
+                historyCount++;
+            }
+        }
+        editor.putInt(PREF_KEY_SUBJECT_HISTORY_COUNT, historyCount);
+        editor.apply();
+    }
+
+    /**
+     * Hide software keyboard for the given {@link View}.
+     */
+    public void hideSoftKeyboard(Context context, View view) {
+        InputMethodManager imm = (InputMethodManager) context.getSystemService(
+                Context.INPUT_METHOD_SERVICE);
+        if (imm != null) {
+            imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
+        }
+    }
+
+    /**
+     * Hides or shows the call history list.
+     *
+     * @param show {@code true} if the call history should be shown, {@code false} otherwise.
+     */
+    private void showCallHistory(final boolean show) {
+        // Bail early if the visibility has not changed.
+        if ((show && mSubjectList.getVisibility() == View.VISIBLE) ||
+                (!show && mSubjectList.getVisibility() == View.GONE)) {
+            return;
+        }
+
+        final int dialogStartingBottom = mDialogView.getBottom();
+        if (show) {
+            // Showing the subject list; bind the list of history items to the list and show it.
+            ArrayAdapter<String> adapter = new ArrayAdapter<String>(CallSubjectDialog.this,
+                    R.layout.call_subject_history_list_item, mSubjectHistory);
+            mSubjectList.setAdapter(adapter);
+            mSubjectList.setVisibility(View.VISIBLE);
+        } else {
+            // Hiding the subject list.
+            mSubjectList.setVisibility(View.GONE);
+        }
+
+        // Use a ViewTreeObserver so that we can animate between the pre-layout and post-layout
+        // states.
+        final ViewTreeObserver observer = mBackgroundView.getViewTreeObserver();
+        observer.addOnPreDrawListener(
+                new ViewTreeObserver.OnPreDrawListener() {
+                    @Override
+                    public boolean onPreDraw() {
+                        // We don't want to continue getting called.
+                        if (observer.isAlive()) {
+                            observer.removeOnPreDrawListener(this);
+                        }
+
+                        // Determine the amount the dialog has shifted due to the relayout.
+                        int shiftAmount = dialogStartingBottom - mDialogView.getBottom();
+
+                        // If the dialog needs to be shifted, do that now.
+                        if (shiftAmount != 0) {
+                            // Start animation in translated state and animate to translationY 0.
+                            mDialogView.setTranslationY(shiftAmount);
+                            mDialogView.animate()
+                                    .translationY(0)
+                                    .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+                                    .setDuration(mAnimationDuration)
+                                    .start();
+                        }
+
+                        if (show) {
+                            // Show the subhect list.
+                            mSubjectList.setTranslationY(mSubjectList.getHeight());
+
+                            mSubjectList.animate()
+                                    .translationY(0)
+                                    .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+                                    .setDuration(mAnimationDuration)
+                                    .setListener(new AnimatorListenerAdapter() {
+                                        @Override
+                                        public void onAnimationEnd(Animator animation) {
+                                            super.onAnimationEnd(animation);
+                                        }
+
+                                        @Override
+                                        public void onAnimationStart(Animator animation) {
+                                            super.onAnimationStart(animation);
+                                            mSubjectList.setVisibility(View.VISIBLE);
+                                        }
+                                    })
+                                    .start();
+                        } else {
+                            // Hide the subject list.
+                            mSubjectList.setTranslationY(0);
+
+                            mSubjectList.animate()
+                                    .translationY(mSubjectList.getHeight())
+                                    .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+                                    .setDuration(mAnimationDuration)
+                                    .setListener(new AnimatorListenerAdapter() {
+                                        @Override
+                                        public void onAnimationEnd(Animator animation) {
+                                            super.onAnimationEnd(animation);
+                                            mSubjectList.setVisibility(View.GONE);
+                                        }
+
+                                        @Override
+                                        public void onAnimationStart(Animator animation) {
+                                            super.onAnimationStart(animation);
+                                        }
+                                    })
+                                    .start();
+                        }
+                        return true;
+                    }
+                }
+        );
+    }
+
+    /**
+     * Loads the message encoding and maximum message length from the phone account extras for the
+     * current phone account.
+     */
+    private void loadConfiguration() {
+        // Only attempt to load configuration from the phone account extras if the SDK is N or
+        // later.  If we've got a prior SDK the default encoding and message length will suffice.
+        int sdk = android.os.Build.VERSION.SDK_INT;
+        if(sdk <= android.os.Build.VERSION_CODES.M) {
+            return;
+        }
+
+        if (mPhoneAccountHandle == null) {
+            return;
+        }
+
+        TelecomManager telecomManager =
+                (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
+        final PhoneAccount account = telecomManager.getPhoneAccount(mPhoneAccountHandle);
+
+        Bundle phoneAccountExtras = PhoneAccountSdkCompat.getExtras(account);
+        if (phoneAccountExtras == null) {
+            return;
+        }
+
+        // Get limit, if provided; otherwise default to existing value.
+        mLimit = phoneAccountExtras
+                .getInt(PhoneAccountSdkCompat.EXTRA_CALL_SUBJECT_MAX_LENGTH, mLimit);
+
+        // Get charset; default to none (e.g. count characters 1:1).
+        String charsetName = phoneAccountExtras.getString(
+                PhoneAccountSdkCompat.EXTRA_CALL_SUBJECT_CHARACTER_ENCODING);
+
+        if (!TextUtils.isEmpty(charsetName)) {
+            try {
+                mMessageEncoding = Charset.forName(charsetName);
+            } catch (java.nio.charset.UnsupportedCharsetException uce) {
+                // Character set was invalid; log warning and fallback to none.
+                Log.w(TAG, "Invalid charset: " + charsetName);
+                mMessageEncoding = null;
+            }
+        } else {
+            // No character set specified, so count characters 1:1.
+            mMessageEncoding = null;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/dialog/ClearFrequentsDialog.java b/src/com/android/contacts/common/dialog/ClearFrequentsDialog.java
new file mode 100644
index 0000000..2fab3e1
--- /dev/null
+++ b/src/com/android/contacts/common/dialog/ClearFrequentsDialog.java
@@ -0,0 +1,81 @@
+/*
+ * 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.dialog;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.PermissionsUtil;
+
+/**
+ * Dialog that clears the frequently contacted list after confirming with the user.
+ */
+public class ClearFrequentsDialog extends DialogFragment {
+    /** Preferred way to show this dialog */
+    public static void show(FragmentManager fragmentManager) {
+        ClearFrequentsDialog dialog = new ClearFrequentsDialog();
+        dialog.show(fragmentManager, "clearFrequents");
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final Context context = getActivity().getApplicationContext();
+        final ContentResolver resolver = getActivity().getContentResolver();
+        final OnClickListener okListener = new OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                if (!PermissionsUtil.hasContactsPermissions(context)) {
+                    return;
+                }
+                final IndeterminateProgressDialog progressDialog = IndeterminateProgressDialog.show(
+                        getFragmentManager(), getString(R.string.clearFrequentsProgress_title),
+                        null, 500);
+                final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
+                    @Override
+                    protected Void doInBackground(Void... params) {
+                        resolver.delete(ContactsContract.DataUsageFeedback.DELETE_USAGE_URI,
+                                null, null);
+                        return null;
+                    }
+
+                    @Override
+                    protected void onPostExecute(Void result) {
+                        progressDialog.dismiss();
+                    }
+                };
+                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+            }
+        };
+        return new AlertDialog.Builder(getActivity())
+            .setTitle(R.string.clearFrequentsConfirmation_title)
+            .setMessage(R.string.clearFrequentsConfirmation)
+            .setNegativeButton(android.R.string.cancel, null)
+            .setPositiveButton(android.R.string.ok, okListener)
+            .setCancelable(true)
+            .create();
+    }
+}
diff --git a/src/com/android/contacts/common/dialog/IndeterminateProgressDialog.java b/src/com/android/contacts/common/dialog/IndeterminateProgressDialog.java
new file mode 100644
index 0000000..2fe059f
--- /dev/null
+++ b/src/com/android/contacts/common/dialog/IndeterminateProgressDialog.java
@@ -0,0 +1,208 @@
+/*
+ * 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.dialog;
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Handler;
+
+/**
+ * Indeterminate progress dialog wrapped up in a DialogFragment to work even when the device
+ * orientation is changed. Currently, only supports adding a title and/or message to the progress
+ * dialog.  There is an additional parameter of the minimum amount of time to display the progress
+ * dialog even after a call to dismiss the dialog {@link #dismiss()} or
+ * {@link #dismissAllowingStateLoss()}.
+ * <p>
+ * To create and show the progress dialog, use
+ * {@link #show(FragmentManager, CharSequence, CharSequence, long)} and retain the reference to the
+ * IndeterminateProgressDialog instance.
+ * <p>
+ * To dismiss the dialog, use {@link #dismiss()} or {@link #dismissAllowingStateLoss()} on the
+ * instance.  The instance returned by
+ * {@link #show(FragmentManager, CharSequence, CharSequence, long)} is guaranteed to be valid
+ * after a device orientation change because the {@link #setRetainInstance(boolean)} is called
+ * internally with true.
+ */
+public class IndeterminateProgressDialog extends DialogFragment {
+    private static final String TAG = IndeterminateProgressDialog.class.getSimpleName();
+
+    private CharSequence mTitle;
+    private CharSequence mMessage;
+    private long mMinDisplayTime;
+    private long mShowTime = 0;
+    private boolean mActivityReady = false;
+    private Dialog mOldDialog;
+    private final Handler mHandler = new Handler();
+    private boolean mCalledSuperDismiss = false;
+    private boolean mAllowStateLoss;
+    private final Runnable mDismisser = new Runnable() {
+        @Override
+        public void run() {
+            superDismiss();
+        }
+    };
+
+    /**
+     * Creates and shows an indeterminate progress dialog.  Once the progress dialog is shown, it
+     * will be shown for at least the minDisplayTime (in milliseconds), so that the progress dialog
+     * does not flash in and out to quickly.
+     */
+    public static IndeterminateProgressDialog show(FragmentManager fragmentManager,
+            CharSequence title, CharSequence message, long minDisplayTime) {
+        IndeterminateProgressDialog dialogFragment = new IndeterminateProgressDialog();
+        dialogFragment.mTitle = title;
+        dialogFragment.mMessage = message;
+        dialogFragment.mMinDisplayTime = minDisplayTime;
+        dialogFragment.show(fragmentManager, TAG);
+        dialogFragment.mShowTime = System.currentTimeMillis();
+        dialogFragment.setCancelable(false);
+
+        return dialogFragment;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setRetainInstance(true);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        // Create the progress dialog and set its properties
+        final ProgressDialog dialog = new ProgressDialog(getActivity());
+        dialog.setIndeterminate(true);
+        dialog.setIndeterminateDrawable(null);
+        dialog.setTitle(mTitle);
+        dialog.setMessage(mMessage);
+
+        return dialog;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        mActivityReady = true;
+
+        // Check if superDismiss() had been called before.  This can happen if in a long
+        // running operation, the user hits the home button and closes this fragment's activity.
+        // Upon returning, we want to dismiss this progress dialog fragment.
+        if (mCalledSuperDismiss) {
+            superDismiss();
+        }
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        mActivityReady = false;
+    }
+
+    /**
+     * There is a race condition that is not handled properly by the DialogFragment class.
+     * If we don't check that this onDismiss callback isn't for the old progress dialog from before
+     * the device orientation change, then this will cause the newly created dialog after the
+     * orientation change to be dismissed immediately.
+     */
+    @Override
+    public void onDismiss(DialogInterface dialog) {
+        if (mOldDialog != null && mOldDialog == dialog) {
+            // This is the callback from the old progress dialog that was already dismissed before
+            // the device orientation change, so just ignore it.
+            return;
+        }
+        super.onDismiss(dialog);
+    }
+
+    /**
+     * Save the old dialog that is about to get destroyed in case this is due to a change
+     * in device orientation.  This will allow us to intercept the callback to
+     * {@link #onDismiss(DialogInterface)} in case the callback happens after a new progress dialog
+     * instance was created.
+     */
+    @Override
+    public void onDestroyView() {
+        mOldDialog = getDialog();
+        super.onDestroyView();
+    }
+
+    /**
+     * This tells the progress dialog to dismiss itself after guaranteeing to be shown for the
+     * specified time in {@link #show(FragmentManager, CharSequence, CharSequence, long)}.
+     */
+    @Override
+    public void dismiss() {
+        mAllowStateLoss = false;
+        dismissWhenReady();
+    }
+
+    /**
+     * This tells the progress dialog to dismiss itself (with state loss) after guaranteeing to be
+     * shown for the specified time in
+     * {@link #show(FragmentManager, CharSequence, CharSequence, long)}.
+     */
+    @Override
+    public void dismissAllowingStateLoss() {
+        mAllowStateLoss = true;
+        dismissWhenReady();
+    }
+
+    /**
+     * Tells the progress dialog to dismiss itself after guaranteeing that the dialog had been
+     * showing for at least the minimum display time as set in
+     * {@link #show(FragmentManager, CharSequence, CharSequence, long)}.
+     */
+    private void dismissWhenReady() {
+        // Compute how long the dialog has been showing
+        final long shownTime = System.currentTimeMillis() - mShowTime;
+        if (shownTime >= mMinDisplayTime) {
+            // dismiss immediately
+            mHandler.post(mDismisser);
+        } else {
+            // Need to wait some more, so compute the amount of time to sleep.
+            final long sleepTime = mMinDisplayTime - shownTime;
+            mHandler.postDelayed(mDismisser, sleepTime);
+        }
+    }
+
+    /**
+     * Actually dismiss the dialog fragment.
+     */
+    private void superDismiss() {
+        mCalledSuperDismiss = true;
+        if (mActivityReady) {
+            // The fragment is either in onStart or past it, but has not gotten to onStop yet.
+            // It is safe to dismiss this dialog fragment.
+            if (mAllowStateLoss) {
+                super.dismissAllowingStateLoss();
+            } else {
+                super.dismiss();
+            }
+        }
+        // If mActivityReady is false, then this dialog fragment has already passed the onStop
+        // state. This can happen if the user hit the 'home' button before this dialog fragment was
+        // dismissed or if there is a configuration change.
+        // In the event that this dialog fragment is re-attached and reaches onStart (e.g.,
+        // because the user returns to this fragment's activity or the device configuration change
+        // has re-attached this dialog fragment), because the mCalledSuperDismiss flag was set to
+        // true, this dialog fragment will be dismissed within onStart.  So, there's nothing else
+        // that needs to be done.
+    }
+}
diff --git a/src/com/android/contacts/common/editor/SelectAccountDialogFragment.java b/src/com/android/contacts/common/editor/SelectAccountDialogFragment.java
new file mode 100644
index 0000000..96da89a
--- /dev/null
+++ b/src/com/android/contacts/common/editor/SelectAccountDialogFragment.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.editor;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.AccountsListAdapter;
+import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
+
+/**
+ * Shows a dialog asking the user which account to chose.
+ *
+ * The result is passed to {@code targetFragment} passed to {@link #show}.
+ */
+public final class SelectAccountDialogFragment extends DialogFragment {
+    public static final String TAG = "SelectAccountDialogFragment";
+
+    private static final String KEY_TITLE_RES_ID = "title_res_id";
+    private static final String KEY_LIST_FILTER = "list_filter";
+    private static final String KEY_EXTRA_ARGS = "extra_args";
+
+    public SelectAccountDialogFragment() { // All fragments must have a public default constructor.
+    }
+
+    /**
+     * Show the dialog.
+     *
+     * @param fragmentManager {@link FragmentManager}.
+     * @param targetFragment {@link Fragment} that implements {@link Listener}.
+     * @param titleResourceId resource ID to use as the title.
+     * @param accountListFilter account filter.
+     * @param extraArgs Extra arguments, which will later be passed to
+     *     {@link Listener#onAccountChosen}.  {@code null} will be converted to
+     *     {@link Bundle#EMPTY}.
+     */
+    public static <F extends Fragment & Listener> void show(FragmentManager fragmentManager,
+            F targetFragment, int titleResourceId,
+            AccountListFilter accountListFilter, Bundle extraArgs) {
+        show(fragmentManager, targetFragment, titleResourceId, accountListFilter, extraArgs,
+                /* tag */ null);
+    }
+
+    public static <F extends Fragment & Listener> void show(FragmentManager fragmentManager,
+            F targetFragment, int titleResourceId,
+            AccountListFilter accountListFilter, Bundle extraArgs, String tag) {
+        final Bundle args = new Bundle();
+        args.putInt(KEY_TITLE_RES_ID, titleResourceId);
+        args.putSerializable(KEY_LIST_FILTER, accountListFilter);
+        args.putBundle(KEY_EXTRA_ARGS, (extraArgs == null) ? Bundle.EMPTY : extraArgs);
+
+        final SelectAccountDialogFragment instance = new SelectAccountDialogFragment();
+        instance.setArguments(args);
+        if (targetFragment != null) {
+            instance.setTargetFragment(targetFragment, 0);
+        }
+        instance.show(fragmentManager, tag);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        final Bundle args = getArguments();
+
+        final AccountListFilter filter = (AccountListFilter) args.getSerializable(KEY_LIST_FILTER);
+        final AccountsListAdapter accountAdapter = new AccountsListAdapter(builder.getContext(),
+                filter);
+        accountAdapter.setCustomLayout(R.layout.account_selector_list_item_condensed);
+
+        final DialogInterface.OnClickListener clickListener =
+                new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                dialog.dismiss();
+
+                onAccountSelected(accountAdapter.getItem(which));
+            }
+        };
+
+        final TextView title = (TextView) View.inflate(getActivity(), R.layout.dialog_title, null);
+        title.setText(args.getInt(KEY_TITLE_RES_ID));
+        builder.setCustomTitle(title);
+        builder.setSingleChoiceItems(accountAdapter, 0, clickListener);
+        final AlertDialog result = builder.create();
+        return result;
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        super.onCancel(dialog);
+        final Listener listener = getListener();
+        if (listener != null) {
+            listener.onAccountSelectorCancelled();
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle b) {
+        setTargetFragment(null, -1);
+        super.onSaveInstanceState(b);
+    }
+
+    /**
+     * Calls {@link Listener#onAccountChosen} of {@code targetFragment}.
+     */
+    private void onAccountSelected(AccountWithDataSet account) {
+        final Listener listener = getListener();
+        if (listener != null) {
+            listener.onAccountChosen(account, getArguments().getBundle(KEY_EXTRA_ARGS));
+        }
+    }
+
+    private Listener getListener() {
+        Listener listener = null;
+        final Fragment targetFragment = getTargetFragment();
+        if (targetFragment == null) {
+            final Activity activity = getActivity();
+            if (activity != null && activity instanceof Listener) {
+                listener = (Listener) activity;
+            }
+        } else if (targetFragment instanceof Listener) {
+            listener = (Listener) targetFragment;
+        }
+        return listener;
+    }
+
+    public interface Listener {
+        void onAccountChosen(AccountWithDataSet account, Bundle extraArgs);
+        void onAccountSelectorCancelled();
+    }
+}
diff --git a/src/com/android/contacts/common/extensions/ExtendedPhoneDirectoriesManager.java b/src/com/android/contacts/common/extensions/ExtendedPhoneDirectoriesManager.java
new file mode 100644
index 0000000..eb25934
--- /dev/null
+++ b/src/com/android/contacts/common/extensions/ExtendedPhoneDirectoriesManager.java
@@ -0,0 +1,26 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+
+package com.android.contacts.common.extensions;
+
+import android.content.Context;
+
+import com.android.contacts.common.list.DirectoryPartition;
+
+import java.util.List;
+
+/**
+ * An interface for adding extended phone directories to
+ * {@link com.android.contacts.common.list.PhoneNumberListAdapter}.
+ * An app that wishes to add custom phone directories should implement this class and advertise it
+ * in assets/contacts_extensions.properties. {@link ExtensionsFactory} will load the implementation
+ * and the extended directories will be added by
+ * {@link com.android.contacts.common.list.PhoneNumberListAdapter}.
+ */
+public interface ExtendedPhoneDirectoriesManager {
+
+    /**
+     * Return a list of extended directories to add. May return null if no directories are to be
+     * added.
+     */
+    List<DirectoryPartition> getExtendedDirectories(Context context);
+}
diff --git a/src/com/android/contacts/common/extensions/ExtensionsFactory.java b/src/com/android/contacts/common/extensions/ExtensionsFactory.java
new file mode 100644
index 0000000..d52429e
--- /dev/null
+++ b/src/com/android/contacts/common/extensions/ExtensionsFactory.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2013 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.extensions;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+
+/*
+ * A framework for adding extensions to Dialer. This class reads a property file from
+ * assets/contacts_extensions.properties and loads extension classes that an app has defined. If
+ * an extension class was not defined, null is returned.
+ */
+public class ExtensionsFactory {
+
+    private static String TAG = "ExtensionsFactory";
+
+    // Config filename for mappings of various class names to their custom
+    // implementations.
+    private static final String EXTENSIONS_PROPERTIES = "contacts_extensions.properties";
+
+    private static final String EXTENDED_PHONE_DIRECTORIES_KEY = "extendedPhoneDirectories";
+
+    private static Properties sProperties = null;
+    private static ExtendedPhoneDirectoriesManager mExtendedPhoneDirectoriesManager = null;
+
+    public static void init(Context context) {
+        if (sProperties != null) {
+            return;
+        }
+        try {
+            final InputStream fileStream = context.getAssets().open(EXTENSIONS_PROPERTIES);
+            sProperties = new Properties();
+            sProperties.load(fileStream);
+            fileStream.close();
+
+            final String className = sProperties.getProperty(EXTENDED_PHONE_DIRECTORIES_KEY);
+            if (className != null) {
+                mExtendedPhoneDirectoriesManager = createInstance(className);
+            } else {
+                Log.d(TAG, EXTENDED_PHONE_DIRECTORIES_KEY + " not found in properties file.");
+            }
+
+        } catch (FileNotFoundException e) {
+            // No custom extensions. Ignore.
+            Log.d(TAG, "No custom extensions.");
+        } catch (IOException e) {
+            Log.d(TAG, e.toString());
+        }
+    }
+
+    private static <T> T createInstance(String className) {
+        try {
+            Class<?> c = Class.forName(className);
+            //noinspection unchecked
+            return (T) c.newInstance();
+        } catch (ClassNotFoundException e) {
+            Log.e(TAG, className + ": unable to create instance.", e);
+        } catch (IllegalAccessException e) {
+            Log.e(TAG, className + ": unable to create instance.", e);
+        } catch (InstantiationException e) {
+            Log.e(TAG, className + ": unable to create instance.", e);
+        }
+        return null;
+    }
+
+    public static ExtendedPhoneDirectoriesManager getExtendedPhoneDirectoriesManager() {
+        return mExtendedPhoneDirectoriesManager;
+    }
+}
diff --git a/src/com/android/contacts/common/format/FormatUtils.java b/src/com/android/contacts/common/format/FormatUtils.java
new file mode 100644
index 0000000..376ff13
--- /dev/null
+++ b/src/com/android/contacts/common/format/FormatUtils.java
@@ -0,0 +1,184 @@
+/*
+ * 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.format;
+
+import android.database.CharArrayBuffer;
+import android.graphics.Typeface;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+
+/**
+ * Assorted utility methods related to text formatting in Contacts.
+ */
+public class FormatUtils {
+
+    /**
+     * Finds the earliest point in buffer1 at which the first part of buffer2 matches.  For example,
+     * overlapPoint("abcd", "cdef") == 2.
+     */
+    public static int overlapPoint(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
+        if (buffer1 == null || buffer2 == null) {
+            return -1;
+        }
+        return overlapPoint(Arrays.copyOfRange(buffer1.data, 0, buffer1.sizeCopied),
+                Arrays.copyOfRange(buffer2.data, 0, buffer2.sizeCopied));
+    }
+
+    /**
+     * Finds the earliest point in string1 at which the first part of string2 matches.  For example,
+     * overlapPoint("abcd", "cdef") == 2.
+     */
+    @VisibleForTesting
+    public static int overlapPoint(String string1, String string2) {
+        if (string1 == null || string2 == null) {
+            return -1;
+        }
+        return overlapPoint(string1.toCharArray(), string2.toCharArray());
+    }
+
+    /**
+     * Finds the earliest point in array1 at which the first part of array2 matches.  For example,
+     * overlapPoint("abcd", "cdef") == 2.
+     */
+    public static int overlapPoint(char[] array1, char[] array2) {
+        if (array1 == null || array2 == null) {
+            return -1;
+        }
+        int count1 = array1.length;
+        int count2 = array2.length;
+
+        // Ignore matching tails of the two arrays.
+        while (count1 > 0 && count2 > 0 && array1[count1 - 1] == array2[count2 - 1]) {
+            count1--;
+            count2--;
+        }
+
+        int size = count2;
+        for (int i = 0; i < count1; i++) {
+            if (i + size > count1) {
+                size = count1 - i;
+            }
+            int j;
+            for (j = 0; j < size; j++) {
+                if (array1[i+j] != array2[j]) {
+                    break;
+                }
+            }
+            if (j == size) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * Applies the given style to a range of the input CharSequence.
+     * @param style The style to apply (see the style constants in {@link Typeface}).
+     * @param input The CharSequence to style.
+     * @param start Starting index of the range to style (will be clamped to be a minimum of 0).
+     * @param end Ending index of the range to style (will be clamped to a maximum of the input
+     *     length).
+     * @param flags Bitmask for configuring behavior of the span.  See {@link android.text.Spanned}.
+     * @return The styled CharSequence.
+     */
+    public static CharSequence applyStyleToSpan(int style, CharSequence input, int start, int end,
+            int flags) {
+        // Enforce bounds of the char sequence.
+        start = Math.max(0, start);
+        end = Math.min(input.length(), end);
+        SpannableString text = new SpannableString(input);
+        text.setSpan(new StyleSpan(style), start, end, flags);
+        return text;
+    }
+
+    @VisibleForTesting
+    public static void copyToCharArrayBuffer(String text, CharArrayBuffer buffer) {
+        if (text != null) {
+            char[] data = buffer.data;
+            if (data == null || data.length < text.length()) {
+                buffer.data = text.toCharArray();
+            } else {
+                text.getChars(0, text.length(), data, 0);
+            }
+            buffer.sizeCopied = text.length();
+        } else {
+            buffer.sizeCopied = 0;
+        }
+    }
+
+    /** Returns a String that represents the content of the given {@link CharArrayBuffer}. */
+    @VisibleForTesting
+    public static String charArrayBufferToString(CharArrayBuffer buffer) {
+        return new String(buffer.data, 0, buffer.sizeCopied);
+    }
+
+    /**
+     * Finds the index of the first word that starts with the given prefix.
+     * <p>
+     * If not found, returns -1.
+     *
+     * @param text the text in which to search for the prefix
+     * @param prefix the text to find, in upper case letters
+     */
+    public static int indexOfWordPrefix(CharSequence text, String prefix) {
+        if (prefix == null || text == null) {
+            return -1;
+        }
+
+        int textLength = text.length();
+        int prefixLength = prefix.length();
+
+        if (prefixLength == 0 || textLength < prefixLength) {
+            return -1;
+        }
+
+        int i = 0;
+        while (i < textLength) {
+            // Skip non-word characters
+            while (i < textLength && !Character.isLetterOrDigit(text.charAt(i))) {
+                i++;
+            }
+
+            if (i + prefixLength > textLength) {
+                return -1;
+            }
+
+            // Compare the prefixes
+            int j;
+            for (j = 0; j < prefixLength; j++) {
+                if (Character.toUpperCase(text.charAt(i + j)) != prefix.charAt(j)) {
+                    break;
+                }
+            }
+            if (j == prefixLength) {
+                return i;
+            }
+
+            // Skip this word
+            while (i < textLength && Character.isLetterOrDigit(text.charAt(i))) {
+                i++;
+            }
+        }
+
+        return -1;
+    }
+
+}
diff --git a/src/com/android/contacts/common/format/SpannedTestUtils.java b/src/com/android/contacts/common/format/SpannedTestUtils.java
new file mode 100644
index 0000000..463d7a8
--- /dev/null
+++ b/src/com/android/contacts/common/format/SpannedTestUtils.java
@@ -0,0 +1,88 @@
+/*
+ * 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.format;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.Html;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
+import android.widget.TextView;
+
+import junit.framework.Assert;
+
+/**
+ * Utility class to check the value of spanned text in text views.
+ */
+@SmallTest
+public class SpannedTestUtils {
+    /**
+     * Checks that the text contained in the text view matches the given HTML text.
+     *
+     * @param expectedHtmlText the expected text to be in the text view
+     * @param textView the text view from which to get the text
+     */
+    public static void checkHtmlText(String expectedHtmlText, TextView textView) {
+        String actualHtmlText = Html.toHtml((Spanned) textView.getText());
+        if (TextUtils.isEmpty(expectedHtmlText)) {
+            // If the text is empty, it does not add the <p></p> bits to it.
+            Assert.assertEquals("", actualHtmlText);
+        } else {
+            Assert.assertEquals("<p dir=ltr>" + expectedHtmlText + "</p>\n", actualHtmlText);
+        }
+    }
+
+
+    /**
+     * Assert span exists in the correct location.
+     *
+     * @param seq The spannable string to check.
+     * @param start The starting index.
+     * @param end The ending index.
+     */
+    public static void assertPrefixSpan(CharSequence seq, int start, int end) {
+        Assert.assertTrue(seq instanceof Spanned);
+        Spanned spannable = (Spanned) seq;
+
+        if (start > 0) {
+            Assert.assertEquals(0, getNumForegroundColorSpansBetween(spannable, 0, start - 1));
+        }
+        Assert.assertEquals(1, getNumForegroundColorSpansBetween(spannable, start, end));
+        Assert.assertEquals(0, getNumForegroundColorSpansBetween(spannable, end + 1,
+                spannable.length() - 1));
+    }
+
+    private static int getNumForegroundColorSpansBetween(Spanned value, int start, int end) {
+        return value.getSpans(start, end, StyleSpan.class).length;
+    }
+
+    /**
+     * Asserts that the given character sequence is not a Spanned object and text is correct.
+     *
+     * @param seq The sequence to check.
+     * @param expected The expected text.
+     */
+    public static void assertNotSpanned(CharSequence seq, String expected) {
+        Assert.assertFalse(seq instanceof Spanned);
+        Assert.assertEquals(expected, seq);
+    }
+
+    public static int getNextTransition(SpannableString seq, int start) {
+        return seq.nextSpanTransition(start, seq.length(), StyleSpan.class);
+    }
+}
diff --git a/src/com/android/contacts/common/format/TextHighlighter.java b/src/com/android/contacts/common/format/TextHighlighter.java
new file mode 100644
index 0000000..496dcda
--- /dev/null
+++ b/src/com/android/contacts/common/format/TextHighlighter.java
@@ -0,0 +1,98 @@
+/*
+ * 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.format;
+
+import android.graphics.Typeface;
+import android.text.SpannableString;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.widget.TextView;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Highlights the text in a text field.
+ */
+public class TextHighlighter {
+    private final String TAG = TextHighlighter.class.getSimpleName();
+    private final static boolean DEBUG = false;
+
+    private int mTextStyle;
+
+    private CharacterStyle mTextStyleSpan;
+
+    public TextHighlighter(int textStyle) {
+        mTextStyle = textStyle;
+        mTextStyleSpan = getStyleSpan();
+    }
+
+    /**
+     * Sets the text on the given text view, highlighting the word that matches the given prefix.
+     *
+     * @param view the view on which to set the text
+     * @param text the string to use as the text
+     * @param prefix the prefix to look for
+     */
+    public void setPrefixText(TextView view, String text, String prefix) {
+        view.setText(applyPrefixHighlight(text, prefix));
+    }
+
+    private CharacterStyle getStyleSpan() {
+        return new StyleSpan(mTextStyle);
+    }
+
+    /**
+     * Applies highlight span to the text.
+     * @param text Text sequence to be highlighted.
+     * @param start Start position of the highlight sequence.
+     * @param end End position of the highlight sequence.
+     */
+    public void applyMaskingHighlight(SpannableString text, int start, int end) {
+        /** Sets text color of the masked locations to be highlighted. */
+        text.setSpan(getStyleSpan(), start, end, 0);
+    }
+
+    /**
+     * Returns a CharSequence which highlights the given prefix if found in the given text.
+     *
+     * @param text the text to which to apply the highlight
+     * @param prefix the prefix to look for
+     */
+    public CharSequence applyPrefixHighlight(CharSequence text, String prefix) {
+        if (prefix == null) {
+            return text;
+        }
+
+        // Skip non-word characters at the beginning of prefix.
+        int prefixStart = 0;
+        while (prefixStart < prefix.length() &&
+                !Character.isLetterOrDigit(prefix.charAt(prefixStart))) {
+            prefixStart++;
+        }
+        final String trimmedPrefix = prefix.substring(prefixStart);
+
+        int index = FormatUtils.indexOfWordPrefix(text, trimmedPrefix);
+        if (index != -1) {
+            final SpannableString result = new SpannableString(text);
+            result.setSpan(mTextStyleSpan, index, index + trimmedPrefix.length(), 0 /* flags */);
+            return result;
+        } else {
+            return text;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java b/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java
new file mode 100644
index 0000000..60f983d
--- /dev/null
+++ b/src/com/android/contacts/common/interactions/ImportExportDialogFragment.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.interactions;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.editor.SelectAccountDialogFragment;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.AccountSelectionUtil;
+import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
+import com.android.contacts.common.util.ImplicitIntentsUtil;
+import com.android.contacts.common.vcard.ExportVCardActivity;
+import com.android.contacts.common.vcard.VCardCommonArguments;
+import com.android.contacts.common.vcard.ShareVCardActivity;
+import com.android.contacts.commonbind.analytics.AnalyticsUtil;
+
+import java.util.List;
+
+/**
+ * An dialog invoked to import/export contacts.
+ */
+public class ImportExportDialogFragment extends DialogFragment
+        implements SelectAccountDialogFragment.Listener {
+    public static final String TAG = "ImportExportDialogFragment";
+
+    public static final int EXPORT_MODE_FAVORITES = 0;
+    public static final int EXPORT_MODE_ALL_CONTACTS = 1;
+    public static final int EXPORT_MODE_DEFAULT = -1;
+
+    private static final String KEY_RES_ID = "resourceId";
+    private static final String KEY_SUBSCRIPTION_ID = "subscriptionId";
+    private static final String ARG_CONTACTS_ARE_AVAILABLE = "CONTACTS_ARE_AVAILABLE";
+
+    private static int mExportMode = EXPORT_MODE_DEFAULT;
+
+    private final String[] LOOKUP_PROJECTION = new String[] {
+            Contacts.LOOKUP_KEY
+    };
+
+    private SubscriptionManager mSubscriptionManager;
+
+    /** Preferred way to show this dialog */
+    public static void show(FragmentManager fragmentManager, boolean contactsAreAvailable,
+                            Class callingActivity, int exportMode) {
+        final ImportExportDialogFragment fragment = new ImportExportDialogFragment();
+        Bundle args = new Bundle();
+        args.putBoolean(ARG_CONTACTS_ARE_AVAILABLE, contactsAreAvailable);
+        args.putString(VCardCommonArguments.ARG_CALLING_ACTIVITY, callingActivity.getName());
+        fragment.setArguments(args);
+        fragment.show(fragmentManager, ImportExportDialogFragment.TAG);
+        mExportMode = exportMode;
+    }
+
+    @Override
+    public Context getContext() {
+        return getActivity();
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        // Wrap our context to inflate list items using the correct theme
+        final Resources res = getActivity().getResources();
+        final LayoutInflater dialogInflater = (LayoutInflater)getActivity()
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        final boolean contactsAreAvailable = getArguments().getBoolean(ARG_CONTACTS_ARE_AVAILABLE);
+        final String callingActivity = getArguments().getString(
+                VCardCommonArguments.ARG_CALLING_ACTIVITY);
+
+        // Adapter that shows a list of string resources
+        final ArrayAdapter<AdapterEntry> adapter = new ArrayAdapter<AdapterEntry>(getActivity(),
+                R.layout.select_dialog_item) {
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                final TextView result = (TextView)(convertView != null ? convertView :
+                        dialogInflater.inflate(R.layout.select_dialog_item, parent, false));
+
+                result.setText(getItem(position).mLabel);
+                return result;
+            }
+        };
+
+        final TelephonyManager manager =
+                (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+        if (res.getBoolean(R.bool.config_allow_import_from_vcf_file)) {
+            adapter.add(new AdapterEntry(getString(R.string.import_from_vcf_file),
+                    R.string.import_from_vcf_file));
+        }
+
+        if (CompatUtils.isMSIMCompatible()) {
+            mSubscriptionManager = SubscriptionManager.from(getActivity());
+            if (manager != null && res.getBoolean(R.bool.config_allow_sim_import)) {
+                List<SubscriptionInfo> subInfoRecords = null;
+                try {
+                    subInfoRecords =  mSubscriptionManager.getActiveSubscriptionInfoList();
+                } catch (SecurityException e) {
+                    Log.w(TAG, "SecurityException thrown, lack permission for"
+                            + " getActiveSubscriptionInfoList", e);
+                }
+                if (subInfoRecords != null) {
+                    if (subInfoRecords.size() == 1) {
+                        adapter.add(new AdapterEntry(getString(R.string.import_from_sim),
+                                R.string.import_from_sim, subInfoRecords.get(0).getSubscriptionId()));
+                    } else {
+                        for (SubscriptionInfo record : subInfoRecords) {
+                            adapter.add(new AdapterEntry(getSubDescription(record),
+                                    R.string.import_from_sim, record.getSubscriptionId()));
+                        }
+                    }
+                }
+            }
+        } else {
+            if (manager != null && manager.hasIccCard()
+                    && res.getBoolean(R.bool.config_allow_sim_import)) {
+                adapter.add(new AdapterEntry(getString(R.string.import_from_sim),
+                        R.string.import_from_sim, -1));
+            }
+        }
+
+        if (res.getBoolean(R.bool.config_allow_export)) {
+            if (contactsAreAvailable) {
+                adapter.add(new AdapterEntry(getString(R.string.export_to_vcf_file),
+                        R.string.export_to_vcf_file));
+            }
+        }
+        if (res.getBoolean(R.bool.config_allow_share_contacts) && contactsAreAvailable) {
+            if (mExportMode == EXPORT_MODE_FAVORITES) {
+                // share favorite and frequently contacted contacts from Favorites tab
+                adapter.add(new AdapterEntry(getString(R.string.share_favorite_contacts),
+                        R.string.share_contacts));
+            } else {
+                // share "all" contacts (in groups selected in "Customize") from All tab for now
+                // TODO: change the string to share_visible_contacts if implemented
+                adapter.add(new AdapterEntry(getString(R.string.share_contacts),
+                        R.string.share_contacts));
+            }
+        }
+
+        final DialogInterface.OnClickListener clickListener =
+                new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                boolean dismissDialog;
+                final int resId = adapter.getItem(which).mChoiceResourceId;
+                if (resId == R.string.import_from_sim || resId == R.string.import_from_vcf_file) {
+                        dismissDialog = handleImportRequest(resId,
+                                adapter.getItem(which).mSubscriptionId);
+                } else if (resId == R.string.export_to_vcf_file) {
+                    dismissDialog = true;
+                    final Intent exportIntent = new Intent(
+                            getActivity(), ExportVCardActivity.class);
+                    exportIntent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY,
+                            callingActivity);
+                    getActivity().startActivity(exportIntent);
+                } else if (resId == R.string.share_contacts) {
+                    dismissDialog = true;
+                    if (mExportMode == EXPORT_MODE_FAVORITES) {
+                        doShareFavoriteContacts();
+                    } else { // EXPORT_MODE_ALL_CONTACTS
+                        final Intent exportIntent = new Intent(
+                                getActivity(), ShareVCardActivity.class);
+                        exportIntent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY,
+                                callingActivity);
+                        getActivity().startActivity(exportIntent);
+                    }
+                } else {
+                    dismissDialog = true;
+                    Log.e(TAG, "Unexpected resource: "
+                            + getActivity().getResources().getResourceEntryName(resId));
+                }
+                if (dismissDialog) {
+                    dialog.dismiss();
+                }
+            }
+        };
+        final TextView title = (TextView) View.inflate(getActivity(), R.layout.dialog_title, null);
+        title.setText(contactsAreAvailable
+                ? R.string.dialog_import_export
+                : R.string.dialog_import);
+        return new AlertDialog.Builder(getActivity())
+                .setCustomTitle(title)
+                .setSingleChoiceItems(adapter, -1, clickListener)
+                .create();
+    }
+
+    private void doShareFavoriteContacts() {
+        try{
+            final Cursor cursor = getActivity().getContentResolver().query(
+                    Contacts.CONTENT_STREQUENT_URI, LOOKUP_PROJECTION, null, null,
+                    Contacts.DISPLAY_NAME + " COLLATE NOCASE ASC");
+            if (cursor != null) {
+                try {
+                    if (!cursor.moveToFirst()) {
+                        Toast.makeText(getActivity(), R.string.no_contact_to_share,
+                                Toast.LENGTH_SHORT).show();
+                        return;
+                    }
+
+                    // Build multi-vcard Uri for sharing
+                    final StringBuilder uriListBuilder = new StringBuilder();
+                    int index = 0;
+                    do {
+                        if (index != 0)
+                            uriListBuilder.append(':');
+                        uriListBuilder.append(cursor.getString(0));
+                        index++;
+                    } while (cursor.moveToNext());
+                    final Uri uri = Uri.withAppendedPath(
+                            Contacts.CONTENT_MULTI_VCARD_URI,
+                            Uri.encode(uriListBuilder.toString()));
+
+                    final Intent intent = new Intent(Intent.ACTION_SEND);
+                    intent.setType(Contacts.CONTENT_VCARD_TYPE);
+                    intent.putExtra(Intent.EXTRA_STREAM, uri);
+                    ImplicitIntentsUtil.startActivityOutsideApp(getActivity(), intent);
+                } finally {
+                    cursor.close();
+                }
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Sharing contacts failed", e);
+            getActivity().runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    Toast.makeText(getContext(), R.string.share_contacts_failure,
+                            Toast.LENGTH_SHORT).show();
+                }
+            });
+        }
+    }
+
+    /**
+     * Handle "import from SIM" and "import from SD".
+     *
+     * @return {@code true} if the dialog show be closed.  {@code false} otherwise.
+     */
+    private boolean handleImportRequest(int resId, int subscriptionId) {
+        // There are three possibilities:
+        // - more than one accounts -> ask the user
+        // - just one account -> use the account without asking the user
+        // - no account -> use phone-local storage without asking the user
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(getActivity());
+        final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
+        final int size = accountList.size();
+        if (size > 1) {
+            // Send over to the account selector
+            final Bundle args = new Bundle();
+            args.putInt(KEY_RES_ID, resId);
+            args.putInt(KEY_SUBSCRIPTION_ID, subscriptionId);
+            SelectAccountDialogFragment.show(
+                    getFragmentManager(), this,
+                    R.string.dialog_new_contact_account,
+                    AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, args);
+
+            // In this case, because this DialogFragment is used as a target fragment to
+            // SelectAccountDialogFragment, we can't close it yet.  We close the dialog when
+            // we get a callback from it.
+            return false;
+        }
+
+        AccountSelectionUtil.doImport(getActivity(), resId,
+                (size == 1 ? accountList.get(0) : null),
+                (CompatUtils.isMSIMCompatible() ? subscriptionId : -1));
+        return true; // Close the dialog.
+    }
+
+    /**
+     * Called when an account is selected on {@link SelectAccountDialogFragment}.
+     */
+    @Override
+    public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) {
+        AccountSelectionUtil.doImport(getActivity(), extraArgs.getInt(KEY_RES_ID),
+                account, extraArgs.getInt(KEY_SUBSCRIPTION_ID));
+
+        // At this point the dialog is still showing (which is why we can use getActivity() above)
+        // So close it.
+        dismiss();
+    }
+
+    @Override
+    public void onAccountSelectorCancelled() {
+        // See onAccountChosen() -- at this point the dialog is still showing.  Close it.
+        dismiss();
+    }
+
+    private CharSequence getSubDescription(SubscriptionInfo record) {
+        CharSequence name = record.getDisplayName();
+        if (TextUtils.isEmpty(record.getNumber())) {
+            // Don't include the phone number in the description, since we don't know the number.
+            return getString(R.string.import_from_sim_summary_no_number, name);
+        }
+        return TextUtils.expandTemplate(
+                getString(R.string.import_from_sim_summary),
+                name,
+                PhoneNumberUtilsCompat.createTtsSpannable(record.getNumber()));
+    }
+
+    private static class AdapterEntry {
+        public final CharSequence mLabel;
+        public final int mChoiceResourceId;
+        public final int mSubscriptionId;
+
+        public AdapterEntry(CharSequence label, int resId, int subId) {
+            mLabel = label;
+            mChoiceResourceId = resId;
+            mSubscriptionId = subId;
+        }
+
+        public AdapterEntry(String label, int resId) {
+            // Store a nonsense value for mSubscriptionId. If this constructor is used,
+            // the mSubscriptionId value should not be read later.
+            this(label, resId, /* subId = */ -1);
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/interactions/TouchPointManager.java b/src/com/android/contacts/common/interactions/TouchPointManager.java
new file mode 100644
index 0000000..4c38e22
--- /dev/null
+++ b/src/com/android/contacts/common/interactions/TouchPointManager.java
@@ -0,0 +1,46 @@
+package com.android.contacts.common.interactions;
+
+import android.graphics.Point;
+
+/**
+ * Singleton class to keep track of where the user last touched the screen.
+ *
+ * Used to pass on to the InCallUI for animation.
+ */
+public class TouchPointManager {
+    public static final String TOUCH_POINT = "touchPoint";
+
+    private static TouchPointManager sInstance = new TouchPointManager();
+
+    private Point mPoint = new Point();
+
+    /**
+     * Private constructor.  Instance should only be acquired through getInstance().
+     */
+    private TouchPointManager() {
+    }
+
+    public static TouchPointManager getInstance() {
+        return sInstance;
+    }
+
+    public Point getPoint() {
+        return mPoint;
+    }
+
+    public void setPoint(int x, int y) {
+        mPoint.set(x, y);
+    }
+
+    /**
+     * When a point is initialized, its value is (0,0). Since it is highly unlikely a user will
+     * touch at that exact point, if the point in TouchPointManager is (0,0), it is safe to assume
+     * that the TouchPointManager has not yet collected a touch.
+     *
+     * @return True if there is a valid point saved. Define a valid point as any point that is
+     * not (0,0).
+     */
+    public boolean hasValidPoint() {
+        return mPoint.x != 0 || mPoint.y != 0;
+    }
+}
diff --git a/src/com/android/contacts/common/lettertiles/LetterTileDrawable.java b/src/com/android/contacts/common/lettertiles/LetterTileDrawable.java
new file mode 100644
index 0000000..d1f1811
--- /dev/null
+++ b/src/com/android/contacts/common/lettertiles/LetterTileDrawable.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2013 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.lettertiles;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+
+import com.android.contacts.common.R;
+
+import junit.framework.Assert;
+
+/**
+ * A drawable that encapsulates all the functionality needed to display a letter tile to
+ * represent a contact image.
+ */
+public class LetterTileDrawable extends Drawable {
+
+    private final String TAG = LetterTileDrawable.class.getSimpleName();
+
+    private final Paint mPaint;
+
+    /** Letter tile */
+    private static TypedArray sColors;
+    private static int sDefaultColor;
+    private static int sTileFontColor;
+    private static float sLetterToTileRatio;
+    private static Bitmap DEFAULT_PERSON_AVATAR;
+    private static Bitmap DEFAULT_BUSINESS_AVATAR;
+    private static Bitmap DEFAULT_VOICEMAIL_AVATAR;
+
+    /** Reusable components to avoid new allocations */
+    private static final Paint sPaint = new Paint();
+    private static final Rect sRect = new Rect();
+    private static final char[] sFirstChar = new char[1];
+
+    /** Contact type constants */
+    public static final int TYPE_PERSON = 1;
+    public static final int TYPE_BUSINESS = 2;
+    public static final int TYPE_VOICEMAIL = 3;
+    public static final int TYPE_DEFAULT = TYPE_PERSON;
+
+    /** 54% opacity */
+    private static final int ALPHA = 138;
+
+    private int mContactType = TYPE_DEFAULT;
+    private float mScale = 1.0f;
+    private float mOffset = 0.0f;
+    private boolean mIsCircle = false;
+
+    private int mColor;
+    private Character mLetter = null;
+
+    public LetterTileDrawable(final Resources res) {
+        if (sColors == null) {
+            sColors = res.obtainTypedArray(R.array.letter_tile_colors);
+            sDefaultColor = res.getColor(R.color.letter_tile_default_color);
+            sTileFontColor = res.getColor(R.color.letter_tile_font_color);
+            sLetterToTileRatio = res.getFraction(R.dimen.letter_to_tile_ratio, 1, 1);
+            DEFAULT_PERSON_AVATAR = BitmapFactory.decodeResource(res,
+                    R.drawable.ic_person_avatar);
+            DEFAULT_BUSINESS_AVATAR = BitmapFactory.decodeResource(res,
+                    R.drawable.ic_business_white_120dp);
+            DEFAULT_VOICEMAIL_AVATAR = BitmapFactory.decodeResource(res,
+                    R.drawable.ic_voicemail_avatar);
+            sPaint.setTypeface(Typeface.create(
+                    res.getString(R.string.letter_tile_letter_font_family), Typeface.NORMAL));
+            sPaint.setTextAlign(Align.CENTER);
+            sPaint.setAntiAlias(true);
+        }
+        mPaint = new Paint();
+        mPaint.setFilterBitmap(true);
+        mPaint.setDither(true);
+        mColor = sDefaultColor;
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        final Rect bounds = getBounds();
+        if (!isVisible() || bounds.isEmpty()) {
+            return;
+        }
+        // Draw letter tile.
+        drawLetterTile(canvas);
+    }
+
+    /**
+     * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
+     */
+    private void drawBitmap(final Bitmap bitmap, final int width, final int height,
+            final Canvas canvas) {
+        // The bitmap should be drawn in the middle of the canvas without changing its width to
+        // height ratio.
+        final Rect destRect = copyBounds();
+
+        // Crop the destination bounds into a square, scaled and offset as appropriate
+        final int halfLength = (int) (mScale * Math.min(destRect.width(), destRect.height()) / 2);
+
+        destRect.set(destRect.centerX() - halfLength,
+                (int) (destRect.centerY() - halfLength + mOffset * destRect.height()),
+                destRect.centerX() + halfLength,
+                (int) (destRect.centerY() + halfLength + mOffset * destRect.height()));
+
+        // Source rectangle remains the entire bounds of the source bitmap.
+        sRect.set(0, 0, width, height);
+
+        sPaint.setTextAlign(Align.CENTER);
+        sPaint.setAntiAlias(true);
+        sPaint.setAlpha(ALPHA);
+
+        canvas.drawBitmap(bitmap, sRect, destRect, sPaint);
+    }
+
+    private void drawLetterTile(final Canvas canvas) {
+        // Draw background color.
+        sPaint.setColor(mColor);
+
+        sPaint.setAlpha(mPaint.getAlpha());
+        final Rect bounds = getBounds();
+        final int minDimension = Math.min(bounds.width(), bounds.height());
+
+        if (mIsCircle) {
+            canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint);
+        } else {
+            canvas.drawRect(bounds, sPaint);
+        }
+
+        // Draw letter/digit only if the first character is an english letter or there's a override
+
+        if (mLetter != null) {
+            // Draw letter or digit.
+            sFirstChar[0] = mLetter;
+
+            // Scale text by canvas bounds and user selected scaling factor
+            sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
+            sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
+            sPaint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));
+            sPaint.setColor(sTileFontColor);
+            sPaint.setAlpha(ALPHA);
+
+            // Draw the letter in the canvas, vertically shifted up or down by the user-defined
+            // offset
+            canvas.drawText(sFirstChar, 0, 1, bounds.centerX(),
+                    bounds.centerY() + mOffset * bounds.height() - sRect.exactCenterY(),
+                    sPaint);
+        } else {
+            // Draw the default image if there is no letter/digit to be drawn
+            final Bitmap bitmap = getBitmapForContactType(mContactType);
+            drawBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(),
+                    canvas);
+        }
+    }
+
+    public int getColor() {
+        return mColor;
+    }
+
+    /**
+     * Returns a deterministic color based on the provided contact identifier string.
+     */
+    private int pickColor(final String identifier) {
+        if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) {
+            return sDefaultColor;
+        }
+        // String.hashCode() implementation is not supposed to change across java versions, so
+        // this should guarantee the same email address always maps to the same color.
+        // The email should already have been normalized by the ContactRequest.
+        final int color = Math.abs(identifier.hashCode()) % sColors.length();
+        return sColors.getColor(color, sDefaultColor);
+    }
+
+    private static Bitmap getBitmapForContactType(int contactType) {
+        switch (contactType) {
+            case TYPE_PERSON:
+                return DEFAULT_PERSON_AVATAR;
+            case TYPE_BUSINESS:
+                return DEFAULT_BUSINESS_AVATAR;
+            case TYPE_VOICEMAIL:
+                return DEFAULT_VOICEMAIL_AVATAR;
+            default:
+                return DEFAULT_PERSON_AVATAR;
+        }
+    }
+
+    private static boolean isEnglishLetter(final char c) {
+        return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
+    }
+
+    @Override
+    public void setAlpha(final int alpha) {
+        mPaint.setAlpha(alpha);
+    }
+
+    @Override
+    public void setColorFilter(final ColorFilter cf) {
+        mPaint.setColorFilter(cf);
+    }
+
+    @Override
+    public int getOpacity() {
+        return android.graphics.PixelFormat.OPAQUE;
+    }
+
+    /**
+     * Scale the drawn letter tile to a ratio of its default size
+     *
+     * @param scale The ratio the letter tile should be scaled to as a percentage of its default
+     * size, from a scale of 0 to 2.0f. The default is 1.0f.
+     */
+    public LetterTileDrawable setScale(float scale) {
+        mScale = scale;
+        return this;
+    }
+
+    /**
+     * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
+     *
+     * @param offset The provided offset must be within the range of -0.5f to 0.5f.
+     * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas
+     * it is being drawn on, which means it will be drawn with the center of the letter starting
+     * at the top edge of the canvas.
+     * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the canvas
+     * it is being drawn on, which means it will be drawn with the center of the letter starting
+     * at the bottom edge of the canvas.
+     * The default is 0.0f.
+     */
+    public LetterTileDrawable setOffset(float offset) {
+        Assert.assertTrue(offset >= -0.5f && offset <= 0.5f);
+        mOffset = offset;
+        return this;
+    }
+
+    public LetterTileDrawable setLetter(Character letter){
+        mLetter = letter;
+        return this;
+    }
+
+    public LetterTileDrawable setColor(int color){
+        mColor = color;
+        return this;
+    }
+
+    public LetterTileDrawable setLetterAndColorFromContactDetails(final String displayName,
+            final String identifier) {
+        if (displayName != null && displayName.length() > 0
+                && isEnglishLetter(displayName.charAt(0))) {
+            mLetter = Character.toUpperCase(displayName.charAt(0));
+        }else{
+            mLetter = null;
+        }
+        mColor = pickColor(identifier);
+        return this;
+    }
+
+    public LetterTileDrawable setContactType(int contactType) {
+        mContactType = contactType;
+        return this;
+    }
+
+    public LetterTileDrawable setIsCircular(boolean isCircle) {
+        mIsCircle = isCircle;
+        return this;
+    }
+}
diff --git a/src/com/android/contacts/common/list/AccountFilterActivity.java b/src/com/android/contacts/common/list/AccountFilterActivity.java
new file mode 100644
index 0000000..bed6977
--- /dev/null
+++ b/src/com/android/contacts/common/list/AccountFilterActivity.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Shows a list of all available accounts, letting the user select under which account to view
+ * contacts.
+ */
+public class AccountFilterActivity extends Activity implements AdapterView.OnItemClickListener {
+
+    private static final int SUBACTIVITY_CUSTOMIZE_FILTER = 0;
+
+    public static final String EXTRA_CONTACT_LIST_FILTER = "contactListFilter";
+
+    private ListView mListView;
+
+    // The default contact list type, it should be either FILTER_TYPE_ALL_ACCOUNTS or
+    // FILTER_TYPE_CUSTOM, since those are the only two options we give the user.
+    private int mCurrentFilterType;
+
+    private ContactListFilterView mCustomFilterView; // the "Customize" filter
+
+    private boolean mIsCustomFilterViewSelected;
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.contact_list_filter);
+
+        mListView = (ListView) findViewById(android.R.id.list);
+        mListView.setOnItemClickListener(this);
+
+        ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+
+        mCurrentFilterType = ContactListFilterController.getInstance(this).isCustomFilterPersisted()
+                ? ContactListFilter.FILTER_TYPE_CUSTOM
+                : ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS;
+
+        // We don't need to use AccountFilterUtil.FilterLoader since we only want to show
+        // the "All contacts" and "Customize" options.
+        final List<ContactListFilter> filters = new ArrayList<>();
+        filters.add(ContactListFilter.createFilterWithType(
+                ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS));
+        filters.add(ContactListFilter.createFilterWithType(
+                ContactListFilter.FILTER_TYPE_CUSTOM));
+        mListView.setAdapter(new FilterListAdapter(this, filters, mCurrentFilterType));
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        final ContactListFilterView listFilterView = (ContactListFilterView) view;
+        final ContactListFilter filter = (ContactListFilter) view.getTag();
+        if (filter == null) return; // Just in case
+        if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+            mCustomFilterView = listFilterView;
+            mIsCustomFilterViewSelected = listFilterView.isChecked();
+            final Intent intent = new Intent(this,
+                    CustomContactListFilterActivity.class);
+            listFilterView.setActivated(true);
+            // Switching activity has the highest priority. So when we open another activity, the
+            // announcement that indicates an account is checked will be interrupted. This is the
+            // way to overcome -- View.announceForAccessibility(CharSequence text);
+            listFilterView.announceForAccessibility(listFilterView.generateContentDescription());
+            startActivityForResult(intent, SUBACTIVITY_CUSTOMIZE_FILTER);
+        } else {
+            listFilterView.setActivated(true);
+            listFilterView.announceForAccessibility(listFilterView.generateContentDescription());
+            final Intent intent = new Intent();
+            intent.putExtra(EXTRA_CONTACT_LIST_FILTER, filter);
+            setResult(Activity.RESULT_OK, intent);
+            finish();
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (resultCode == Activity.RESULT_CANCELED && mCustomFilterView != null &&
+                !mIsCustomFilterViewSelected) {
+            mCustomFilterView.setActivated(false);
+            return;
+        }
+
+        if (resultCode != Activity.RESULT_OK) {
+            return;
+        }
+
+        switch (requestCode) {
+            case SUBACTIVITY_CUSTOMIZE_FILTER: {
+                final Intent intent = new Intent();
+                ContactListFilter filter = ContactListFilter.createFilterWithType(
+                        ContactListFilter.FILTER_TYPE_CUSTOM);
+                intent.putExtra(EXTRA_CONTACT_LIST_FILTER, filter);
+                setResult(Activity.RESULT_OK, intent);
+                finish();
+                break;
+            }
+        }
+    }
+
+    private static class FilterListAdapter extends BaseAdapter {
+        private final List<ContactListFilter> mFilters;
+        private final LayoutInflater mLayoutInflater;
+        private final AccountTypeManager mAccountTypes;
+        private final int mCurrentFilter;
+
+        public FilterListAdapter(
+                Context context, List<ContactListFilter> filters, int current) {
+            mLayoutInflater = (LayoutInflater) context.getSystemService
+                    (Context.LAYOUT_INFLATER_SERVICE);
+            mFilters = filters;
+            mCurrentFilter = current;
+            mAccountTypes = AccountTypeManager.getInstance(context);
+        }
+
+        @Override
+        public int getCount() {
+            return mFilters.size();
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public ContactListFilter getItem(int position) {
+            return mFilters.get(position);
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final ContactListFilterView view;
+            if (convertView != null) {
+                view = (ContactListFilterView) convertView;
+            } else {
+                view = (ContactListFilterView) mLayoutInflater.inflate(
+                        R.layout.contact_list_filter_item, parent, false);
+            }
+            view.setSingleAccount(mFilters.size() == 1);
+            final ContactListFilter filter = mFilters.get(position);
+            view.setContactListFilter(filter);
+            view.bindView(mAccountTypes);
+            view.setTag(filter);
+            view.setActivated(filter.filterType == mCurrentFilter);
+            return view;
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                // We have two logical "up" Activities: People and Phone.
+                // Instead of having one static "up" direction, behave like back as an
+                // exceptional case.
+                onBackPressed();
+                return true;
+            default:
+                break;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/src/com/android/contacts/common/list/AutoScrollListView.java b/src/com/android/contacts/common/list/AutoScrollListView.java
new file mode 100644
index 0000000..ae7ca17
--- /dev/null
+++ b/src/com/android/contacts/common/list/AutoScrollListView.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ListView;
+
+/**
+ * A ListView that can be asked to scroll (smoothly or otherwise) to a specific
+ * position.  This class takes advantage of similar functionality that exists
+ * in {@link ListView} and enhances it.
+ */
+public class AutoScrollListView extends ListView {
+
+    /**
+     * Position the element at about 1/3 of the list height
+     */
+    private static final float PREFERRED_SELECTION_OFFSET_FROM_TOP = 0.33f;
+
+    private int mRequestedScrollPosition = -1;
+    private boolean mSmoothScrollRequested;
+
+    public AutoScrollListView(Context context) {
+        super(context);
+    }
+
+    public AutoScrollListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    /**
+     * Brings the specified position to view by optionally performing a jump-scroll maneuver:
+     * first it jumps to some position near the one requested and then does a smooth
+     * scroll to the requested position.  This creates an impression of full smooth
+     * scrolling without actually traversing the entire list.  If smooth scrolling is
+     * not requested, instantly positions the requested item at a preferred offset.
+     */
+    public void requestPositionToScreen(int position, boolean smoothScroll) {
+        mRequestedScrollPosition = position;
+        mSmoothScrollRequested = smoothScroll;
+        requestLayout();
+    }
+
+    @Override
+    protected void layoutChildren() {
+        super.layoutChildren();
+        if (mRequestedScrollPosition == -1) {
+            return;
+        }
+
+        final int position = mRequestedScrollPosition;
+        mRequestedScrollPosition = -1;
+
+        int firstPosition = getFirstVisiblePosition() + 1;
+        int lastPosition = getLastVisiblePosition();
+        if (position >= firstPosition && position <= lastPosition) {
+            return; // Already on screen
+        }
+
+        final int offset = (int) (getHeight() * PREFERRED_SELECTION_OFFSET_FROM_TOP);
+        if (!mSmoothScrollRequested) {
+            setSelectionFromTop(position, offset);
+
+            // Since we have changed the scrolling position, we need to redo child layout
+            // Calling "requestLayout" in the middle of a layout pass has no effect,
+            // so we call layoutChildren explicitly
+            super.layoutChildren();
+
+        } else {
+            // We will first position the list a couple of screens before or after
+            // the new selection and then scroll smoothly to it.
+            int twoScreens = (lastPosition - firstPosition) * 2;
+            int preliminaryPosition;
+            if (position < firstPosition) {
+                preliminaryPosition = position + twoScreens;
+                if (preliminaryPosition >= getCount()) {
+                    preliminaryPosition = getCount() - 1;
+                }
+                if (preliminaryPosition < firstPosition) {
+                    setSelection(preliminaryPosition);
+                    super.layoutChildren();
+                }
+            } else {
+                preliminaryPosition = position - twoScreens;
+                if (preliminaryPosition < 0) {
+                    preliminaryPosition = 0;
+                }
+                if (preliminaryPosition > lastPosition) {
+                    setSelection(preliminaryPosition);
+                    super.layoutChildren();
+                }
+            }
+
+
+            smoothScrollToPositionFromTop(position, offset);
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactEntry.java b/src/com/android/contacts/common/list/ContactEntry.java
new file mode 100644
index 0000000..a29a8d8
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactEntry.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.ContactsContract.PinnedPositions;
+import android.text.TextUtils;
+
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/**
+ * Class to hold contact information
+ */
+public class ContactEntry {
+
+    private static final int UNSET_DISPLAY_ORDER_PREFERENCE = -1;
+
+    /**
+     * Primary name for a Contact
+     */
+    public String namePrimary;
+    /**
+     * Alternative name for a Contact, e.g. last name first
+     */
+    public String nameAlternative;
+    /**
+     * The user's preference on name display order, last name first or first time first.
+     * {@see ContactsPreferences}
+     */
+    public int nameDisplayOrder = UNSET_DISPLAY_ORDER_PREFERENCE;
+
+    public String status;
+    public String phoneLabel;
+    public String phoneNumber;
+    public Uri photoUri;
+    public Uri lookupUri;
+    public String lookupKey;
+    public Drawable presenceIcon;
+    public long id;
+    public int pinned = PinnedPositions.UNPINNED;
+    public boolean isFavorite = false;
+    public boolean isDefaultNumber = false;
+
+    public static final ContactEntry BLANK_ENTRY = new ContactEntry();
+
+    public String getPreferredDisplayName() {
+        if (nameDisplayOrder == UNSET_DISPLAY_ORDER_PREFERENCE
+                || nameDisplayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY
+                || TextUtils.isEmpty(nameAlternative)) {
+            return namePrimary;
+        }
+        return nameAlternative;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/common/list/ContactEntryListAdapter.java b/src/com/android/contacts/common/list/ContactEntryListAdapter.java
new file mode 100644
index 0000000..1ac8fd5
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactEntryListAdapter.java
@@ -0,0 +1,787 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.QuickContactBadge;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.util.SearchUtil;
+
+import java.util.HashSet;
+
+/**
+ * Common base class for various contact-related lists, e.g. contact list, phone number list
+ * etc.
+ */
+public abstract class ContactEntryListAdapter extends IndexerListAdapter {
+
+    private static final String TAG = "ContactEntryListAdapter";
+
+    /**
+     * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should
+     * be included in the search.
+     */
+    public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
+
+    private int mDisplayOrder;
+    private int mSortOrder;
+
+    private boolean mDisplayPhotos;
+    private boolean mCircularPhotos = true;
+    private boolean mQuickContactEnabled;
+    private boolean mAdjustSelectionBoundsEnabled;
+
+    /**
+     * indicates if contact queries include favorites
+     */
+    private boolean mIncludeFavorites;
+
+    private int mNumberOfFavorites;
+
+    /**
+     * The root view of the fragment that this adapter is associated with.
+     */
+    private View mFragmentRootView;
+
+    private ContactPhotoManager mPhotoLoader;
+
+    private String mQueryString;
+    private String mUpperCaseQueryString;
+    private boolean mSearchMode;
+    private int mDirectorySearchMode;
+    private int mDirectoryResultLimit = Integer.MAX_VALUE;
+
+    private boolean mEmptyListEnabled = true;
+
+    private boolean mSelectionVisible;
+
+    private ContactListFilter mFilter;
+    private boolean mDarkTheme = false;
+
+    /** Resource used to provide header-text for default filter. */
+    private CharSequence mDefaultFilterHeaderText;
+
+    public ContactEntryListAdapter(Context context) {
+        super(context);
+        setDefaultFilterHeaderText(R.string.local_search_label);
+        addPartitions();
+    }
+
+    /**
+     * @param fragmentRootView Root view of the fragment. This is used to restrict the scope of
+     * image loading requests that get cancelled on cursor changes.
+     */
+    protected void setFragmentRootView(View fragmentRootView) {
+        mFragmentRootView = fragmentRootView;
+    }
+
+    protected void setDefaultFilterHeaderText(int resourceId) {
+        mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
+    }
+
+    @Override
+    protected ContactListItemView newView(
+            Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+        final ContactListItemView view = new ContactListItemView(context, null);
+        view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
+        view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
+        return view;
+    }
+
+    @Override
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+        final ContactListItemView view = (ContactListItemView) itemView;
+        view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
+        bindWorkProfileIcon(view, partition);
+    }
+
+    @Override
+    protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
+        return new ContactListPinnedHeaderView(context, null, parent);
+    }
+
+    @Override
+    protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
+        ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title);
+    }
+
+    protected void addPartitions() {
+        addPartition(createDefaultDirectoryPartition());
+    }
+
+    protected DirectoryPartition createDefaultDirectoryPartition() {
+        DirectoryPartition partition = new DirectoryPartition(true, true);
+        partition.setDirectoryId(Directory.DEFAULT);
+        partition.setDirectoryType(getContext().getString(R.string.contactsList));
+        partition.setPriorityDirectory(true);
+        partition.setPhotoSupported(true);
+        partition.setLabel(mDefaultFilterHeaderText.toString());
+        return partition;
+    }
+
+    /**
+     * Remove all directories after the default directory. This is typically used when contacts
+     * list screens are asked to exit the search mode and thus need to remove all remote directory
+     * results for the search.
+     *
+     * This code assumes that the default directory and directories before that should not be
+     * deleted (e.g. Join screen has "suggested contacts" directory before the default director,
+     * and we should not remove the directory).
+     */
+    public void removeDirectoriesAfterDefault() {
+        final int partitionCount = getPartitionCount();
+        for (int i = partitionCount - 1; i >= 0; i--) {
+            final Partition partition = getPartition(i);
+            if ((partition instanceof DirectoryPartition)
+                    && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
+                break;
+            } else {
+                removePartition(i);
+            }
+        }
+    }
+
+    protected int getPartitionByDirectoryId(long id) {
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                if (((DirectoryPartition)partition).getDirectoryId() == id) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    protected DirectoryPartition getDirectoryById(long id) {
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+                if (directoryPartition.getDirectoryId() == id) {
+                    return directoryPartition;
+                }
+            }
+        }
+        return null;
+    }
+
+    public abstract String getContactDisplayName(int position);
+    public abstract void configureLoader(CursorLoader loader, long directoryId);
+
+    /**
+     * Marks all partitions as "loading"
+     */
+    public void onDataReload() {
+        boolean notify = false;
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+                if (!directoryPartition.isLoading()) {
+                    notify = true;
+                }
+                directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
+            }
+        }
+        if (notify) {
+            notifyDataSetChanged();
+        }
+    }
+
+    @Override
+    public void clearPartitions() {
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+                directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
+            }
+        }
+        super.clearPartitions();
+    }
+
+    public boolean isSearchMode() {
+        return mSearchMode;
+    }
+
+    public void setSearchMode(boolean flag) {
+        mSearchMode = flag;
+    }
+
+    public String getQueryString() {
+        return mQueryString;
+    }
+
+    public void setQueryString(String queryString) {
+        mQueryString = queryString;
+        if (TextUtils.isEmpty(queryString)) {
+            mUpperCaseQueryString = null;
+        } else {
+            mUpperCaseQueryString = SearchUtil
+                    .cleanStartAndEndOfSearchQuery(queryString.toUpperCase()) ;
+        }
+    }
+
+    public String getUpperCaseQueryString() {
+        return mUpperCaseQueryString;
+    }
+
+    public int getDirectorySearchMode() {
+        return mDirectorySearchMode;
+    }
+
+    public void setDirectorySearchMode(int mode) {
+        mDirectorySearchMode = mode;
+    }
+
+    public int getDirectoryResultLimit() {
+        return mDirectoryResultLimit;
+    }
+
+    public int getDirectoryResultLimit(DirectoryPartition directoryPartition) {
+        final int limit = directoryPartition.getResultLimit();
+        return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit;
+    }
+
+    public void setDirectoryResultLimit(int limit) {
+        this.mDirectoryResultLimit = limit;
+    }
+
+    public int getContactNameDisplayOrder() {
+        return mDisplayOrder;
+    }
+
+    public void setContactNameDisplayOrder(int displayOrder) {
+        mDisplayOrder = displayOrder;
+    }
+
+    public int getSortOrder() {
+        return mSortOrder;
+    }
+
+    public void setSortOrder(int sortOrder) {
+        mSortOrder = sortOrder;
+    }
+
+    public void setPhotoLoader(ContactPhotoManager photoLoader) {
+        mPhotoLoader = photoLoader;
+    }
+
+    protected ContactPhotoManager getPhotoLoader() {
+        return mPhotoLoader;
+    }
+
+    public boolean getDisplayPhotos() {
+        return mDisplayPhotos;
+    }
+
+    public void setDisplayPhotos(boolean displayPhotos) {
+        mDisplayPhotos = displayPhotos;
+    }
+
+    public boolean getCircularPhotos() {
+        return mCircularPhotos;
+    }
+
+    public void setCircularPhotos(boolean circularPhotos) {
+        mCircularPhotos = circularPhotos;
+    }
+
+    public boolean isEmptyListEnabled() {
+        return mEmptyListEnabled;
+    }
+
+    public void setEmptyListEnabled(boolean flag) {
+        mEmptyListEnabled = flag;
+    }
+
+    public boolean isSelectionVisible() {
+        return mSelectionVisible;
+    }
+
+    public void setSelectionVisible(boolean flag) {
+        this.mSelectionVisible = flag;
+    }
+
+    public boolean isQuickContactEnabled() {
+        return mQuickContactEnabled;
+    }
+
+    public void setQuickContactEnabled(boolean quickContactEnabled) {
+        mQuickContactEnabled = quickContactEnabled;
+    }
+
+    public boolean isAdjustSelectionBoundsEnabled() {
+        return mAdjustSelectionBoundsEnabled;
+    }
+
+    public void setAdjustSelectionBoundsEnabled(boolean enabled) {
+        mAdjustSelectionBoundsEnabled = enabled;
+    }
+
+    public boolean shouldIncludeFavorites() {
+        return mIncludeFavorites;
+    }
+
+    public void setIncludeFavorites(boolean includeFavorites) {
+        mIncludeFavorites = includeFavorites;
+    }
+
+    public void setFavoritesSectionHeader(int numberOfFavorites) {
+        if (mIncludeFavorites) {
+            mNumberOfFavorites = numberOfFavorites;
+            setSectionHeader(R.string.star_sign, numberOfFavorites);
+        }
+    }
+
+    public int getNumberOfFavorites() {
+        return mNumberOfFavorites;
+    }
+
+    private void setSectionHeader(int resId, int numberOfItems) {
+        SectionIndexer indexer = getIndexer();
+        if (indexer != null) {
+            ((ContactsSectionIndexer) indexer).setProfileAndFavoritesHeader(
+                    getContext().getString(resId), numberOfItems);
+        }
+    }
+
+    public void setDarkTheme(boolean value) {
+        mDarkTheme = value;
+    }
+
+    /**
+     * Updates partitions according to the directory meta-data contained in the supplied
+     * cursor.
+     */
+    public void changeDirectories(Cursor cursor) {
+        if (cursor.getCount() == 0) {
+            // Directory table must have at least local directory, without which this adapter will
+            // enter very weird state.
+            Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " +
+                    "no directory entries.", new RuntimeException());
+            return;
+        }
+        HashSet<Long> directoryIds = new HashSet<Long>();
+
+        int idColumnIndex = cursor.getColumnIndex(Directory._ID);
+        int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE);
+        int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME);
+        int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT);
+
+        // TODO preserve the order of partition to match those of the cursor
+        // Phase I: add new directories
+        cursor.moveToPosition(-1);
+        while (cursor.moveToNext()) {
+            long id = cursor.getLong(idColumnIndex);
+            directoryIds.add(id);
+            if (getPartitionByDirectoryId(id) == -1) {
+                DirectoryPartition partition = new DirectoryPartition(false, true);
+                partition.setDirectoryId(id);
+                if (DirectoryCompat.isRemoteDirectoryId(id)) {
+                    if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
+                        partition.setLabel(mContext.getString(R.string.directory_search_label_work));
+                    } else {
+                        partition.setLabel(mContext.getString(R.string.directory_search_label));
+                    }
+                } else {
+                    if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
+                        partition.setLabel(mContext.getString(R.string.list_filter_phones_work));
+                    } else {
+                        partition.setLabel(mDefaultFilterHeaderText.toString());
+                    }
+                }
+                partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
+                partition.setDisplayName(cursor.getString(displayNameColumnIndex));
+                int photoSupport = cursor.getInt(photoSupportColumnIndex);
+                partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
+                        || photoSupport == Directory.PHOTO_SUPPORT_FULL);
+                addPartition(partition);
+            }
+        }
+
+        // Phase II: remove deleted directories
+        int count = getPartitionCount();
+        for (int i = count; --i >= 0; ) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                long id = ((DirectoryPartition)partition).getDirectoryId();
+                if (!directoryIds.contains(id)) {
+                    removePartition(i);
+                }
+            }
+        }
+
+        invalidate();
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public void changeCursor(int partitionIndex, Cursor cursor) {
+        if (partitionIndex >= getPartitionCount()) {
+            // There is no partition for this data
+            return;
+        }
+
+        Partition partition = getPartition(partitionIndex);
+        if (partition instanceof DirectoryPartition) {
+            ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED);
+        }
+
+        if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) {
+            mPhotoLoader.refreshCache();
+        }
+
+        super.changeCursor(partitionIndex, cursor);
+
+        if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
+            updateIndexer(cursor);
+        }
+
+        // When the cursor changes, cancel any pending asynchronous photo loads.
+        mPhotoLoader.cancelPendingRequests(mFragmentRootView);
+    }
+
+    public void changeCursor(Cursor cursor) {
+        changeCursor(0, cursor);
+    }
+
+    /**
+     * Updates the indexer, which is used to produce section headers.
+     */
+    private void updateIndexer(Cursor cursor) {
+        if (cursor == null || cursor.isClosed()) {
+            setIndexer(null);
+            return;
+        }
+
+        Bundle bundle = cursor.getExtras();
+        if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES) &&
+                bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) {
+            String sections[] =
+                    bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
+            int counts[] = bundle.getIntArray(
+                    Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
+
+            if (getExtraStartingSection()) {
+                // Insert an additional unnamed section at the top of the list.
+                String allSections[] = new String[sections.length + 1];
+                int allCounts[] = new int[counts.length + 1];
+                for (int i = 0; i < sections.length; i++) {
+                    allSections[i + 1] = sections[i];
+                    allCounts[i + 1] = counts[i];
+                }
+                allCounts[0] = 1;
+                allSections[0] = "";
+                setIndexer(new ContactsSectionIndexer(allSections, allCounts));
+            } else {
+                setIndexer(new ContactsSectionIndexer(sections, counts));
+            }
+        } else {
+            setIndexer(null);
+        }
+    }
+
+    protected boolean getExtraStartingSection() {
+        return false;
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        // We need a separate view type for each item type, plus another one for
+        // each type with header, plus one for "other".
+        return getItemViewTypeCount() * 2 + 1;
+    }
+
+    @Override
+    public int getItemViewType(int partitionIndex, int position) {
+        int type = super.getItemViewType(partitionIndex, position);
+        if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
+            Placement placement = getItemPlacementInSection(position);
+            return placement.firstInSection ? type : getItemViewTypeCount() + type;
+        } else {
+            return type;
+        }
+    }
+
+    @Override
+    public boolean isEmpty() {
+        // TODO
+//        if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
+//            return true;
+//        }
+
+        if (!mEmptyListEnabled) {
+            return false;
+        } else if (isSearchMode()) {
+            return TextUtils.isEmpty(getQueryString());
+        } else {
+            return super.isEmpty();
+        }
+    }
+
+    public boolean isLoading() {
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition
+                    && ((DirectoryPartition) partition).isLoading()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean areAllPartitionsEmpty() {
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            if (!isPartitionEmpty(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Changes visibility parameters for the default directory partition.
+     */
+    public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) {
+        int defaultPartitionIndex = -1;
+        int count = getPartitionCount();
+        for (int i = 0; i < count; i++) {
+            Partition partition = getPartition(i);
+            if (partition instanceof DirectoryPartition &&
+                    ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) {
+                defaultPartitionIndex = i;
+                break;
+            }
+        }
+        if (defaultPartitionIndex != -1) {
+            setShowIfEmpty(defaultPartitionIndex, showIfEmpty);
+            setHasHeader(defaultPartitionIndex, hasHeader);
+        }
+    }
+
+    @Override
+    protected View newHeaderView(Context context, int partition, Cursor cursor,
+            ViewGroup parent) {
+        LayoutInflater inflater = LayoutInflater.from(context);
+        View view = inflater.inflate(R.layout.directory_header, parent, false);
+        if (!getPinnedPartitionHeadersEnabled()) {
+            // If the headers are unpinned, there is no need for their background
+            // color to be non-transparent. Setting this transparent reduces maintenance for
+            // non-pinned headers. We don't need to bother synchronizing the activity's
+            // background color with the header background color.
+            view.setBackground(null);
+        }
+        return view;
+    }
+
+    protected void bindWorkProfileIcon(final ContactListItemView view, int partitionId) {
+        final Partition partition = getPartition(partitionId);
+        if (partition instanceof DirectoryPartition) {
+            final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+            final long directoryId = directoryPartition.getDirectoryId();
+            final long userType = ContactsUtils.determineUserType(directoryId, null);
+            view.setWorkProfileIconEnabled(userType == ContactsUtils.USER_TYPE_WORK);
+        }
+    }
+
+    @Override
+    protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
+        Partition partition = getPartition(partitionIndex);
+        if (!(partition instanceof DirectoryPartition)) {
+            return;
+        }
+
+        DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+        long directoryId = directoryPartition.getDirectoryId();
+        TextView labelTextView = (TextView)view.findViewById(R.id.label);
+        TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name);
+        labelTextView.setText(directoryPartition.getLabel());
+        if (!DirectoryCompat.isRemoteDirectoryId(directoryId)) {
+            displayNameTextView.setText(null);
+        } else {
+            String directoryName = directoryPartition.getDisplayName();
+            String displayName = !TextUtils.isEmpty(directoryName)
+                    ? directoryName
+                    : directoryPartition.getDirectoryType();
+            displayNameTextView.setText(displayName);
+        }
+
+        final Resources res = getContext().getResources();
+        final int headerPaddingTop = partitionIndex == 1 && getPartition(0).isEmpty()?
+                0 : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding);
+        // There should be no extra padding at the top of the first directory header
+        view.setPaddingRelative(view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(),
+                view.getPaddingBottom());
+    }
+
+    // Default implementation simply returns number of rows in the cursor.
+    // Broken out into its own routine so can be overridden by child classes
+    // for eg number of unique contacts for a phone list.
+    protected int getResultCount(Cursor cursor) {
+        return cursor == null ? 0 : cursor.getCount();
+    }
+
+    // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
+    public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
+        if (count == 0) {
+            return getContext().getString(zeroResourceId);
+        } else {
+            String format = getContext().getResources()
+                    .getQuantityText(pluralResourceId, count).toString();
+            return String.format(format, count);
+        }
+    }
+
+    public boolean isPhotoSupported(int partitionIndex) {
+        Partition partition = getPartition(partitionIndex);
+        if (partition instanceof DirectoryPartition) {
+            return ((DirectoryPartition) partition).isPhotoSupported();
+        }
+        return true;
+    }
+
+    /**
+     * Returns the currently selected filter.
+     */
+    public ContactListFilter getFilter() {
+        return mFilter;
+    }
+
+    public void setFilter(ContactListFilter filter) {
+        mFilter = filter;
+    }
+
+    // TODO: move sharable logic (bindXX() methods) to here with extra arguments
+
+    /**
+     * Loads the photo for the quick contact view and assigns the contact uri.
+     * @param photoIdColumn Index of the photo id column
+     * @param photoUriColumn Index of the photo uri column. Optional: Can be -1
+     * @param contactIdColumn Index of the contact id column
+     * @param lookUpKeyColumn Index of the lookup key column
+     * @param displayNameColumn Index of the display name column
+     */
+    protected void bindQuickContact(final ContactListItemView view, int partitionIndex,
+            Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn,
+            int lookUpKeyColumn, int displayNameColumn) {
+        long photoId = 0;
+        if (!cursor.isNull(photoIdColumn)) {
+            photoId = cursor.getLong(photoIdColumn);
+        }
+
+        QuickContactBadge quickContact = view.getQuickContact();
+        quickContact.assignContactUri(
+                getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
+        if (CompatUtils.hasPrioritizedMimeType()) {
+            // The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume
+            // that only Dialer will use this QuickContact badge. This means prioritizing the phone
+            // mimetype here is reasonable.
+            quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+        }
+
+        if (photoId != 0 || photoUriColumn == -1) {
+            getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos,
+                    null);
+        } else {
+            final String photoUriString = cursor.getString(photoUriColumn);
+            final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+            DefaultImageRequest request = null;
+            if (photoUri == null) {
+                request = getDefaultImageRequestFromCursor(cursor, displayNameColumn,
+                        lookUpKeyColumn);
+            }
+            getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos,
+                    request);
+        }
+
+    }
+
+    @Override
+    public boolean hasStableIds() {
+        // Whenever bindViewId() is called, the values passed into setId() are stable or
+        // stable-ish. For example, when one contact is modified we don't expect a second
+        // contact's Contact._ID values to change.
+        return true;
+    }
+
+    protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) {
+        // Set a semi-stable id, so that talkback won't get confused when the list gets
+        // refreshed. There is little harm in inserting the same ID twice.
+        long contactId = cursor.getLong(idColumn);
+        view.setId((int) (contactId % Integer.MAX_VALUE));
+
+    }
+
+    protected Uri getContactUri(int partitionIndex, Cursor cursor,
+            int contactIdColumn, int lookUpKeyColumn) {
+        long contactId = cursor.getLong(contactIdColumn);
+        String lookupKey = cursor.getString(lookUpKeyColumn);
+        long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
+        Uri uri = Contacts.getLookupUri(contactId, lookupKey);
+        if (uri != null && directoryId != Directory.DEFAULT) {
+            uri = uri.buildUpon().appendQueryParameter(
+                    ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
+        }
+        return uri;
+    }
+
+    /**
+     * Retrieves the lookup key and display name from a cursor, and returns a
+     * {@link DefaultImageRequest} containing these contact details
+     *
+     * @param cursor Contacts cursor positioned at the current row to retrieve contact details for
+     * @param displayNameColumn Column index of the display name
+     * @param lookupKeyColumn Column index of the lookup key
+     * @return {@link DefaultImageRequest} with the displayName and identifier fields set to the
+     * display name and lookup key of the contact.
+     */
+    public DefaultImageRequest getDefaultImageRequestFromCursor(Cursor cursor,
+            int displayNameColumn, int lookupKeyColumn) {
+        final String displayName = cursor.getString(displayNameColumn);
+        final String lookupKey = cursor.getString(lookupKeyColumn);
+        return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos);
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactEntryListFragment.java b/src/com/android/contacts/common/list/ContactEntryListFragment.java
new file mode 100644
index 0000000..0c72d68
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactEntryListFragment.java
@@ -0,0 +1,987 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnFocusChangeListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.ListView;
+
+import com.android.common.widget.CompositeCursorAdapter.Partition;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.logging.ListEvent.ActionType;
+import com.android.contacts.common.logging.Logger;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+import java.util.Locale;
+
+/**
+ * Common base class for various contact-related list fragments.
+ */
+public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter>
+        extends Fragment
+        implements OnItemClickListener, OnScrollListener, OnFocusChangeListener, OnTouchListener,
+                OnItemLongClickListener, LoaderCallbacks<Cursor> {
+    private static final String TAG = "ContactEntryListFragment";
+
+    // TODO: Make this protected. This should not be used from the PeopleActivity but
+    // instead use the new startActivityWithResultFromFragment API
+    public static final int ACTIVITY_REQUEST_CODE_PICKER = 1;
+
+    private static final String KEY_LIST_STATE = "liststate";
+    private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled";
+    private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled";
+    private static final String KEY_QUICK_CONTACT_ENABLED = "quickContactEnabled";
+    private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED =
+            "adjustSelectionBoundsEnabled";
+    private static final String KEY_SEARCH_MODE = "searchMode";
+    private static final String KEY_DISPLAY_DIRECTORY_HEADER = "displayDirectoryHeader";
+    private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled";
+    private static final String KEY_SCROLLBAR_POSITION = "scrollbarPosition";
+    private static final String KEY_QUERY_STRING = "queryString";
+    private static final String KEY_DIRECTORY_SEARCH_MODE = "directorySearchMode";
+    private static final String KEY_SELECTION_VISIBLE = "selectionVisible";
+    private static final String KEY_REQUEST = "request";
+    private static final String KEY_DARK_THEME = "darkTheme";
+    private static final String KEY_LEGACY_COMPATIBILITY = "legacyCompatibility";
+    private static final String KEY_DIRECTORY_RESULT_LIMIT = "directoryResultLimit";
+    private static final String KEY_LOGS_LIST_EVENTS = "logsListEvents";
+    private static final String KEY_DATA_LOADED = "dataLoaded";
+
+    private static final String DIRECTORY_ID_ARG_KEY = "directoryId";
+
+    private static final int DIRECTORY_LOADER_ID = -1;
+
+    private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300;
+    private static final int DIRECTORY_SEARCH_MESSAGE = 1;
+
+    private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20;
+
+    private boolean mSectionHeaderDisplayEnabled;
+    private boolean mPhotoLoaderEnabled;
+    private boolean mQuickContactEnabled = true;
+    private boolean mAdjustSelectionBoundsEnabled = true;
+    private boolean mIncludeFavorites;
+    private boolean mSearchMode;
+    private boolean mDisplayDirectoryHeader = true;
+    private boolean mVisibleScrollbarEnabled;
+    private boolean mShowEmptyListForEmptyQuery;
+    private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition();
+    private String mQueryString;
+    private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE;
+    private boolean mSelectionVisible;
+    private boolean mLegacyCompatibility;
+    // Whether we should log list LOAD events. It may be modified when list filter is changed.
+    private boolean mLogListEvents = true;
+    // Whether data has been loaded ever. It will stay true once it's set to true in the lifecycle.
+    // We use this flag to log LOAD events when the activity/fragment is initialized.
+    private boolean mDataLoaded;
+
+    private boolean mEnabled = true;
+
+    private T mAdapter;
+    private View mView;
+    private ListView mListView;
+
+    /**
+     * Used to save the scrolling state of the list when the fragment is not recreated.
+     */
+    private int mListViewTopIndex;
+    private int mListViewTopOffset;
+
+    /**
+     * Used for keeping track of the scroll state of the list.
+     */
+    private Parcelable mListState;
+
+    /**
+     * The type of the contacts list.
+     */
+    private int mListType;
+
+    private int mDisplayOrder;
+    private int mSortOrder;
+    private int mDirectoryResultLimit = DEFAULT_DIRECTORY_RESULT_LIMIT;
+
+    private ContactPhotoManager mPhotoManager;
+    private ContactsPreferences mContactsPrefs;
+
+    private boolean mForceLoad;
+
+    private boolean mDarkTheme;
+
+    private static final int STATUS_NOT_LOADED = 0;
+    private static final int STATUS_LOADING = 1;
+    private static final int STATUS_LOADED = 2;
+
+    private int mDirectoryListStatus = STATUS_NOT_LOADED;
+
+    /**
+     * Indicates whether we are doing the initial complete load of data (false) or
+     * a refresh caused by a change notification (true)
+     */
+    private boolean mLoadPriorityDirectoriesOnly;
+
+    private Context mContext;
+
+    private LoaderManager mLoaderManager;
+
+    private Handler mDelayedDirectorySearchHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
+                loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
+            }
+        }
+    };
+    private int defaultVerticalScrollbarPosition;
+
+    protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
+    protected abstract T createListAdapter();
+
+    /**
+     * @param position Please note that the position is already adjusted for
+     *            header views, so "0" means the first list item below header
+     *            views.
+     */
+    protected abstract void onItemClick(int position, long id);
+
+    /**
+     * @param position Please note that the position is already adjusted for
+     *            header views, so "0" means the first list item below header
+     *            views.
+     */
+    protected boolean onItemLongClick(int position, long id) {
+        return false;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        setContext(activity);
+        setLoaderManager(super.getLoaderManager());
+    }
+
+    /**
+     * Sets a context for the fragment in the unit test environment.
+     */
+    public void setContext(Context context) {
+        mContext = context;
+        configurePhotoLoader();
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public void setEnabled(boolean enabled) {
+        if (mEnabled != enabled) {
+            mEnabled = enabled;
+            if (mAdapter != null) {
+                if (mEnabled) {
+                    reloadData();
+                } else {
+                    mAdapter.clearPartitions();
+                }
+            }
+        }
+    }
+
+    /**
+     * Overrides a loader manager for use in unit tests.
+     */
+    public void setLoaderManager(LoaderManager loaderManager) {
+        mLoaderManager = loaderManager;
+    }
+
+    @Override
+    public LoaderManager getLoaderManager() {
+        return mLoaderManager;
+    }
+
+    public T getAdapter() {
+        return mAdapter;
+    }
+
+    @Override
+    public View getView() {
+        return mView;
+    }
+
+    public ListView getListView() {
+        return mListView;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED, mSectionHeaderDisplayEnabled);
+        outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled);
+        outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled);
+        outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled);
+        outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
+        outState.putBoolean(KEY_DISPLAY_DIRECTORY_HEADER, mDisplayDirectoryHeader);
+        outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled);
+        outState.putInt(KEY_SCROLLBAR_POSITION, mVerticalScrollbarPosition);
+        outState.putInt(KEY_DIRECTORY_SEARCH_MODE, mDirectorySearchMode);
+        outState.putBoolean(KEY_SELECTION_VISIBLE, mSelectionVisible);
+        outState.putBoolean(KEY_LEGACY_COMPATIBILITY, mLegacyCompatibility);
+        outState.putString(KEY_QUERY_STRING, mQueryString);
+        outState.putInt(KEY_DIRECTORY_RESULT_LIMIT, mDirectoryResultLimit);
+        outState.putBoolean(KEY_DARK_THEME, mDarkTheme);
+        outState.putBoolean(KEY_LOGS_LIST_EVENTS, mLogListEvents);
+        outState.putBoolean(KEY_DATA_LOADED, mDataLoaded);
+
+        if (mListView != null) {
+            outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState());
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        restoreSavedState(savedState);
+        mAdapter = createListAdapter();
+        mContactsPrefs = new ContactsPreferences(mContext);
+    }
+
+    public void restoreSavedState(Bundle savedState) {
+        if (savedState == null) {
+            return;
+        }
+
+        mSectionHeaderDisplayEnabled = savedState.getBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED);
+        mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED);
+        mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED);
+        mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED);
+        mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE);
+        mDisplayDirectoryHeader = savedState.getBoolean(KEY_DISPLAY_DIRECTORY_HEADER);
+        mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED);
+        mVerticalScrollbarPosition = savedState.getInt(KEY_SCROLLBAR_POSITION);
+        mDirectorySearchMode = savedState.getInt(KEY_DIRECTORY_SEARCH_MODE);
+        mSelectionVisible = savedState.getBoolean(KEY_SELECTION_VISIBLE);
+        mLegacyCompatibility = savedState.getBoolean(KEY_LEGACY_COMPATIBILITY);
+        mQueryString = savedState.getString(KEY_QUERY_STRING);
+        mDirectoryResultLimit = savedState.getInt(KEY_DIRECTORY_RESULT_LIMIT);
+        mDarkTheme = savedState.getBoolean(KEY_DARK_THEME);
+
+        // Retrieve list state. This will be applied in onLoadFinished
+        mListState = savedState.getParcelable(KEY_LIST_STATE);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
+
+        mForceLoad = loadPreferences();
+
+        mDirectoryListStatus = STATUS_NOT_LOADED;
+        mLoadPriorityDirectoriesOnly = true;
+
+        startLoading();
+    }
+
+    protected void startLoading() {
+        if (mAdapter == null) {
+            // The method was called before the fragment was started
+            return;
+        }
+
+        configureAdapter();
+        int partitionCount = mAdapter.getPartitionCount();
+        for (int i = 0; i < partitionCount; i++) {
+            Partition partition = mAdapter.getPartition(i);
+            if (partition instanceof DirectoryPartition) {
+                DirectoryPartition directoryPartition = (DirectoryPartition)partition;
+                if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) {
+                    if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) {
+                        startLoadingDirectoryPartition(i);
+                    }
+                }
+            } else {
+                getLoaderManager().initLoader(i, null, this);
+            }
+        }
+
+        // Next time this method is called, we should start loading non-priority directories
+        mLoadPriorityDirectoriesOnly = false;
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        if (id == DIRECTORY_LOADER_ID) {
+            DirectoryListLoader loader = new DirectoryListLoader(mContext);
+            loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode());
+            loader.setLocalInvisibleDirectoryEnabled(
+                    ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED);
+            return loader;
+        } else {
+            CursorLoader loader = createCursorLoader(mContext);
+            long directoryId = args != null && args.containsKey(DIRECTORY_ID_ARG_KEY)
+                    ? args.getLong(DIRECTORY_ID_ARG_KEY)
+                    : Directory.DEFAULT;
+            mAdapter.configureLoader(loader, directoryId);
+            return loader;
+        }
+    }
+
+    public CursorLoader createCursorLoader(Context context) {
+        return new CursorLoader(context, null, null, null, null, null) {
+            @Override
+            protected Cursor onLoadInBackground() {
+                try {
+                    return super.onLoadInBackground();
+                } catch (RuntimeException e) {
+                    // We don't even know what the projection should be, so no point trying to
+                    // return an empty MatrixCursor with the correct projection here.
+                    Log.w(TAG, "RuntimeException while trying to query ContactsProvider.");
+                    return null;
+                }
+            }
+        };
+    }
+
+    private void startLoadingDirectoryPartition(int partitionIndex) {
+        DirectoryPartition partition = (DirectoryPartition)mAdapter.getPartition(partitionIndex);
+        partition.setStatus(DirectoryPartition.STATUS_LOADING);
+        long directoryId = partition.getDirectoryId();
+        if (mForceLoad) {
+            if (directoryId == Directory.DEFAULT) {
+                loadDirectoryPartition(partitionIndex, partition);
+            } else {
+                loadDirectoryPartitionDelayed(partitionIndex, partition);
+            }
+        } else {
+            Bundle args = new Bundle();
+            args.putLong(DIRECTORY_ID_ARG_KEY, directoryId);
+            getLoaderManager().initLoader(partitionIndex, args, this);
+        }
+    }
+
+    /**
+     * Queues up a delayed request to search the specified directory. Since
+     * directory search will likely introduce a lot of network traffic, we want
+     * to wait for a pause in the user's typing before sending a directory request.
+     */
+    private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) {
+        mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition);
+        Message msg = mDelayedDirectorySearchHandler.obtainMessage(
+                DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition);
+        mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS);
+    }
+
+    /**
+     * Loads the directory partition.
+     */
+    protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) {
+        Bundle args = new Bundle();
+        args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId());
+        getLoaderManager().restartLoader(partitionIndex, args, this);
+    }
+
+    /**
+     * Cancels all queued directory loading requests.
+     */
+    private void removePendingDirectorySearchRequests() {
+        mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE);
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        if (!mEnabled) {
+            return;
+        }
+
+        getListView().setVisibility(View.VISIBLE);
+        getView().setVisibility(View.VISIBLE);
+
+        int loaderId = loader.getId();
+        if (loaderId == DIRECTORY_LOADER_ID) {
+            mDirectoryListStatus = STATUS_LOADED;
+            mAdapter.changeDirectories(data);
+            startLoading();
+        } else {
+            onPartitionLoaded(loaderId, data);
+            if (isSearchMode()) {
+                int directorySearchMode = getDirectorySearchMode();
+                if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) {
+                    if (mDirectoryListStatus == STATUS_NOT_LOADED) {
+                        mDirectoryListStatus = STATUS_LOADING;
+                        getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this);
+                    } else {
+                        startLoading();
+                    }
+                }
+            } else {
+                maybeLogListEvent();
+                mDirectoryListStatus = STATUS_NOT_LOADED;
+                getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
+            }
+        }
+    }
+
+    protected void maybeLogListEvent() {
+        if (!mDataLoaded || mLogListEvents) {
+            Logger.logListEvent(ActionType.LOAD, getListType(), getAdapter().getCount(),
+                        /* clickedIndex */ -1, /* numSelected */ 0);
+            mLogListEvents = false;
+            mDataLoaded = true;
+        }
+    }
+
+    public void onLoaderReset(Loader<Cursor> loader) {
+    }
+
+    protected void onPartitionLoaded(int partitionIndex, Cursor data) {
+        if (partitionIndex >= mAdapter.getPartitionCount()) {
+            // When we get unsolicited data, ignore it.  This could happen
+            // when we are switching from search mode to the default mode.
+            return;
+        }
+
+        mAdapter.changeCursor(partitionIndex, data);
+        setListHeader();
+
+        if (!isLoading()) {
+            completeRestoreInstanceState();
+        }
+    }
+
+    public boolean isLoading() {
+        if (mAdapter != null && mAdapter.isLoading()) {
+            return true;
+        }
+
+        if (isLoadingDirectoryList()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    public boolean isLoadingDirectoryList() {
+        return isSearchMode() && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE
+                && (mDirectoryListStatus == STATUS_NOT_LOADED
+                        || mDirectoryListStatus == STATUS_LOADING);
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        mContactsPrefs.unregisterChangeListener();
+        mAdapter.clearPartitions();
+    }
+
+    protected void reloadData() {
+        removePendingDirectorySearchRequests();
+        mAdapter.onDataReload();
+        mLoadPriorityDirectoriesOnly = true;
+        mForceLoad = true;
+        startLoading();
+    }
+
+    /**
+     * Shows a view at the top of the list.
+     */
+    protected void setListHeader() {}
+
+    /**
+     * Provides logic that dismisses this fragment. The default implementation
+     * does nothing.
+     */
+    protected void finish() {
+    }
+
+    public void setSectionHeaderDisplayEnabled(boolean flag) {
+        if (mSectionHeaderDisplayEnabled != flag) {
+            mSectionHeaderDisplayEnabled = flag;
+            if (mAdapter != null) {
+                mAdapter.setSectionHeaderDisplayEnabled(flag);
+            }
+            configureVerticalScrollbar();
+        }
+    }
+
+    public boolean isSectionHeaderDisplayEnabled() {
+        return mSectionHeaderDisplayEnabled;
+    }
+
+    public void setVisibleScrollbarEnabled(boolean flag) {
+        if (mVisibleScrollbarEnabled != flag) {
+            mVisibleScrollbarEnabled = flag;
+            configureVerticalScrollbar();
+        }
+    }
+
+    public boolean isVisibleScrollbarEnabled() {
+        return mVisibleScrollbarEnabled;
+    }
+
+    public void setVerticalScrollbarPosition(int position) {
+        if (mVerticalScrollbarPosition != position) {
+            mVerticalScrollbarPosition = position;
+            configureVerticalScrollbar();
+        }
+    }
+
+    private void configureVerticalScrollbar() {
+        boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled();
+
+        if (mListView != null) {
+            mListView.setFastScrollEnabled(hasScrollbar);
+            mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition);
+            mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+        }
+    }
+
+    public void setPhotoLoaderEnabled(boolean flag) {
+        mPhotoLoaderEnabled = flag;
+        configurePhotoLoader();
+    }
+
+    public boolean isPhotoLoaderEnabled() {
+        return mPhotoLoaderEnabled;
+    }
+
+    /**
+     * Returns true if the list is supposed to visually highlight the selected item.
+     */
+    public boolean isSelectionVisible() {
+        return mSelectionVisible;
+    }
+
+    public void setSelectionVisible(boolean flag) {
+        this.mSelectionVisible = flag;
+    }
+
+    public void setQuickContactEnabled(boolean flag) {
+        this.mQuickContactEnabled = flag;
+    }
+
+    public void setAdjustSelectionBoundsEnabled(boolean flag) {
+        mAdjustSelectionBoundsEnabled = flag;
+    }
+
+    public void setIncludeFavorites(boolean flag) {
+        mIncludeFavorites = flag;
+        if (mAdapter != null) {
+            mAdapter.setIncludeFavorites(flag);
+        }
+    }
+
+    public void setDisplayDirectoryHeader(boolean flag) {
+        mDisplayDirectoryHeader = flag;
+    }
+
+    /**
+     * Enter/exit search mode. This is method is tightly related to the current query, and should
+     * only be called by {@link #setQueryString}.
+     *
+     * Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it.
+     */
+    protected void setSearchMode(boolean flag) {
+        if (mSearchMode != flag) {
+            mSearchMode = flag;
+            setSectionHeaderDisplayEnabled(!mSearchMode);
+
+            if (!flag) {
+                mDirectoryListStatus = STATUS_NOT_LOADED;
+                getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
+            }
+
+            if (mAdapter != null) {
+                mAdapter.setSearchMode(flag);
+
+                mAdapter.clearPartitions();
+                if (!flag) {
+                    // If we are switching from search to regular display, remove all directory
+                    // partitions after default one, assuming they are remote directories which
+                    // should be cleaned up on exiting the search mode.
+                    mAdapter.removeDirectoriesAfterDefault();
+                }
+                mAdapter.configureDefaultPartition(false, shouldDisplayDirectoryHeader());
+            }
+
+            if (mListView != null) {
+                mListView.setFastScrollEnabled(!flag);
+            }
+        }
+    }
+
+    /**
+     * When not in search mode, directory header should always be hidden.
+     * When in search mode, directory header should be displayed when mDisplayDirectoryHeader is
+     * set to true. (mDisplayDirectoryHeader default value is true)
+     */
+    private boolean shouldDisplayDirectoryHeader() {
+        if (!mSearchMode) {
+            return false;
+        }
+        return mDisplayDirectoryHeader;
+    }
+
+    public final boolean isSearchMode() {
+        return mSearchMode;
+    }
+
+    public final String getQueryString() {
+        return mQueryString;
+    }
+
+    // TODO: the paramter delaySelection is not in use, and let's remove it.
+    public void setQueryString(String queryString, boolean delaySelection) {
+        if (!TextUtils.equals(mQueryString, queryString)) {
+            if (mShowEmptyListForEmptyQuery && mAdapter != null && mListView != null) {
+                if (TextUtils.isEmpty(mQueryString)) {
+                    // Restore the adapter if the query used to be empty.
+                    mListView.setAdapter(mAdapter);
+                } else if (TextUtils.isEmpty(queryString)) {
+                    // Instantly clear the list view if the new query is empty.
+                    mListView.setAdapter(null);
+                }
+            }
+
+            mQueryString = queryString;
+            setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery);
+
+            if (mAdapter != null) {
+                mAdapter.setQueryString(queryString);
+                reloadData();
+            }
+        }
+    }
+
+    public void setShowEmptyListForNullQuery(boolean show) {
+        mShowEmptyListForEmptyQuery = show;
+    }
+
+    public int getDirectoryLoaderId() {
+        return DIRECTORY_LOADER_ID;
+    }
+
+    public int getDirectorySearchMode() {
+        return mDirectorySearchMode;
+    }
+
+    public void setDirectorySearchMode(int mode) {
+        mDirectorySearchMode = mode;
+    }
+
+    public boolean isLegacyCompatibilityMode() {
+        return mLegacyCompatibility;
+    }
+
+    public void setLegacyCompatibilityMode(boolean flag) {
+        mLegacyCompatibility = flag;
+    }
+
+    protected int getContactNameDisplayOrder() {
+        return mDisplayOrder;
+    }
+
+    protected void setContactNameDisplayOrder(int displayOrder) {
+        mDisplayOrder = displayOrder;
+        if (mAdapter != null) {
+            mAdapter.setContactNameDisplayOrder(displayOrder);
+        }
+    }
+
+    public int getSortOrder() {
+        return mSortOrder;
+    }
+
+    public void setSortOrder(int sortOrder) {
+        mSortOrder = sortOrder;
+        if (mAdapter != null) {
+            mAdapter.setSortOrder(sortOrder);
+        }
+    }
+
+    public void setDirectoryResultLimit(int limit) {
+        mDirectoryResultLimit = limit;
+    }
+
+    protected boolean loadPreferences() {
+        boolean changed = false;
+        if (getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) {
+            setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
+            changed = true;
+        }
+
+        if (getSortOrder() != mContactsPrefs.getSortOrder()) {
+            setSortOrder(mContactsPrefs.getSortOrder());
+            changed = true;
+        }
+
+        return changed;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        onCreateView(inflater, container);
+
+        boolean searchMode = isSearchMode();
+        mAdapter.setSearchMode(searchMode);
+        mAdapter.configureDefaultPartition(false, shouldDisplayDirectoryHeader());
+        mAdapter.setPhotoLoader(mPhotoManager);
+        mListView.setAdapter(mAdapter);
+
+        if (!isSearchMode()) {
+            mListView.setFocusableInTouchMode(true);
+            mListView.requestFocus();
+        }
+
+        if (savedInstanceState != null) {
+            mLogListEvents = savedInstanceState.getBoolean(KEY_LOGS_LIST_EVENTS, true);
+            mDataLoaded = savedInstanceState.getBoolean(KEY_DATA_LOADED, false);
+        }
+
+        return mView;
+    }
+
+    protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+        mView = inflateView(inflater, container);
+
+        mListView = (ListView)mView.findViewById(android.R.id.list);
+        if (mListView == null) {
+            throw new RuntimeException(
+                    "Your content must have a ListView whose id attribute is " +
+                    "'android.R.id.list'");
+        }
+
+        View emptyView = mView.findViewById(android.R.id.empty);
+        if (emptyView != null) {
+            mListView.setEmptyView(emptyView);
+        }
+
+        mListView.setOnItemClickListener(this);
+        mListView.setOnItemLongClickListener(this);
+        mListView.setOnFocusChangeListener(this);
+        mListView.setOnTouchListener(this);
+        mListView.setFastScrollEnabled(!isSearchMode());
+
+        // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
+        // them when an A-Z headers is visible.
+        mListView.setDividerHeight(0);
+
+        // We manually save/restore the listview state
+        mListView.setSaveEnabled(false);
+
+        configureVerticalScrollbar();
+        configurePhotoLoader();
+
+        getAdapter().setFragmentRootView(getView());
+    }
+
+    protected void configurePhotoLoader() {
+        if (isPhotoLoaderEnabled() && mContext != null) {
+            if (mPhotoManager == null) {
+                mPhotoManager = ContactPhotoManager.getInstance(mContext);
+            }
+            if (mListView != null) {
+                mListView.setOnScrollListener(this);
+            }
+            if (mAdapter != null) {
+                mAdapter.setPhotoLoader(mPhotoManager);
+            }
+        }
+    }
+
+    protected void configureAdapter() {
+        if (mAdapter == null) {
+            return;
+        }
+
+        mAdapter.setQuickContactEnabled(mQuickContactEnabled);
+        mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled);
+        mAdapter.setIncludeFavorites(mIncludeFavorites);
+        mAdapter.setQueryString(mQueryString);
+        mAdapter.setDirectorySearchMode(mDirectorySearchMode);
+        mAdapter.setPinnedPartitionHeadersEnabled(false);
+        mAdapter.setContactNameDisplayOrder(mDisplayOrder);
+        mAdapter.setSortOrder(mSortOrder);
+        mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled);
+        mAdapter.setSelectionVisible(mSelectionVisible);
+        mAdapter.setDirectoryResultLimit(mDirectoryResultLimit);
+        mAdapter.setDarkTheme(mDarkTheme);
+    }
+
+    @Override
+    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+            int totalItemCount) {
+    }
+
+    @Override
+    public void onScrollStateChanged(AbsListView view, int scrollState) {
+        if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
+            mPhotoManager.pause();
+        } else if (isPhotoLoaderEnabled()) {
+            mPhotoManager.resume();
+        }
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        hideSoftKeyboard();
+
+        int adjPosition = position - mListView.getHeaderViewsCount();
+        if (adjPosition >= 0) {
+            onItemClick(adjPosition, id);
+        }
+    }
+
+    @Override
+    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+        int adjPosition = position - mListView.getHeaderViewsCount();
+
+        if (adjPosition >= 0) {
+            return onItemLongClick(adjPosition, id);
+        }
+        return false;
+    }
+
+    private void hideSoftKeyboard() {
+        // Hide soft keyboard, if visible
+        InputMethodManager inputMethodManager = (InputMethodManager)
+                mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
+        inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0);
+    }
+
+    /**
+     * Dismisses the soft keyboard when the list takes focus.
+     */
+    @Override
+    public void onFocusChange(View view, boolean hasFocus) {
+        if (view == mListView && hasFocus) {
+            hideSoftKeyboard();
+        }
+    }
+
+    /**
+     * Dismisses the soft keyboard when the list is touched.
+     */
+    @Override
+    public boolean onTouch(View view, MotionEvent event) {
+        if (view == mListView) {
+            hideSoftKeyboard();
+        }
+        return false;
+    }
+
+    @Override
+    public void onPause() {
+        // Save the scrolling state of the list view
+        mListViewTopIndex = mListView.getFirstVisiblePosition();
+        View v = mListView.getChildAt(0);
+        mListViewTopOffset = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop());
+
+        super.onPause();
+        removePendingDirectorySearchRequests();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        // Restore the selection of the list view. See b/19982820.
+        // This has to be done manually because if the list view has its emptyView set,
+        // the scrolling state will be reset when clearPartitions() is called on the adapter.
+        mListView.setSelectionFromTop(mListViewTopIndex, mListViewTopOffset);
+    }
+
+    /**
+     * Restore the list state after the adapter is populated.
+     */
+    protected void completeRestoreInstanceState() {
+        if (mListState != null) {
+            mListView.onRestoreInstanceState(mListState);
+            mListState = null;
+        }
+    }
+
+    public void setDarkTheme(boolean value) {
+        mDarkTheme = value;
+        if (mAdapter != null) mAdapter.setDarkTheme(value);
+    }
+
+    /**
+     * Processes a result returned by the contact picker.
+     */
+    public void onPickerResult(Intent data) {
+        throw new UnsupportedOperationException("Picker result handler is not implemented.");
+    }
+
+    private ContactsPreferences.ChangeListener mPreferencesChangeListener =
+            new ContactsPreferences.ChangeListener() {
+        @Override
+        public void onChange() {
+            loadPreferences();
+            reloadData();
+        }
+    };
+
+    private int getDefaultVerticalScrollbarPosition() {
+        final Locale locale = Locale.getDefault();
+        final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
+        switch (layoutDirection) {
+            case View.LAYOUT_DIRECTION_RTL:
+                return View.SCROLLBAR_POSITION_LEFT;
+            case View.LAYOUT_DIRECTION_LTR:
+            default:
+                return View.SCROLLBAR_POSITION_RIGHT;
+        }
+    }
+
+    public void setListType(int listType) {
+        mListType = listType;
+    }
+
+    public int getListType() {
+        return mListType;
+    }
+
+    public void setLogListEvents(boolean logListEvents) {
+        mLogListEvents = logListEvents;
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactListAdapter.java b/src/com/android/contacts/common/list/ContactListAdapter.java
new file mode 100644
index 0000000..677aa46
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListAdapter.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.SearchSnippets;
+import android.text.TextUtils;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.ContactsCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
+ */
+public abstract class ContactListAdapter extends MultiSelectEntryContactListAdapter {
+
+    public static class ContactQuery {
+        private static final String[] CONTACT_PROJECTION_PRIMARY = new String[] {
+            Contacts._ID,                           // 0
+            Contacts.DISPLAY_NAME_PRIMARY,          // 1
+            Contacts.CONTACT_PRESENCE,              // 2
+            Contacts.CONTACT_STATUS,                // 3
+            Contacts.PHOTO_ID,                      // 4
+            Contacts.PHOTO_THUMBNAIL_URI,           // 5
+            Contacts.LOOKUP_KEY,                    // 6
+            Contacts.PHONETIC_NAME,                 // 7
+            Contacts.STARRED,                       // 9
+        };
+
+        private static final String[] CONTACT_PROJECTION_ALTERNATIVE = new String[] {
+            Contacts._ID,                           // 0
+            Contacts.DISPLAY_NAME_ALTERNATIVE,      // 1
+            Contacts.CONTACT_PRESENCE,              // 2
+            Contacts.CONTACT_STATUS,                // 3
+            Contacts.PHOTO_ID,                      // 4
+            Contacts.PHOTO_THUMBNAIL_URI,           // 5
+            Contacts.LOOKUP_KEY,                    // 6
+            Contacts.PHONETIC_NAME,                 // 7
+            Contacts.STARRED,                       // 8
+        };
+
+        private static final String[] FILTER_PROJECTION_PRIMARY = new String[] {
+            Contacts._ID,                           // 0
+            Contacts.DISPLAY_NAME_PRIMARY,          // 1
+            Contacts.CONTACT_PRESENCE,              // 2
+            Contacts.CONTACT_STATUS,                // 3
+            Contacts.PHOTO_ID,                      // 4
+            Contacts.PHOTO_THUMBNAIL_URI,           // 5
+            Contacts.LOOKUP_KEY,                    // 6
+            Contacts.PHONETIC_NAME,                 // 7
+            Contacts.STARRED,                       // 8
+            SearchSnippets.SNIPPET,                 // 9
+        };
+
+        private static final String[] FILTER_PROJECTION_ALTERNATIVE = new String[] {
+            Contacts._ID,                           // 0
+            Contacts.DISPLAY_NAME_ALTERNATIVE,      // 1
+            Contacts.CONTACT_PRESENCE,              // 2
+            Contacts.CONTACT_STATUS,                // 3
+            Contacts.PHOTO_ID,                      // 4
+            Contacts.PHOTO_THUMBNAIL_URI,           // 5
+            Contacts.LOOKUP_KEY,                    // 6
+            Contacts.PHONETIC_NAME,                 // 7
+            Contacts.STARRED,                       // 8
+            SearchSnippets.SNIPPET,                 // 9
+        };
+
+        public static final int CONTACT_ID               = 0;
+        public static final int CONTACT_DISPLAY_NAME     = 1;
+        public static final int CONTACT_PRESENCE_STATUS  = 2;
+        public static final int CONTACT_CONTACT_STATUS   = 3;
+        public static final int CONTACT_PHOTO_ID         = 4;
+        public static final int CONTACT_PHOTO_URI        = 5;
+        public static final int CONTACT_LOOKUP_KEY       = 6;
+        public static final int CONTACT_PHONETIC_NAME    = 7;
+        public static final int CONTACT_STARRED          = 8;
+        public static final int CONTACT_SNIPPET          = 9;
+    }
+
+    private CharSequence mUnknownNameText;
+
+    private long mSelectedContactDirectoryId;
+    private String mSelectedContactLookupKey;
+    private long mSelectedContactId;
+    private ContactListItemView.PhotoPosition mPhotoPosition;
+
+    public ContactListAdapter(Context context) {
+        super(context, ContactQuery.CONTACT_ID);
+
+        mUnknownNameText = context.getText(R.string.missing_name);
+    }
+
+    public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
+        mPhotoPosition = photoPosition;
+    }
+
+    public ContactListItemView.PhotoPosition getPhotoPosition() {
+        return mPhotoPosition;
+    }
+
+    public CharSequence getUnknownNameText() {
+        return mUnknownNameText;
+    }
+
+    public long getSelectedContactDirectoryId() {
+        return mSelectedContactDirectoryId;
+    }
+
+    public String getSelectedContactLookupKey() {
+        return mSelectedContactLookupKey;
+    }
+
+    public long getSelectedContactId() {
+        return mSelectedContactId;
+    }
+
+    public void setSelectedContact(long selectedDirectoryId, String lookupKey, long contactId) {
+        mSelectedContactDirectoryId = selectedDirectoryId;
+        mSelectedContactLookupKey = lookupKey;
+        mSelectedContactId = contactId;
+    }
+
+    protected static Uri buildSectionIndexerUri(Uri uri) {
+        return uri.buildUpon()
+                .appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true").build();
+    }
+
+    @Override
+    public String getContactDisplayName(int position) {
+        return ((Cursor) getItem(position)).getString(ContactQuery.CONTACT_DISPLAY_NAME);
+    }
+
+    /**
+     * Builds the {@link Contacts#CONTENT_LOOKUP_URI} for the given
+     * {@link ListView} position.
+     */
+    public Uri getContactUri(int position) {
+        int partitionIndex = getPartitionForPosition(position);
+        Cursor item = (Cursor)getItem(position);
+        return item != null ? getContactUri(partitionIndex, item) : null;
+    }
+
+    public Uri getContactUri(int partitionIndex, Cursor cursor) {
+        long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+        String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY);
+        Uri uri = Contacts.getLookupUri(contactId, lookupKey);
+        long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
+        if (uri != null && directoryId != Directory.DEFAULT) {
+            uri = uri.buildUpon().appendQueryParameter(
+                    ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
+        }
+        return uri;
+    }
+
+    /**
+     * Returns the {@link Contacts#_ID} for the given {@link ListView} position.
+     */
+    public long getContactId(int position) {
+        final Cursor cursor = (Cursor) getItem(position);
+        return cursor == null ? -1 : cursor.getLong(ContactQuery.CONTACT_ID);
+    }
+
+    public boolean isEnterpriseContact(int position) {
+        final Cursor cursor = (Cursor) getItem(position);
+        if (cursor != null) {
+            final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+            return ContactsCompat.isEnterpriseContactId(contactId);
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the specified contact is selected in the list. For a
+     * contact to be shown as selected, we need both the directory and and the
+     * lookup key to be the same. We are paying no attention to the contactId,
+     * because it is volatile, especially in the case of directories.
+     */
+    public boolean isSelectedContact(int partitionIndex, Cursor cursor) {
+        long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
+        if (getSelectedContactDirectoryId() != directoryId) {
+            return false;
+        }
+        String lookupKey = getSelectedContactLookupKey();
+        if (lookupKey != null && TextUtils.equals(lookupKey,
+                cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY))) {
+            return true;
+        }
+
+        return directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE
+                && getSelectedContactId() == cursor.getLong(ContactQuery.CONTACT_ID);
+    }
+
+    @Override
+    protected ContactListItemView newView(
+            Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+        ContactListItemView view = super.newView(context, partition, cursor, position, parent);
+        view.setUnknownNameText(mUnknownNameText);
+        view.setQuickContactEnabled(isQuickContactEnabled());
+        view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
+        view.setActivatedStateSupported(isSelectionVisible());
+        if (mPhotoPosition != null) {
+            view.setPhotoPosition(mPhotoPosition);
+        }
+        return view;
+    }
+
+    protected void bindSectionHeaderAndDivider(ContactListItemView view, int position,
+            Cursor cursor) {
+        view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
+        if (isSectionHeaderDisplayEnabled()) {
+            Placement placement = getItemPlacementInSection(position);
+            view.setSectionHeader(placement.sectionHeader);
+        } else {
+            view.setSectionHeader(null);
+        }
+    }
+
+    protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
+        if (!isPhotoSupported(partitionIndex)) {
+            view.removePhotoView();
+            return;
+        }
+
+        // Set the photo, if available
+        long photoId = 0;
+        if (!cursor.isNull(ContactQuery.CONTACT_PHOTO_ID)) {
+            photoId = cursor.getLong(ContactQuery.CONTACT_PHOTO_ID);
+        }
+
+        if (photoId != 0) {
+            getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false,
+                    getCircularPhotos(), null);
+        } else {
+            final String photoUriString = cursor.getString(ContactQuery.CONTACT_PHOTO_URI);
+            final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+            DefaultImageRequest request = null;
+            if (photoUri == null) {
+                request = getDefaultImageRequestFromCursor(cursor,
+                        ContactQuery.CONTACT_DISPLAY_NAME,
+                        ContactQuery.CONTACT_LOOKUP_KEY);
+            }
+            getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false,
+                    getCircularPhotos(), request);
+        }
+    }
+
+    protected void bindNameAndViewId(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(
+                cursor, ContactQuery.CONTACT_DISPLAY_NAME, getContactNameDisplayOrder());
+        // Note: we don't show phonetic any more (See issue 5265330)
+
+        bindViewId(view, cursor, ContactQuery.CONTACT_ID);
+    }
+
+    protected void bindPresenceAndStatusMessage(final ContactListItemView view, Cursor cursor) {
+        view.showPresenceAndStatusMessage(cursor, ContactQuery.CONTACT_PRESENCE_STATUS,
+                ContactQuery.CONTACT_CONTACT_STATUS);
+    }
+
+    protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
+        view.showSnippet(cursor, ContactQuery.CONTACT_SNIPPET);
+    }
+
+    public int getSelectedContactPosition() {
+        if (mSelectedContactLookupKey == null && mSelectedContactId == 0) {
+            return -1;
+        }
+
+        Cursor cursor = null;
+        int partitionIndex = -1;
+        int partitionCount = getPartitionCount();
+        for (int i = 0; i < partitionCount; i++) {
+            DirectoryPartition partition = (DirectoryPartition) getPartition(i);
+            if (partition.getDirectoryId() == mSelectedContactDirectoryId) {
+                partitionIndex = i;
+                break;
+            }
+        }
+        if (partitionIndex == -1) {
+            return -1;
+        }
+
+        cursor = getCursor(partitionIndex);
+        if (cursor == null) {
+            return -1;
+        }
+
+        cursor.moveToPosition(-1);      // Reset cursor
+        int offset = -1;
+        while (cursor.moveToNext()) {
+            if (mSelectedContactLookupKey != null) {
+                String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY);
+                if (mSelectedContactLookupKey.equals(lookupKey)) {
+                    offset = cursor.getPosition();
+                    break;
+                }
+            }
+            if (mSelectedContactId != 0 && (mSelectedContactDirectoryId == Directory.DEFAULT
+                    || mSelectedContactDirectoryId == Directory.LOCAL_INVISIBLE)) {
+                long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+                if (contactId == mSelectedContactId) {
+                    offset = cursor.getPosition();
+                    break;
+                }
+            }
+        }
+        if (offset == -1) {
+            return -1;
+        }
+
+        int position = getPositionForPartition(partitionIndex) + offset;
+        if (hasHeader(partitionIndex)) {
+            position++;
+        }
+        return position;
+    }
+
+    public boolean hasValidSelection() {
+        return getSelectedContactPosition() != -1;
+    }
+
+    public Uri getFirstContactUri() {
+        int partitionCount = getPartitionCount();
+        for (int i = 0; i < partitionCount; i++) {
+            DirectoryPartition partition = (DirectoryPartition) getPartition(i);
+            if (partition.isLoading()) {
+                continue;
+            }
+
+            Cursor cursor = getCursor(i);
+            if (cursor == null) {
+                continue;
+            }
+
+            if (!cursor.moveToFirst()) {
+                continue;
+            }
+
+            return getContactUri(i, cursor);
+        }
+
+        return null;
+    }
+
+    @Override
+    public void changeCursor(int partitionIndex, Cursor cursor) {
+        super.changeCursor(partitionIndex, cursor);
+
+        if (cursor == null || !cursor.moveToFirst()) {
+            return;
+        }
+
+        if (shouldIncludeFavorites()) {
+            if (cursor.getInt(ContactQuery.CONTACT_STARRED) == 1) {
+                final Set<Integer> favorites = new HashSet<>();
+                favorites.add(cursor.getInt(ContactQuery.CONTACT_ID));
+                while (cursor != null && cursor.moveToNext()) {
+                    if (cursor.getInt(ContactQuery.CONTACT_STARRED) != 1
+                            || favorites.contains(cursor.getInt(ContactQuery.CONTACT_ID))) {
+                        break;
+                    }
+                    favorites.add(cursor.getInt(ContactQuery.CONTACT_ID));
+                }
+                setFavoritesSectionHeader(favorites.size());
+            }
+        }
+    }
+
+    /**
+     * @return Projection useful for children.
+     */
+    protected final String[] getProjection(boolean forSearch) {
+        final int sortOrder = getContactNameDisplayOrder();
+        if (forSearch) {
+            if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+                return ContactQuery.FILTER_PROJECTION_PRIMARY;
+            } else {
+                return ContactQuery.FILTER_PROJECTION_ALTERNATIVE;
+            }
+        } else {
+            if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+                return ContactQuery.CONTACT_PROJECTION_PRIMARY;
+            } else {
+                return ContactQuery.CONTACT_PROJECTION_ALTERNATIVE;
+            }
+        }
+    }
+
+    /**
+     * @return Projection from Data that is useful for children.
+     */
+    protected final String[] getDataProjectionForContacts(boolean forSearch) {
+        final int sortOrder = getContactNameDisplayOrder();
+        if (forSearch) {
+            if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+                return replaceFirstString(ContactQuery.FILTER_PROJECTION_PRIMARY);
+            } else {
+                return replaceFirstString(ContactQuery.FILTER_PROJECTION_ALTERNATIVE);
+            }
+        } else {
+            if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+                return replaceFirstString(ContactQuery.CONTACT_PROJECTION_PRIMARY);
+            } else {
+                return replaceFirstString(ContactQuery.CONTACT_PROJECTION_ALTERNATIVE);
+            }
+        }
+    }
+
+    /**
+     * @param sourceProjection
+     * @return Replace the first String of sourceProjection with Data.CONTACT_ID.
+     */
+    private String[] replaceFirstString(String[] sourceProjection) {
+        String[] result = sourceProjection.clone();
+        result[0] = Data.CONTACT_ID;
+        return result;
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactListFilter.java b/src/com/android/contacts/common/list/ContactListFilter.java
new file mode 100644
index 0000000..6d60a82
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListFilter.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+
+import com.android.contacts.common.logging.ListEvent;
+
+/**
+ * Contact list filter parameters.
+ */
+public final class ContactListFilter implements Comparable<ContactListFilter>, Parcelable {
+
+    public static final int FILTER_TYPE_DEFAULT = -1;
+    public static final int FILTER_TYPE_ALL_ACCOUNTS = -2;
+    public static final int FILTER_TYPE_CUSTOM = -3;
+    public static final int FILTER_TYPE_STARRED = -4;
+    public static final int FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY = -5;
+    public static final int FILTER_TYPE_SINGLE_CONTACT = -6;
+    public static final int FILTER_TYPE_GROUP_MEMBERS = -7;
+    public static final int FILTER_TYPE_DEVICE_CONTACTS = -8;
+
+    public static final int FILTER_TYPE_ACCOUNT = 0;
+
+    /**
+     * Obsolete filter which had been used in Honeycomb. This may be stored in
+     * {@link SharedPreferences}, but should be replaced with ALL filter when it is found.
+     *
+     * TODO: "group" filter and relevant variables are all obsolete. Remove them.
+     */
+    private static final int FILTER_TYPE_GROUP = 1;
+
+    private static final String KEY_FILTER_TYPE = "filter.type";
+    private static final String KEY_ACCOUNT_NAME = "filter.accountName";
+    private static final String KEY_ACCOUNT_TYPE = "filter.accountType";
+    private static final String KEY_DATA_SET = "filter.dataSet";
+
+    public final int filterType;
+    public final String accountType;
+    public final String accountName;
+    public final String dataSet;
+    public final Drawable icon;
+    private String mId;
+
+    public ContactListFilter(int filterType, String accountType, String accountName, String dataSet,
+            Drawable icon) {
+        this.filterType = filterType;
+        this.accountType = accountType;
+        this.accountName = accountName;
+        this.dataSet = dataSet;
+        this.icon = icon;
+    }
+
+    public static ContactListFilter createFilterWithType(int filterType) {
+        return new ContactListFilter(filterType, null, null, null, null);
+    }
+
+    public static ContactListFilter createAccountFilter(String accountType, String accountName,
+            String dataSet, Drawable icon) {
+        return new ContactListFilter(ContactListFilter.FILTER_TYPE_ACCOUNT, accountType,
+                accountName, dataSet, icon);
+    }
+
+    public static ContactListFilter createGroupMembersFilter(String accountType, String accountName,
+            String dataSet) {
+        return new ContactListFilter(ContactListFilter.FILTER_TYPE_GROUP_MEMBERS, accountType,
+                accountName, dataSet, /* icon */ null);
+    }
+
+    public static ContactListFilter createDeviceContactsFilter(Drawable icon) {
+        return new ContactListFilter(ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS,
+                /* accountType= */ null, /* accountName= */ null, /* dataSet= */ null, icon);
+    }
+
+    /**
+     * Whether the given {@link ContactListFilter} has a filter type that should be displayed as
+     * the default contacts list view.
+     */
+    public boolean isContactsFilterType() {
+        return filterType == ContactListFilter.FILTER_TYPE_DEFAULT
+                || filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS
+                || filterType == ContactListFilter.FILTER_TYPE_CUSTOM;
+    }
+
+    /** Returns the {@link ListEvent.ListType} for the type of this filter. */
+    public int toListType() {
+        switch (filterType) {
+            case FILTER_TYPE_DEFAULT:
+                // Fall through
+            case FILTER_TYPE_ALL_ACCOUNTS:
+                return ListEvent.ListType.ALL_CONTACTS;
+            case FILTER_TYPE_CUSTOM:
+                return ListEvent.ListType.CUSTOM;
+            case FILTER_TYPE_STARRED:
+                return ListEvent.ListType.STARRED;
+            case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+                return ListEvent.ListType.PHONE_NUMBERS;
+            case FILTER_TYPE_SINGLE_CONTACT:
+                return ListEvent.ListType.SINGLE_CONTACT;
+            case FILTER_TYPE_ACCOUNT:
+                return ListEvent.ListType.ACCOUNT;
+            case FILTER_TYPE_GROUP_MEMBERS:
+                return ListEvent.ListType.GROUP;
+            case FILTER_TYPE_DEVICE_CONTACTS:
+                return ListEvent.ListType.DEVICE;
+        }
+        return ListEvent.ListType.UNKNOWN_LIST;
+    }
+
+
+    /**
+     * Returns true if this filter is based on data and may become invalid over time.
+     */
+    public boolean isValidationRequired() {
+        return filterType == FILTER_TYPE_ACCOUNT;
+    }
+
+    @Override
+    public String toString() {
+        switch (filterType) {
+            case FILTER_TYPE_DEFAULT:
+                return "default";
+            case FILTER_TYPE_ALL_ACCOUNTS:
+                return "all_accounts";
+            case FILTER_TYPE_CUSTOM:
+                return "custom";
+            case FILTER_TYPE_STARRED:
+                return "starred";
+            case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+                return "with_phones";
+            case FILTER_TYPE_SINGLE_CONTACT:
+                return "single";
+            case FILTER_TYPE_ACCOUNT:
+                return "account: " + accountType + (dataSet != null ? "/" + dataSet : "")
+                        + " " + accountName;
+            case FILTER_TYPE_GROUP_MEMBERS:
+                return "group_members";
+            case FILTER_TYPE_DEVICE_CONTACTS:
+                return "device_contacts";
+        }
+        return super.toString();
+    }
+
+    @Override
+    public int compareTo(ContactListFilter another) {
+        int res = accountName.compareTo(another.accountName);
+        if (res != 0) {
+            return res;
+        }
+
+        res = accountType.compareTo(another.accountType);
+        if (res != 0) {
+            return res;
+        }
+
+        return filterType - another.filterType;
+    }
+
+    @Override
+    public int hashCode() {
+        int code = filterType;
+        if (accountType != null) {
+            code = code * 31 + accountType.hashCode();
+            code = code * 31 + accountName.hashCode();
+        }
+        if (dataSet != null) {
+            code = code * 31 + dataSet.hashCode();
+        }
+        return code;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (!(other instanceof ContactListFilter)) {
+            return false;
+        }
+
+        ContactListFilter otherFilter = (ContactListFilter) other;
+        if (filterType != otherFilter.filterType
+                || !TextUtils.equals(accountName, otherFilter.accountName)
+                || !TextUtils.equals(accountType, otherFilter.accountType)
+                || !TextUtils.equals(dataSet, otherFilter.dataSet)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Store the given {@link ContactListFilter} to preferences. If the requested filter is
+     * of type {@link #FILTER_TYPE_SINGLE_CONTACT} then do not save it to preferences because
+     * it is a temporary state.
+     */
+    public static void storeToPreferences(SharedPreferences prefs, ContactListFilter filter) {
+        if (filter != null && filter.filterType == FILTER_TYPE_SINGLE_CONTACT) {
+            return;
+        }
+        prefs.edit()
+            .putInt(KEY_FILTER_TYPE, filter == null ? FILTER_TYPE_DEFAULT : filter.filterType)
+            .putString(KEY_ACCOUNT_NAME, filter == null ? null : filter.accountName)
+            .putString(KEY_ACCOUNT_TYPE, filter == null ? null : filter.accountType)
+            .putString(KEY_DATA_SET, filter == null ? null : filter.dataSet)
+            .apply();
+    }
+
+    /**
+     * Try to obtain ContactListFilter object saved in SharedPreference.
+     * If there's no info there, return ALL filter instead.
+     */
+    public static ContactListFilter restoreDefaultPreferences(SharedPreferences prefs) {
+        ContactListFilter filter = restoreFromPreferences(prefs);
+        if (filter == null) {
+            filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS);
+        }
+        // "Group" filter is obsolete and thus is not exposed anymore. The "single contact mode"
+        // should also not be stored in preferences anymore since it is a temporary state.
+        if (filter.filterType == FILTER_TYPE_GROUP ||
+                filter.filterType == FILTER_TYPE_SINGLE_CONTACT) {
+            filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS);
+        }
+        return filter;
+    }
+
+    private static ContactListFilter restoreFromPreferences(SharedPreferences prefs) {
+        int filterType = prefs.getInt(KEY_FILTER_TYPE, FILTER_TYPE_DEFAULT);
+        if (filterType == FILTER_TYPE_DEFAULT) {
+            return null;
+        }
+
+        String accountName = prefs.getString(KEY_ACCOUNT_NAME, null);
+        String accountType = prefs.getString(KEY_ACCOUNT_TYPE, null);
+        String dataSet = prefs.getString(KEY_DATA_SET, null);
+        return new ContactListFilter(filterType, accountType, accountName, dataSet, null);
+    }
+
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(filterType);
+        dest.writeString(accountName);
+        dest.writeString(accountType);
+        dest.writeString(dataSet);
+    }
+
+    public static final Parcelable.Creator<ContactListFilter> CREATOR =
+            new Parcelable.Creator<ContactListFilter>() {
+        @Override
+        public ContactListFilter createFromParcel(Parcel source) {
+            int filterType = source.readInt();
+            String accountName = source.readString();
+            String accountType = source.readString();
+            String dataSet = source.readString();
+            return new ContactListFilter(filterType, accountType, accountName, dataSet, null);
+        }
+
+        @Override
+        public ContactListFilter[] newArray(int size) {
+            return new ContactListFilter[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Returns a string that can be used as a stable persistent identifier for this filter.
+     */
+    public String getId() {
+        if (mId == null) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(filterType);
+            if (accountType != null) {
+                sb.append('-').append(accountType);
+            }
+            if (dataSet != null) {
+                sb.append('/').append(dataSet);
+            }
+            if (accountName != null) {
+                sb.append('-').append(accountName.replace('-', '_'));
+            }
+            mId = sb.toString();
+        }
+        return mId;
+    }
+
+    /**
+     * Adds the account query parameters to the given {@code uriBuilder}.
+     *
+     * @throws IllegalStateException if the filter type is not {@link #FILTER_TYPE_ACCOUNT}.
+     */
+    public Uri.Builder addAccountQueryParameterToUrl(Uri.Builder uriBuilder) {
+        if (filterType != FILTER_TYPE_ACCOUNT
+                && filterType != FILTER_TYPE_GROUP_MEMBERS) {
+            throw new IllegalStateException(
+                    "filterType must be FILTER_TYPE_ACCOUNT or FILER_TYPE_GROUP_MEMBERS");
+        }
+        uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName);
+        uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType);
+        if (!TextUtils.isEmpty(dataSet)) {
+            uriBuilder.appendQueryParameter(RawContacts.DATA_SET, dataSet);
+        }
+        return uriBuilder;
+    }
+
+    public String toDebugString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("[filter type: " + filterType + " (" + filterTypeToString(filterType) + ")");
+        if (filterType == FILTER_TYPE_ACCOUNT) {
+            builder.append(", accountType: " + accountType)
+                    .append(", accountName: " + accountName)
+                    .append(", dataSet: " + dataSet);
+        }
+        builder.append(", icon: " + icon + "]");
+        return builder.toString();
+    }
+
+    public static final String filterTypeToString(int filterType) {
+        switch (filterType) {
+            case FILTER_TYPE_DEFAULT:
+                return "FILTER_TYPE_DEFAULT";
+            case FILTER_TYPE_ALL_ACCOUNTS:
+                return "FILTER_TYPE_ALL_ACCOUNTS";
+            case FILTER_TYPE_CUSTOM:
+                return "FILTER_TYPE_CUSTOM";
+            case FILTER_TYPE_STARRED:
+                return "FILTER_TYPE_STARRED";
+            case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+                return "FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY";
+            case FILTER_TYPE_SINGLE_CONTACT:
+                return "FILTER_TYPE_SINGLE_CONTACT";
+            case FILTER_TYPE_ACCOUNT:
+                return "FILTER_TYPE_ACCOUNT";
+            case FILTER_TYPE_GROUP_MEMBERS:
+                return "FILTER_TYPE_GROUP_MEMBERS";
+            case FILTER_TYPE_DEVICE_CONTACTS:
+                return "FILTER_TYPE_DEVICE_CONTACTS";
+            default:
+                return "(unknown)";
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactListFilterController.java b/src/com/android/contacts/common/list/ContactListFilterController.java
new file mode 100644
index 0000000..48d36ed
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListFilterController.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.android.contacts.common.logging.ListEvent;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages {@link ContactListFilter}. All methods must be called from UI thread.
+ */
+public abstract class ContactListFilterController {
+
+    // singleton to cache the filter controller
+    private static ContactListFilterControllerImpl sFilterController = null;
+
+    public interface ContactListFilterListener {
+        void onContactListFilterChanged();
+    }
+
+    public static ContactListFilterController getInstance(Context context) {
+        // We may need to synchronize this in the future if background task will call this.
+        if (sFilterController == null) {
+            sFilterController = new ContactListFilterControllerImpl(context);
+        }
+        return sFilterController;
+    }
+
+    public abstract void addListener(ContactListFilterListener listener);
+
+    public abstract void removeListener(ContactListFilterListener listener);
+
+    /**
+     * Return the currently-active filter.
+     */
+    public abstract ContactListFilter getFilter();
+
+    public abstract int getFilterListType();
+
+    /** Whether the persisted filter is a custom filter. */
+    public abstract boolean isCustomFilterPersisted();
+
+    /** Returns the persisted filter. */
+    public abstract ContactListFilter getPersistedFilter();
+
+    /**
+     * @param filter the filter
+     * @param persistent True when the given filter should be saved soon. False when the filter
+     * should not be saved. The latter case may happen when some Intent requires a certain type of
+     * UI (e.g. single contact) temporarily.
+     */
+    public abstract void setContactListFilter(ContactListFilter filter, boolean persistent);
+
+    public abstract void selectCustomFilter();
+
+    /**
+     * Checks if the current filter is valid and reset the filter if not. It may happen when
+     * an account is removed while the filter points to the account with
+     * {@link ContactListFilter#FILTER_TYPE_ACCOUNT} type, for example. It may also happen if
+     * the current filter is {@link ContactListFilter#FILTER_TYPE_SINGLE_CONTACT}, in
+     * which case, we should switch to the last saved filter in {@link SharedPreferences}.
+     */
+    public abstract void checkFilterValidity(boolean notifyListeners);
+}
+
+/**
+ * Stores the {@link ContactListFilter} selected by the user and saves it to
+ * {@link SharedPreferences} if necessary.
+ */
+class ContactListFilterControllerImpl extends ContactListFilterController {
+    private final Context mContext;
+    private final List<ContactListFilterListener> mListeners =
+            new ArrayList<ContactListFilterListener>();
+    private ContactListFilter mFilter;
+
+    public ContactListFilterControllerImpl(Context context) {
+        mContext = context.getApplicationContext();
+        mFilter = ContactListFilter.restoreDefaultPreferences(getSharedPreferences());
+        checkFilterValidity(true /* notify listeners */);
+    }
+
+    @Override
+    public void addListener(ContactListFilterListener listener) {
+        mListeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(ContactListFilterListener listener) {
+        mListeners.remove(listener);
+    }
+
+    @Override
+    public ContactListFilter getFilter() {
+        return mFilter;
+    }
+
+    @Override
+    public int getFilterListType() {
+        return mFilter == null ? ListEvent.ListType.UNKNOWN_LIST : mFilter.toListType();
+    }
+
+    @Override
+    public boolean isCustomFilterPersisted() {
+        final ContactListFilter filter = getPersistedFilter();
+        return filter != null && filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM;
+    }
+
+    @Override
+    public ContactListFilter getPersistedFilter() {
+        return ContactListFilter.restoreDefaultPreferences(getSharedPreferences());
+    }
+
+    private SharedPreferences getSharedPreferences() {
+        return PreferenceManager.getDefaultSharedPreferences(mContext);
+    }
+
+    @Override
+    public void setContactListFilter(ContactListFilter filter, boolean persistent) {
+        setContactListFilter(filter, persistent, /* notifyListeners */ true);
+    }
+
+    private void setContactListFilter(ContactListFilter filter, boolean persistent,
+            boolean notifyListeners) {
+        if (!filter.equals(mFilter)) {
+            mFilter = filter;
+            if (persistent) {
+                ContactListFilter.storeToPreferences(getSharedPreferences(), mFilter);
+            }
+            if (notifyListeners && !mListeners.isEmpty()) {
+                notifyContactListFilterChanged();
+            }
+        }
+    }
+
+    @Override
+    public void selectCustomFilter() {
+        setContactListFilter(ContactListFilter.createFilterWithType(
+                ContactListFilter.FILTER_TYPE_CUSTOM), /* persistent */ true);
+    }
+
+    private void notifyContactListFilterChanged() {
+        for (ContactListFilterListener listener : mListeners) {
+            listener.onContactListFilterChanged();
+        }
+    }
+
+    @Override
+    public void checkFilterValidity(boolean notifyListeners) {
+        if (mFilter == null) {
+            return;
+        }
+
+        switch (mFilter.filterType) {
+            case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT:
+                setContactListFilter(
+                        ContactListFilter.restoreDefaultPreferences(getSharedPreferences()),
+                        false, notifyListeners);
+                break;
+            case ContactListFilter.FILTER_TYPE_ACCOUNT:
+                if (!filterAccountExists()) {
+                    // The current account filter points to invalid account. Use "all" filter
+                    // instead.
+                    setContactListFilter(ContactListFilter.createFilterWithType(
+                            ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS), true, notifyListeners);
+                }
+                break;
+            case ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS:
+                if (!localAccountExists()) {
+                    setContactListFilter(ContactListFilter.createFilterWithType(
+                            ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS), true, notifyListeners);
+                }
+                break;
+        }
+    }
+
+    /**
+     * @return true if the Account for the current filter exists.
+     */
+    private boolean filterAccountExists() {
+        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
+        final AccountWithDataSet filterAccount = new AccountWithDataSet(
+                mFilter.accountName, mFilter.accountType, mFilter.dataSet);
+        return accountTypeManager.contains(filterAccount, /* contactWritableOnly */ false);
+    }
+
+    /**
+     * @return true if the local account still exists.
+     */
+    private boolean localAccountExists() {
+        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
+        final AccountWithDataSet localAccount = AccountWithDataSet.getLocalAccount();
+        return accountTypeManager.contains(localAccount, /* contactWritableOnly */ false);
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactListFilterView.java b/src/com/android/contacts/common/list/ContactListFilterView.java
new file mode 100644
index 0000000..76e43aa
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListFilterView.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+
+/**
+ * Contact list filter parameters.
+ */
+public class ContactListFilterView extends LinearLayout {
+
+    private static final String TAG = ContactListFilterView.class.getSimpleName();
+
+    private ImageView mIcon;
+    private TextView mAccountType;
+    private TextView mAccountUserName;
+    private RadioButton mRadioButton;
+    private ContactListFilter mFilter;
+    private boolean mSingleAccount;
+
+    public ContactListFilterView(Context context) {
+        super(context);
+    }
+
+    public ContactListFilterView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void setContactListFilter(ContactListFilter filter) {
+        mFilter = filter;
+    }
+
+    public ContactListFilter getContactListFilter() {
+        return mFilter;
+    }
+
+    public void setSingleAccount(boolean flag) {
+        this.mSingleAccount = flag;
+    }
+
+    @Override
+    public void setActivated(boolean activated) {
+        super.setActivated(activated);
+        if (mRadioButton != null) {
+            mRadioButton.setChecked(activated);
+        } else {
+            // We're guarding against null-pointer exceptions,
+            // but otherwise this code is not expected to work
+            // properly if the button hasn't been initialized.
+            Log.wtf(TAG, "radio-button cannot be activated because it is null");
+        }
+        setContentDescription(generateContentDescription());
+    }
+
+    public boolean isChecked() {
+        return mRadioButton.isChecked();
+    }
+
+    public void bindView(AccountTypeManager accountTypes) {
+        if (mAccountType == null) {
+            mIcon = (ImageView) findViewById(R.id.icon);
+            mAccountType = (TextView) findViewById(R.id.accountType);
+            mAccountUserName = (TextView) findViewById(R.id.accountUserName);
+            mRadioButton = (RadioButton) findViewById(R.id.radioButton);
+            mRadioButton.setChecked(isActivated());
+        }
+
+        if (mFilter == null) {
+            mAccountType.setText(R.string.contactsList);
+            return;
+        }
+
+        mAccountUserName.setVisibility(View.GONE);
+        switch (mFilter.filterType) {
+            case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: {
+                bindView(0, R.string.list_filter_all_accounts);
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_STARRED: {
+                bindView(R.drawable.ic_menu_star_holo_light, R.string.list_filter_all_starred);
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_CUSTOM: {
+                bindView(0, R.string.list_filter_customize);
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: {
+                bindView(0, R.string.list_filter_phones);
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: {
+                bindView(0, R.string.list_filter_single);
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_ACCOUNT: {
+                mAccountUserName.setVisibility(View.VISIBLE);
+                mIcon.setVisibility(View.VISIBLE);
+                if (mFilter.icon != null) {
+                    mIcon.setImageDrawable(mFilter.icon);
+                } else {
+                    mIcon.setImageResource(R.drawable.unknown_source);
+                }
+                final AccountType accountType =
+                        accountTypes.getAccountType(mFilter.accountType, mFilter.dataSet);
+                mAccountUserName.setText(mFilter.accountName);
+                mAccountType.setText(accountType.getDisplayLabel(getContext()));
+                break;
+            }
+        }
+        setContentDescription(generateContentDescription());
+    }
+
+    private void bindView(int iconResource, int textResource) {
+        if (iconResource != 0) {
+            mIcon.setVisibility(View.VISIBLE);
+            mIcon.setImageResource(iconResource);
+        } else {
+            mIcon.setVisibility(View.GONE);
+        }
+
+        mAccountType.setText(textResource);
+    }
+
+    String generateContentDescription() {
+        final StringBuilder sb = new StringBuilder();
+        if (!TextUtils.isEmpty(mAccountType.getText())) {
+            sb.append(mAccountType.getText());
+        }
+        if (!TextUtils.isEmpty(mAccountUserName.getText())) {
+            if (sb.length() > 0) {
+                sb.append(" ");
+            }
+            sb.append(mAccountUserName.getText());
+        }
+        return getContext().getString(isActivated() ? R.string.account_filter_view_checked :
+                R.string.account_filter_view_not_checked, sb.toString());
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java
new file mode 100644
index 0000000..bb28858
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListItemView.java
@@ -0,0 +1,1890 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.SearchSnippets;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v7.widget.AppCompatCheckBox;
+import android.support.v7.widget.AppCompatImageButton;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView.SelectionBoundsAdjuster;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.contacts.common.ContactPresenceIconUtil;
+import com.android.contacts.common.ContactStatusUtil;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.format.TextHighlighter;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.contacts.common.util.SearchUtil;
+import com.android.contacts.common.util.ViewUtil;
+
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A custom view for an item in the contact list.
+ * The view contains the contact's photo, a set of text views (for name, status, etc...) and
+ * icons for presence and call.
+ * The view uses no XML file for layout and all the measurements and layouts are done
+ * in the onMeasure and onLayout methods.
+ *
+ * The layout puts the contact's photo on the right side of the view, the call icon (if present)
+ * to the left of the photo, the text lines are aligned to the left and the presence icon (if
+ * present) is set to the left of the status line.
+ *
+ * The layout also supports a header (used as a header of a group of contacts) that is above the
+ * contact's data and a divider between contact view.
+ */
+
+public class ContactListItemView extends ViewGroup
+        implements SelectionBoundsAdjuster {
+
+    private static final String TAG = "ContactListItemView";
+
+    // Style values for layout and appearance
+    // The initialized values are defaults if none is provided through xml.
+    private int mPreferredHeight = 0;
+    private int mGapBetweenImageAndText = 0;
+    private int mGapBetweenIndexerAndImage = 0;
+    private int mGapBetweenLabelAndData = 0;
+    private int mPresenceIconMargin = 4;
+    private int mPresenceIconSize = 16;
+    private int mTextIndent = 0;
+    private int mTextOffsetTop;
+    private int mAvatarOffsetTop;
+    private int mNameTextViewTextSize;
+    private int mHeaderWidth;
+    private Drawable mActivatedBackgroundDrawable;
+    private int mVideoCallIconSize = 32;
+    private int mVideoCallIconMargin = 16;
+    private int mGapFromScrollBar = 20;
+
+    // Set in onLayout. Represent left and right position of the View on the screen.
+    private int mLeftOffset;
+    private int mRightOffset;
+
+    /**
+     * Used with {@link #mLabelView}, specifying the width ratio between label and data.
+     */
+    private int mLabelViewWidthWeight = 3;
+    /**
+     * Used with {@link #mDataView}, specifying the width ratio between label and data.
+     */
+    private int mDataViewWidthWeight = 5;
+
+    protected static class HighlightSequence {
+        private final int start;
+        private final int end;
+
+        HighlightSequence(int start, int end) {
+            this.start = start;
+            this.end = end;
+        }
+    }
+
+    private ArrayList<HighlightSequence> mNameHighlightSequence;
+    private ArrayList<HighlightSequence> mNumberHighlightSequence;
+
+    // Highlighting prefix for names.
+    private String mHighlightedPrefix;
+
+    /**
+     * Used to notify listeners when a video call icon is clicked.
+     */
+    private PhoneNumberListAdapter.Listener mPhoneNumberListAdapterListener;
+
+    /**
+     * Indicates whether to show the "video call" icon, used to initiate a video call.
+     */
+    private boolean mShowVideoCallIcon = false;
+
+    /**
+     * Indicates whether the view should leave room for the "video call" icon.
+     */
+    private boolean mSupportVideoCallIcon = false;
+
+    /**
+     * Where to put contact photo. This affects the other Views' layout or look-and-feel.
+     *
+     * TODO: replace enum with int constants
+     */
+    public enum PhotoPosition {
+        LEFT,
+        RIGHT
+    }
+
+    static public final PhotoPosition getDefaultPhotoPosition(boolean opposite) {
+        final Locale locale = Locale.getDefault();
+        final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
+        switch (layoutDirection) {
+            case View.LAYOUT_DIRECTION_RTL:
+                return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT);
+            case View.LAYOUT_DIRECTION_LTR:
+            default:
+                return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT);
+        }
+    }
+
+    private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */);
+
+    // Header layout data
+    private View mHeaderView;
+    private boolean mIsSectionHeaderEnabled;
+
+    // The views inside the contact view
+    private boolean mQuickContactEnabled = true;
+    private QuickContactBadge mQuickContact;
+    private ImageView mPhotoView;
+    private TextView mNameTextView;
+    private TextView mPhoneticNameTextView;
+    private TextView mLabelView;
+    private TextView mDataView;
+    private TextView mSnippetView;
+    private TextView mStatusView;
+    private ImageView mPresenceIcon;
+    private AppCompatCheckBox mCheckBox;
+    private AppCompatImageButton mDeleteImageButton;
+    private ImageView mVideoCallIcon;
+    private ImageView mWorkProfileIcon;
+
+    private ColorStateList mSecondaryTextColor;
+
+    private int mDefaultPhotoViewSize = 0;
+    /**
+     * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
+     * to align other data in this View.
+     */
+    private int mPhotoViewWidth;
+    /**
+     * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
+     */
+    private int mPhotoViewHeight;
+
+    /**
+     * Only effective when {@link #mPhotoView} is null.
+     * When true all the Views on the right side of the photo should have horizontal padding on
+     * those left assuming there is a photo.
+     */
+    private boolean mKeepHorizontalPaddingForPhotoView;
+    /**
+     * Only effective when {@link #mPhotoView} is null.
+     */
+    private boolean mKeepVerticalPaddingForPhotoView;
+
+    /**
+     * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
+     * False indicates those values should be updated before being used in position calculation.
+     */
+    private boolean mPhotoViewWidthAndHeightAreReady = false;
+
+    private int mNameTextViewHeight;
+    private int mNameTextViewTextColor = Color.BLACK;
+    private int mPhoneticNameTextViewHeight;
+    private int mLabelViewHeight;
+    private int mDataViewHeight;
+    private int mSnippetTextViewHeight;
+    private int mStatusTextViewHeight;
+    private int mCheckBoxHeight;
+    private int mCheckBoxWidth;
+    private int mDeleteImageButtonHeight;
+    private int mDeleteImageButtonWidth;
+
+    // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
+    // same row.
+    private int mLabelAndDataViewMaxHeight;
+
+    // TODO: some TextView fields are using CharArrayBuffer while some are not. Determine which is
+    // more efficient for each case or in general, and simplify the whole implementation.
+    // Note: if we're sure MARQUEE will be used every time, there's no reason to use
+    // CharArrayBuffer, since MARQUEE requires Span and thus we need to copy characters inside the
+    // buffer to Spannable once, while CharArrayBuffer is for directly applying char array to
+    // TextView without any modification.
+    private final CharArrayBuffer mDataBuffer = new CharArrayBuffer(128);
+    private final CharArrayBuffer mPhoneticNameBuffer = new CharArrayBuffer(128);
+
+    private boolean mActivatedStateSupported;
+    private boolean mAdjustSelectionBoundsEnabled = true;
+
+    private Rect mBoundsWithoutHeader = new Rect();
+
+    /** A helper used to highlight a prefix in a text field. */
+    private final TextHighlighter mTextHighlighter;
+    private CharSequence mUnknownNameText;
+    private int mPosition;
+
+    public ContactListItemView(Context context) {
+        super(context);
+
+        mTextHighlighter = new TextHighlighter(Typeface.BOLD);
+        mNameHighlightSequence = new ArrayList<HighlightSequence>();
+        mNumberHighlightSequence = new ArrayList<HighlightSequence>();
+    }
+
+    public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) {
+        this(context, attrs);
+
+        mSupportVideoCallIcon = supportVideoCallIcon;
+    }
+
+    public ContactListItemView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        TypedArray a;
+
+        if (R.styleable.ContactListItemView != null) {
+            // Read all style values
+            a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
+            mPreferredHeight = a.getDimensionPixelSize(
+                    R.styleable.ContactListItemView_list_item_height, mPreferredHeight);
+            mActivatedBackgroundDrawable = a.getDrawable(
+                    R.styleable.ContactListItemView_activated_background);
+
+            mGapBetweenImageAndText = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_gap_between_image_and_text,
+                    mGapBetweenImageAndText);
+            mGapBetweenIndexerAndImage = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_gap_between_indexer_and_image,
+                    mGapBetweenIndexerAndImage);
+            mGapBetweenLabelAndData = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_gap_between_label_and_data,
+                    mGapBetweenLabelAndData);
+            mPresenceIconMargin = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_presence_icon_margin,
+                    mPresenceIconMargin);
+            mPresenceIconSize = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_presence_icon_size,
+                    mPresenceIconSize);
+            mDefaultPhotoViewSize = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize);
+            mTextIndent = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_text_indent, mTextIndent);
+            mTextOffsetTop = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop);
+            mAvatarOffsetTop = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_avatar_offset_top, mAvatarOffsetTop);
+            mDataViewWidthWeight = a.getInteger(
+                    R.styleable.ContactListItemView_list_item_data_width_weight,
+                    mDataViewWidthWeight);
+            mLabelViewWidthWeight = a.getInteger(
+                    R.styleable.ContactListItemView_list_item_label_width_weight,
+                    mLabelViewWidthWeight);
+            mNameTextViewTextColor = a.getColor(
+                    R.styleable.ContactListItemView_list_item_name_text_color,
+                    mNameTextViewTextColor);
+            mNameTextViewTextSize = (int) a.getDimension(
+                    R.styleable.ContactListItemView_list_item_name_text_size,
+                    (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size));
+            mVideoCallIconSize = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_video_call_icon_size,
+                    mVideoCallIconSize);
+            mVideoCallIconMargin = a.getDimensionPixelOffset(
+                    R.styleable.ContactListItemView_list_item_video_call_icon_margin,
+                    mVideoCallIconMargin);
+
+
+            setPaddingRelative(
+                    a.getDimensionPixelOffset(
+                            R.styleable.ContactListItemView_list_item_padding_left, 0),
+                    a.getDimensionPixelOffset(
+                            R.styleable.ContactListItemView_list_item_padding_top, 0),
+                    a.getDimensionPixelOffset(
+                            R.styleable.ContactListItemView_list_item_padding_right, 0),
+                    a.getDimensionPixelOffset(
+                            R.styleable.ContactListItemView_list_item_padding_bottom, 0));
+
+            a.recycle();
+        }
+
+        mTextHighlighter = new TextHighlighter(Typeface.BOLD);
+
+        if (R.styleable.Theme != null) {
+            a = getContext().obtainStyledAttributes(R.styleable.Theme);
+            mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary);
+            a.recycle();
+        }
+
+        mHeaderWidth =
+                getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width);
+
+        if (mActivatedBackgroundDrawable != null) {
+            mActivatedBackgroundDrawable.setCallback(this);
+        }
+
+        mNameHighlightSequence = new ArrayList<HighlightSequence>();
+        mNumberHighlightSequence = new ArrayList<HighlightSequence>();
+
+        setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
+    }
+
+    public void setUnknownNameText(CharSequence unknownNameText) {
+        mUnknownNameText = unknownNameText;
+    }
+
+    public void setQuickContactEnabled(boolean flag) {
+        mQuickContactEnabled = flag;
+    }
+
+    /**
+     * Sets whether the video calling icon is shown.  For the video calling icon to be shown,
+     * {@link #mSupportVideoCallIcon} must be {@code true}.
+     *
+     * @param showVideoCallIcon {@code true} if the video calling icon is shown, {@code false}
+     *      otherwise.
+     * @param listener Listener to notify when the video calling icon is clicked.
+     * @param position The position in the adapater of the video calling icon.
+     */
+    public void setShowVideoCallIcon(boolean showVideoCallIcon,
+            PhoneNumberListAdapter.Listener listener, int position) {
+        mShowVideoCallIcon = showVideoCallIcon;
+        mPhoneNumberListAdapterListener = listener;
+        mPosition = position;
+
+        if (mShowVideoCallIcon) {
+            if (mVideoCallIcon == null) {
+                mVideoCallIcon = new ImageView(getContext());
+                addView(mVideoCallIcon);
+            }
+            mVideoCallIcon.setContentDescription(getContext().getString(
+                    R.string.description_search_video_call));
+            mVideoCallIcon.setImageResource(R.drawable.ic_search_video_call);
+            mVideoCallIcon.setScaleType(ScaleType.CENTER);
+            mVideoCallIcon.setVisibility(View.VISIBLE);
+            mVideoCallIcon.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    // Inform the adapter that the video calling icon was clicked.
+                    if (mPhoneNumberListAdapterListener != null) {
+                        mPhoneNumberListAdapterListener.onVideoCallIconClicked(mPosition);
+                    }
+                }
+            });
+        } else {
+            if (mVideoCallIcon != null) {
+                mVideoCallIcon.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    /**
+     * Sets whether the view supports a video calling icon.  This is independent of whether the view
+     * is actually showing an icon.  Support for the video calling icon ensures that the layout
+     * leaves space for the video icon, should it be shown.
+     *
+     * @param supportVideoCallIcon {@code true} if the video call icon is supported, {@code false}
+     *      otherwise.
+     */
+    public void setSupportVideoCallIcon(boolean supportVideoCallIcon) {
+        mSupportVideoCallIcon = supportVideoCallIcon;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // We will match parent's width and wrap content vertically, but make sure
+        // height is no less than listPreferredItemHeight.
+        final int specWidth = resolveSize(0, widthMeasureSpec);
+        final int preferredHeight = mPreferredHeight;
+
+        mNameTextViewHeight = 0;
+        mPhoneticNameTextViewHeight = 0;
+        mLabelViewHeight = 0;
+        mDataViewHeight = 0;
+        mLabelAndDataViewMaxHeight = 0;
+        mSnippetTextViewHeight = 0;
+        mStatusTextViewHeight = 0;
+        mCheckBoxWidth = 0;
+        mCheckBoxHeight = 0;
+        mDeleteImageButtonWidth = 0;
+        mDeleteImageButtonHeight = 0;
+
+        ensurePhotoViewSize();
+
+        // Width each TextView is able to use.
+        int effectiveWidth;
+        // All the other Views will honor the photo, so available width for them may be shrunk.
+        if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
+            effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight()
+                    - (mPhotoViewWidth + mGapBetweenImageAndText);
+        } else {
+            effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
+        }
+
+        if (mIsSectionHeaderEnabled) {
+            effectiveWidth -= mHeaderWidth + mGapBetweenIndexerAndImage;
+        }
+
+        if (mSupportVideoCallIcon) {
+            effectiveWidth -= (mVideoCallIconSize + mVideoCallIconMargin);
+        }
+
+        // Go over all visible text views and measure actual width of each of them.
+        // Also calculate their heights to get the total height for this entire view.
+
+        if (isVisible(mCheckBox)) {
+            mCheckBox.measure(
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mCheckBoxWidth = mCheckBox.getMeasuredWidth();
+            mCheckBoxHeight = mCheckBox.getMeasuredHeight();
+            effectiveWidth -= mCheckBoxWidth + mGapBetweenImageAndText;
+        }
+
+        if (isVisible(mDeleteImageButton)) {
+            mDeleteImageButton.measure(
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mDeleteImageButtonWidth = mDeleteImageButton.getMeasuredWidth();
+            mDeleteImageButtonHeight = mDeleteImageButton.getMeasuredHeight();
+            effectiveWidth -= mDeleteImageButtonWidth + mGapBetweenImageAndText;
+        }
+
+        if (isVisible(mNameTextView)) {
+            // Calculate width for name text - this parallels similar measurement in onLayout.
+            int nameTextWidth = effectiveWidth;
+            if (mPhotoPosition != PhotoPosition.LEFT) {
+                nameTextWidth -= mTextIndent;
+            }
+            mNameTextView.measure(
+                    MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mNameTextViewHeight = mNameTextView.getMeasuredHeight();
+        }
+
+        if (isVisible(mPhoneticNameTextView)) {
+            mPhoneticNameTextView.measure(
+                    MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mPhoneticNameTextViewHeight = mPhoneticNameTextView.getMeasuredHeight();
+        }
+
+        // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
+        // we should ellipsize both using appropriate ratio.
+        final int dataWidth;
+        final int labelWidth;
+        if (isVisible(mDataView)) {
+            if (isVisible(mLabelView)) {
+                final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
+                dataWidth = ((totalWidth * mDataViewWidthWeight)
+                        / (mDataViewWidthWeight + mLabelViewWidthWeight));
+                labelWidth = ((totalWidth * mLabelViewWidthWeight) /
+                        (mDataViewWidthWeight + mLabelViewWidthWeight));
+            } else {
+                dataWidth = effectiveWidth;
+                labelWidth = 0;
+            }
+        } else {
+            dataWidth = 0;
+            if (isVisible(mLabelView)) {
+                labelWidth = effectiveWidth;
+            } else {
+                labelWidth = 0;
+            }
+        }
+
+        if (isVisible(mDataView)) {
+            mDataView.measure(MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mDataViewHeight = mDataView.getMeasuredHeight();
+        }
+
+        if (isVisible(mLabelView)) {
+            mLabelView.measure(MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mLabelViewHeight = mLabelView.getMeasuredHeight();
+        }
+        mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
+
+        if (isVisible(mSnippetView)) {
+            mSnippetView.measure(
+                    MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
+        }
+
+        // Status view height is the biggest of the text view and the presence icon
+        if (isVisible(mPresenceIcon)) {
+            mPresenceIcon.measure(
+                    MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
+            mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
+        }
+
+        if (mSupportVideoCallIcon && isVisible(mVideoCallIcon)) {
+            mVideoCallIcon.measure(
+                    MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY));
+        }
+
+        if (isVisible(mWorkProfileIcon)) {
+            mWorkProfileIcon.measure(
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mNameTextViewHeight =
+                    Math.max(mNameTextViewHeight, mWorkProfileIcon.getMeasuredHeight());
+        }
+
+        if (isVisible(mStatusView)) {
+            // Presence and status are in a same row, so status will be affected by icon size.
+            final int statusWidth;
+            if (isVisible(mPresenceIcon)) {
+                statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth()
+                        - mPresenceIconMargin);
+            } else {
+                statusWidth = effectiveWidth;
+            }
+            mStatusView.measure(MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+            mStatusTextViewHeight =
+                    Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
+        }
+
+        // Calculate height including padding.
+        int height = (mNameTextViewHeight + mPhoneticNameTextViewHeight +
+                mLabelAndDataViewMaxHeight +
+                mSnippetTextViewHeight + mStatusTextViewHeight);
+
+        // Make sure the height is at least as high as the photo
+        height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
+
+        // Make sure height is at least the preferred height
+        height = Math.max(height, preferredHeight);
+
+        // Measure the header if it is visible.
+        if (mHeaderView != null && mHeaderView.getVisibility() == VISIBLE) {
+            mHeaderView.measure(
+                    MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+        }
+
+        setMeasuredDimension(specWidth, height);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final int height = bottom - top;
+        final int width = right - left;
+
+        // Determine the vertical bounds by laying out the header first.
+        int topBound = 0;
+        int bottomBound = height;
+        int leftBound = getPaddingLeft();
+        int rightBound = width - getPaddingRight();
+
+        final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this);
+
+        // Put the section header on the left side of the contact view.
+        if (mIsSectionHeaderEnabled) {
+            if (mHeaderView != null) {
+                int headerHeight = mHeaderView.getMeasuredHeight();
+                int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop;
+
+                mHeaderView.layout(
+                        isLayoutRtl ? rightBound - mHeaderWidth : leftBound,
+                        headerTopBound,
+                        isLayoutRtl ? rightBound : leftBound + mHeaderWidth,
+                        headerTopBound + headerHeight);
+            }
+            if (isLayoutRtl) {
+                rightBound -= mHeaderWidth;
+            } else {
+                leftBound += mHeaderWidth;
+            }
+        }
+
+        mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound);
+        mLeftOffset = left + leftBound;
+        mRightOffset = left + rightBound;
+        if (isLayoutRtl) {
+            rightBound -= mGapBetweenIndexerAndImage;
+        } else {
+            leftBound += mGapBetweenIndexerAndImage;
+        }
+
+        if (mActivatedStateSupported && isActivated()) {
+            mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
+        }
+
+        if (isVisible(mCheckBox)) {
+            final int photoTop = topBound + (bottomBound - topBound - mCheckBoxHeight) / 2;
+            if (mPhotoPosition == PhotoPosition.LEFT) {
+                mCheckBox.layout(rightBound - mGapFromScrollBar - mCheckBoxWidth,
+                        photoTop,
+                        rightBound - mGapFromScrollBar,
+                        photoTop + mCheckBoxHeight);
+            } else {
+                mCheckBox.layout(leftBound + mGapFromScrollBar,
+                        photoTop,
+                        leftBound + mGapFromScrollBar + mCheckBoxWidth,
+                        photoTop + mCheckBoxHeight);
+            }
+        }
+
+        if (isVisible(mDeleteImageButton)) {
+            final int photoTop = topBound + (bottomBound - topBound - mDeleteImageButtonHeight) / 2;
+            final int mDeleteImageButtonSize = mDeleteImageButtonHeight > mDeleteImageButtonWidth
+                    ? mDeleteImageButtonHeight : mDeleteImageButtonWidth;
+            if (mPhotoPosition == PhotoPosition.LEFT) {
+                mDeleteImageButton.layout(rightBound - mDeleteImageButtonSize,
+                        photoTop,
+                        rightBound,
+                        photoTop + mDeleteImageButtonSize);
+            } else {
+                mDeleteImageButton.layout(leftBound,
+                        photoTop,
+                        leftBound + mDeleteImageButtonSize,
+                        photoTop + mDeleteImageButtonSize);
+            }
+        }
+
+        final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
+        if (mPhotoPosition == PhotoPosition.LEFT) {
+            // Photo is the left most view. All the other Views should on the right of the photo.
+            if (photoView != null) {
+                // Center the photo vertically
+                final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2
+                        + mAvatarOffsetTop;
+                photoView.layout(
+                        leftBound,
+                        photoTop,
+                        leftBound + mPhotoViewWidth,
+                        photoTop + mPhotoViewHeight);
+                leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+            } else if (mKeepHorizontalPaddingForPhotoView) {
+                // Draw nothing but keep the padding.
+                leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+            }
+        } else {
+            // Photo is the right most view. Right bound should be adjusted that way.
+            if (photoView != null) {
+                // Center the photo vertically
+                final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2
+                        + mAvatarOffsetTop;
+                photoView.layout(
+                        rightBound - mPhotoViewWidth,
+                        photoTop,
+                        rightBound,
+                        photoTop + mPhotoViewHeight);
+                rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
+            } else if (mKeepHorizontalPaddingForPhotoView) {
+                // Draw nothing but keep the padding.
+                rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
+            }
+
+            // Add indent between left-most padding and texts.
+            leftBound += mTextIndent;
+        }
+
+        if (mSupportVideoCallIcon) {
+            // Place the video call button at the end of the list (e.g. take into account RTL mode).
+            if (isVisible(mVideoCallIcon)) {
+                // Center the video icon vertically
+                final int videoIconTop = topBound +
+                        (bottomBound - topBound - mVideoCallIconSize) / 2;
+
+                if (!isLayoutRtl) {
+                    // When photo is on left, video icon is placed on the right edge.
+                    mVideoCallIcon.layout(rightBound - mVideoCallIconSize,
+                            videoIconTop,
+                            rightBound,
+                            videoIconTop + mVideoCallIconSize);
+                } else {
+                    // When photo is on right, video icon is placed on the left edge.
+                    mVideoCallIcon.layout(leftBound,
+                            videoIconTop,
+                            leftBound + mVideoCallIconSize,
+                            videoIconTop + mVideoCallIconSize);
+                }
+            }
+
+            if (mPhotoPosition == PhotoPosition.LEFT) {
+                rightBound -= (mVideoCallIconSize + mVideoCallIconMargin);
+            } else {
+                leftBound += mVideoCallIconSize + mVideoCallIconMargin;
+            }
+        }
+
+
+        // Center text vertically, then apply the top offset.
+        final int totalTextHeight = mNameTextViewHeight + mPhoneticNameTextViewHeight +
+                mLabelAndDataViewMaxHeight + mSnippetTextViewHeight + mStatusTextViewHeight;
+        int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop;
+
+        // Work Profile icon align top
+        int workProfileIconWidth = 0;
+        if (isVisible(mWorkProfileIcon)) {
+            workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth();
+            final int distanceFromEnd = mCheckBoxWidth > 0
+                    ? mCheckBoxWidth + mGapBetweenImageAndText : 0;
+            if (mPhotoPosition == PhotoPosition.LEFT) {
+                // When photo is on left, label is placed on the right edge of the list item.
+                mWorkProfileIcon.layout(rightBound - workProfileIconWidth - distanceFromEnd,
+                        textTopBound,
+                        rightBound - distanceFromEnd,
+                        textTopBound + mNameTextViewHeight);
+            } else {
+                // When photo is on right, label is placed on the left of data view.
+                mWorkProfileIcon.layout(leftBound + distanceFromEnd,
+                        textTopBound,
+                        leftBound + workProfileIconWidth + distanceFromEnd,
+                        textTopBound + mNameTextViewHeight);
+            }
+        }
+
+        // Layout all text view and presence icon
+        // Put name TextView first
+        if (isVisible(mNameTextView)) {
+            final int distanceFromEnd = workProfileIconWidth
+                    + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0);
+            if (mPhotoPosition == PhotoPosition.LEFT) {
+                mNameTextView.layout(leftBound,
+                        textTopBound,
+                        rightBound - distanceFromEnd,
+                        textTopBound + mNameTextViewHeight);
+            } else {
+                mNameTextView.layout(leftBound + distanceFromEnd,
+                        textTopBound,
+                        rightBound,
+                        textTopBound + mNameTextViewHeight);
+            }
+        }
+
+        if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) {
+            textTopBound += mNameTextViewHeight;
+        }
+
+        // Presence and status
+        if (isLayoutRtl) {
+            int statusRightBound = rightBound;
+            if (isVisible(mPresenceIcon)) {
+                int iconWidth = mPresenceIcon.getMeasuredWidth();
+                mPresenceIcon.layout(
+                        rightBound - iconWidth,
+                        textTopBound,
+                        rightBound,
+                        textTopBound + mStatusTextViewHeight);
+                statusRightBound -= (iconWidth + mPresenceIconMargin);
+            }
+
+            if (isVisible(mStatusView)) {
+                mStatusView.layout(leftBound,
+                        textTopBound,
+                        statusRightBound,
+                        textTopBound + mStatusTextViewHeight);
+            }
+        } else {
+            int statusLeftBound = leftBound;
+            if (isVisible(mPresenceIcon)) {
+                int iconWidth = mPresenceIcon.getMeasuredWidth();
+                mPresenceIcon.layout(
+                        leftBound,
+                        textTopBound,
+                        leftBound + iconWidth,
+                        textTopBound + mStatusTextViewHeight);
+                statusLeftBound += (iconWidth + mPresenceIconMargin);
+            }
+
+            if (isVisible(mStatusView)) {
+                mStatusView.layout(statusLeftBound,
+                        textTopBound,
+                        rightBound,
+                        textTopBound + mStatusTextViewHeight);
+            }
+        }
+
+        if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
+            textTopBound += mStatusTextViewHeight;
+        }
+
+        // Rest of text views
+        int dataLeftBound = leftBound;
+        if (isVisible(mPhoneticNameTextView)) {
+            mPhoneticNameTextView.layout(leftBound,
+                    textTopBound,
+                    rightBound,
+                    textTopBound + mPhoneticNameTextViewHeight);
+            textTopBound += mPhoneticNameTextViewHeight;
+        }
+
+        // Label and Data align bottom.
+        if (isVisible(mLabelView)) {
+            if (!isLayoutRtl) {
+                mLabelView.layout(dataLeftBound,
+                        textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
+                        rightBound,
+                        textTopBound + mLabelAndDataViewMaxHeight);
+                dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData;
+            } else {
+                dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
+                mLabelView.layout(rightBound - mLabelView.getMeasuredWidth(),
+                        textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
+                        rightBound,
+                        textTopBound + mLabelAndDataViewMaxHeight);
+                rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData);
+            }
+        }
+
+        if (isVisible(mDataView)) {
+            if (!isLayoutRtl) {
+                mDataView.layout(dataLeftBound,
+                        textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
+                        rightBound,
+                        textTopBound + mLabelAndDataViewMaxHeight);
+            } else {
+                mDataView.layout(rightBound - mDataView.getMeasuredWidth(),
+                        textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
+                        rightBound,
+                        textTopBound + mLabelAndDataViewMaxHeight);
+            }
+        }
+        if (isVisible(mLabelView) || isVisible(mDataView)) {
+            textTopBound += mLabelAndDataViewMaxHeight;
+        }
+
+        if (isVisible(mSnippetView)) {
+            mSnippetView.layout(leftBound,
+                    textTopBound,
+                    rightBound,
+                    textTopBound + mSnippetTextViewHeight);
+        }
+    }
+
+    @Override
+    public void adjustListItemSelectionBounds(Rect bounds) {
+        if (mAdjustSelectionBoundsEnabled) {
+            bounds.top += mBoundsWithoutHeader.top;
+            bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
+            bounds.left = mBoundsWithoutHeader.left;
+            bounds.right = mBoundsWithoutHeader.right;
+        }
+    }
+
+    protected boolean isVisible(View view) {
+        return view != null && view.getVisibility() == View.VISIBLE;
+    }
+
+    /**
+     * Extracts width and height from the style
+     */
+    private void ensurePhotoViewSize() {
+        if (!mPhotoViewWidthAndHeightAreReady) {
+            mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
+            if (!mQuickContactEnabled && mPhotoView == null) {
+                if (!mKeepHorizontalPaddingForPhotoView) {
+                    mPhotoViewWidth = 0;
+                }
+                if (!mKeepVerticalPaddingForPhotoView) {
+                    mPhotoViewHeight = 0;
+                }
+            }
+
+            mPhotoViewWidthAndHeightAreReady = true;
+        }
+    }
+
+    protected int getDefaultPhotoViewSize() {
+        return mDefaultPhotoViewSize;
+    }
+
+    /**
+     * Gets a LayoutParam that corresponds to the default photo size.
+     *
+     * @return A new LayoutParam.
+     */
+    private LayoutParams getDefaultPhotoLayoutParams() {
+        LayoutParams params = generateDefaultLayoutParams();
+        params.width = getDefaultPhotoViewSize();
+        params.height = params.width;
+        return params;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        if (mActivatedStateSupported) {
+            mActivatedBackgroundDrawable.setState(getDrawableState());
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(Drawable who) {
+        return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+        if (mActivatedStateSupported) {
+            mActivatedBackgroundDrawable.jumpToCurrentState();
+        }
+    }
+
+    @Override
+    public void dispatchDraw(Canvas canvas) {
+        if (mActivatedStateSupported && isActivated()) {
+            mActivatedBackgroundDrawable.draw(canvas);
+        }
+
+        super.dispatchDraw(canvas);
+    }
+
+    /**
+     * Sets section header or makes it invisible if the title is null.
+     */
+    public void setSectionHeader(String title) {
+        if (!TextUtils.isEmpty(title)) {
+            if (TextUtils.equals(getContext().getString(R.string.star_sign), title)) {
+                if (mHeaderView == null) {
+                    addStarImageHeader();
+                } else if (mHeaderView instanceof TextView) {
+                    removeView(mHeaderView);
+                    addStarImageHeader();
+                } else {
+                    mHeaderView.setVisibility(View.VISIBLE);
+                }
+            } else {
+                if (mHeaderView == null) {
+                    addTextHeader(title);
+                } else if (mHeaderView instanceof ImageView) {
+                    removeView(mHeaderView);
+                    addTextHeader(title);
+                } else {
+                    updateHeaderText((TextView) mHeaderView, title);
+                }
+            }
+        } else if (mHeaderView != null) {
+            mHeaderView.setVisibility(View.GONE);
+        }
+    }
+
+    private void addTextHeader(String title) {
+        mHeaderView = new TextView(getContext());
+        final TextView headerTextView = (TextView) mHeaderView;
+        headerTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle);
+        headerTextView.setGravity(Gravity.CENTER_HORIZONTAL);
+        updateHeaderText(headerTextView, title);
+        addView(headerTextView);
+    }
+
+    private void updateHeaderText(TextView headerTextView, String title) {
+        setMarqueeText(headerTextView, title);
+        headerTextView.setAllCaps(true);
+        if (ContactsSectionIndexer.BLANK_HEADER_STRING.equals(title)) {
+            headerTextView.setContentDescription(
+                    getContext().getString(R.string.description_no_name_header));
+        } else {
+            headerTextView.setContentDescription(title);
+        }
+        headerTextView.setVisibility(View.VISIBLE);
+    }
+
+    private void addStarImageHeader() {
+        mHeaderView = new ImageView(getContext());
+        final ImageView headerImageView = (ImageView) mHeaderView;
+        headerImageView.setImageDrawable(
+                getResources().getDrawable(R.drawable.ic_material_star, getContext().getTheme()));
+        headerImageView.setImageTintList(ColorStateList.valueOf(getResources()
+                .getColor(R.color.material_star_pink)));
+        headerImageView.setContentDescription(
+                getContext().getString(R.string.list_filter_all_starred));
+        headerImageView.setVisibility(View.VISIBLE);
+        addView(headerImageView);
+    }
+
+    public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) {
+        mIsSectionHeaderEnabled = isSectionHeaderEnabled;
+    }
+
+    /**
+     * Returns the quick contact badge, creating it if necessary.
+     */
+    public QuickContactBadge getQuickContact() {
+        if (!mQuickContactEnabled) {
+            throw new IllegalStateException("QuickContact is disabled for this view");
+        }
+        if (mQuickContact == null) {
+            mQuickContact = new QuickContactBadge(getContext());
+            if (CompatUtils.isLollipopCompatible()) {
+                mQuickContact.setOverlay(null);
+            }
+            mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
+            if (mNameTextView != null) {
+                mQuickContact.setContentDescription(getContext().getString(
+                        R.string.description_quick_contact_for, mNameTextView.getText()));
+            }
+
+            addView(mQuickContact);
+            mPhotoViewWidthAndHeightAreReady = false;
+        }
+        return mQuickContact;
+    }
+
+    /**
+     * Returns the photo view, creating it if necessary.
+     */
+    public ImageView getPhotoView() {
+        if (mPhotoView == null) {
+            mPhotoView = new ImageView(getContext());
+            mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
+            // Quick contact style used above will set a background - remove it
+            mPhotoView.setBackground(null);
+            addView(mPhotoView);
+            mPhotoViewWidthAndHeightAreReady = false;
+        }
+        return mPhotoView;
+    }
+
+    /**
+     * Removes the photo view.
+     */
+    public void removePhotoView() {
+        removePhotoView(false, true);
+    }
+
+    /**
+     * Removes the photo view.
+     *
+     * @param keepHorizontalPadding True means data on the right side will have
+     *            padding on left, pretending there is still a photo view.
+     * @param keepVerticalPadding True means the View will have some height
+     *            enough for accommodating a photo view.
+     */
+    public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
+        mPhotoViewWidthAndHeightAreReady = false;
+        mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
+        mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
+        if (mPhotoView != null) {
+            removeView(mPhotoView);
+            mPhotoView = null;
+        }
+        if (mQuickContact != null) {
+            removeView(mQuickContact);
+            mQuickContact = null;
+        }
+    }
+
+    /**
+     * Sets a word prefix that will be highlighted if encountered in fields like
+     * name and search snippet. This will disable the mask highlighting for names.
+     * <p>
+     * NOTE: must be all upper-case
+     */
+    public void setHighlightedPrefix(String upperCasePrefix) {
+        mHighlightedPrefix = upperCasePrefix;
+    }
+
+    /**
+     * Clears previously set highlight sequences for the view.
+     */
+    public void clearHighlightSequences() {
+        mNameHighlightSequence.clear();
+        mNumberHighlightSequence.clear();
+        mHighlightedPrefix = null;
+    }
+
+    /**
+     * Adds a highlight sequence to the name highlighter.
+     * @param start The start position of the highlight sequence.
+     * @param end The end position of the highlight sequence.
+     */
+    public void addNameHighlightSequence(int start, int end) {
+        mNameHighlightSequence.add(new HighlightSequence(start, end));
+    }
+
+    /**
+     * Adds a highlight sequence to the number highlighter.
+     * @param start The start position of the highlight sequence.
+     * @param end The end position of the highlight sequence.
+     */
+    public void addNumberHighlightSequence(int start, int end) {
+        mNumberHighlightSequence.add(new HighlightSequence(start, end));
+    }
+
+    /**
+     * Returns the text view for the contact name, creating it if necessary.
+     */
+    public TextView getNameTextView() {
+        if (mNameTextView == null) {
+            mNameTextView = new TextView(getContext());
+            mNameTextView.setSingleLine(true);
+            mNameTextView.setEllipsize(getTextEllipsis());
+            mNameTextView.setTextColor(mNameTextViewTextColor);
+            mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize);
+            // Manually call setActivated() since this view may be added after the first
+            // setActivated() call toward this whole item view.
+            mNameTextView.setActivated(isActivated());
+            mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
+            mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+            mNameTextView.setId(R.id.cliv_name_textview);
+            if (CompatUtils.isLollipopCompatible()) {
+                mNameTextView.setElegantTextHeight(false);
+            }
+            addView(mNameTextView);
+        }
+        return mNameTextView;
+    }
+
+    /**
+     * Adds or updates a text view for the phonetic name.
+     */
+    public void setPhoneticName(char[] text, int size) {
+        if (text == null || size == 0) {
+            if (mPhoneticNameTextView != null) {
+                mPhoneticNameTextView.setVisibility(View.GONE);
+            }
+        } else {
+            getPhoneticNameTextView();
+            setMarqueeText(mPhoneticNameTextView, text, size);
+            mPhoneticNameTextView.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Returns the text view for the phonetic name, creating it if necessary.
+     */
+    public TextView getPhoneticNameTextView() {
+        if (mPhoneticNameTextView == null) {
+            mPhoneticNameTextView = new TextView(getContext());
+            mPhoneticNameTextView.setSingleLine(true);
+            mPhoneticNameTextView.setEllipsize(getTextEllipsis());
+            mPhoneticNameTextView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
+            mPhoneticNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+            mPhoneticNameTextView.setTypeface(mPhoneticNameTextView.getTypeface(), Typeface.BOLD);
+            mPhoneticNameTextView.setActivated(isActivated());
+            mPhoneticNameTextView.setId(R.id.cliv_phoneticname_textview);
+            addView(mPhoneticNameTextView);
+        }
+        return mPhoneticNameTextView;
+    }
+
+    /**
+     * Adds or updates a text view for the data label.
+     */
+    public void setLabel(CharSequence text) {
+        if (TextUtils.isEmpty(text)) {
+            if (mLabelView != null) {
+                mLabelView.setVisibility(View.GONE);
+            }
+        } else {
+            getLabelView();
+            setMarqueeText(mLabelView, text);
+            mLabelView.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Returns the text view for the data label, creating it if necessary.
+     */
+    public TextView getLabelView() {
+        if (mLabelView == null) {
+            mLabelView = new TextView(getContext());
+            mLabelView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+                    LayoutParams.WRAP_CONTENT));
+
+            mLabelView.setSingleLine(true);
+            mLabelView.setEllipsize(getTextEllipsis());
+            mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
+            if (mPhotoPosition == PhotoPosition.LEFT) {
+                mLabelView.setAllCaps(true);
+            } else {
+                mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
+            }
+            mLabelView.setActivated(isActivated());
+            mLabelView.setId(R.id.cliv_label_textview);
+            addView(mLabelView);
+        }
+        return mLabelView;
+    }
+
+    /**
+     * Adds or updates a text view for the data element.
+     */
+    public void setData(char[] text, int size) {
+        if (text == null || size == 0) {
+            if (mDataView != null) {
+                mDataView.setVisibility(View.GONE);
+            }
+        } else {
+            getDataView();
+            setMarqueeText(mDataView, text, size);
+            mDataView.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Sets phone number for a list item. This takes care of number highlighting if the highlight
+     * mask exists.
+     */
+    public void setPhoneNumber(String text, String countryIso) {
+        if (text == null) {
+            if (mDataView != null) {
+                mDataView.setVisibility(View.GONE);
+            }
+        } else {
+            getDataView();
+
+            // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to
+            // mDataView. Make sure that determination of the highlight sequences are done only
+            // after number formatting.
+
+            // Sets phone number texts for display after highlighting it, if applicable.
+            // CharSequence textToSet = text;
+            final SpannableString textToSet = new SpannableString(text);
+
+            if (mNumberHighlightSequence.size() != 0) {
+                final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0);
+                mTextHighlighter.applyMaskingHighlight(textToSet, highlightSequence.start,
+                        highlightSequence.end);
+            }
+
+            setMarqueeText(mDataView, textToSet);
+            mDataView.setVisibility(VISIBLE);
+
+            // We have a phone number as "mDataView" so make it always LTR and VIEW_START
+            mDataView.setTextDirection(View.TEXT_DIRECTION_LTR);
+            mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+        }
+    }
+
+    private void setMarqueeText(TextView textView, char[] text, int size) {
+        if (getTextEllipsis() == TruncateAt.MARQUEE) {
+            setMarqueeText(textView, new String(text, 0, size));
+        } else {
+            textView.setText(text, 0, size);
+        }
+    }
+
+    private void setMarqueeText(TextView textView, CharSequence text) {
+        if (getTextEllipsis() == TruncateAt.MARQUEE) {
+            // To show MARQUEE correctly (with END effect during non-active state), we need
+            // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
+            final SpannableString spannable = new SpannableString(text);
+            spannable.setSpan(TruncateAt.MARQUEE, 0, spannable.length(),
+                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            textView.setText(spannable);
+        } else {
+            textView.setText(text);
+        }
+    }
+
+    /**
+     * Returns the {@link AppCompatCheckBox} view, creating it if necessary.
+     */
+    public AppCompatCheckBox getCheckBox() {
+        if (mCheckBox == null) {
+            mCheckBox = new AppCompatCheckBox(getContext());
+            // Make non-focusable, so the rest of the ContactListItemView can be clicked.
+            mCheckBox.setFocusable(false);
+            addView(mCheckBox);
+        }
+        return mCheckBox;
+    }
+
+    /**
+     * Returns the {@link AppCompatImageButton} delete button, creating it if necessary.
+     */
+    public AppCompatImageButton getDeleteImageButton(
+            final MultiSelectEntryContactListAdapter.DeleteContactListener listener,
+            final int position) {
+        if (mDeleteImageButton == null) {
+            mDeleteImageButton = new AppCompatImageButton(getContext());
+            mDeleteImageButton.setImageResource(R.drawable.ic_cancel_black_24dp);
+            mDeleteImageButton.setScaleType(ScaleType.CENTER);
+            mDeleteImageButton.setBackgroundColor(Color.TRANSPARENT);
+            mDeleteImageButton.setContentDescription(
+                    getResources().getString(R.string.description_delete_contact));
+            if (CompatUtils. isLollipopCompatible()) {
+                final TypedValue typedValue = new TypedValue();
+                getContext().getTheme().resolveAttribute(
+                        android.R.attr.selectableItemBackgroundBorderless, typedValue, true);
+                mDeleteImageButton.setBackgroundResource(typedValue.resourceId);
+            }
+            addView(mDeleteImageButton);
+        }
+        // Reset onClickListener because after reloading the view, position might be changed.
+        mDeleteImageButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                // Inform the adapter that delete icon was clicked.
+                if (listener != null) {
+                    listener.onContactDeleteClicked(position);
+                }
+            }
+        });
+        return mDeleteImageButton;
+    }
+
+    /**
+     * Returns the text view for the data text, creating it if necessary.
+     */
+    public TextView getDataView() {
+        if (mDataView == null) {
+            mDataView = new TextView(getContext());
+            mDataView.setSingleLine(true);
+            mDataView.setEllipsize(getTextEllipsis());
+            mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
+            mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+            mDataView.setActivated(isActivated());
+            mDataView.setId(R.id.cliv_data_view);
+            if (CompatUtils.isLollipopCompatible()) {
+                mDataView.setElegantTextHeight(false);
+            }
+            addView(mDataView);
+        }
+        return mDataView;
+    }
+
+    /**
+     * Adds or updates a text view for the search snippet.
+     */
+    public void setSnippet(String text) {
+        if (TextUtils.isEmpty(text)) {
+            if (mSnippetView != null) {
+                mSnippetView.setVisibility(View.GONE);
+            }
+        } else {
+            mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix);
+            mSnippetView.setVisibility(VISIBLE);
+            if (ContactDisplayUtils.isPossiblePhoneNumber(text)) {
+                // Give the text-to-speech engine a hint that it's a phone number
+                mSnippetView.setContentDescription(
+                        PhoneNumberUtilsCompat.createTtsSpannable(text));
+            } else {
+                mSnippetView.setContentDescription(null);
+            }
+        }
+    }
+
+    /**
+     * Returns the text view for the search snippet, creating it if necessary.
+     */
+    public TextView getSnippetView() {
+        if (mSnippetView == null) {
+            mSnippetView = new TextView(getContext());
+            mSnippetView.setSingleLine(true);
+            mSnippetView.setEllipsize(getTextEllipsis());
+            mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
+            mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+            mSnippetView.setActivated(isActivated());
+            addView(mSnippetView);
+        }
+        return mSnippetView;
+    }
+
+    /**
+     * Returns the text view for the status, creating it if necessary.
+     */
+    public TextView getStatusView() {
+        if (mStatusView == null) {
+            mStatusView = new TextView(getContext());
+            mStatusView.setSingleLine(true);
+            mStatusView.setEllipsize(getTextEllipsis());
+            mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
+            mStatusView.setTextColor(mSecondaryTextColor);
+            mStatusView.setActivated(isActivated());
+            mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+            addView(mStatusView);
+        }
+        return mStatusView;
+    }
+
+    /**
+     * Adds or updates a text view for the status.
+     */
+    public void setStatus(CharSequence text) {
+        if (TextUtils.isEmpty(text)) {
+            if (mStatusView != null) {
+                mStatusView.setVisibility(View.GONE);
+            }
+        } else {
+            getStatusView();
+            setMarqueeText(mStatusView, text);
+            mStatusView.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * Adds or updates the presence icon view.
+     */
+    public void setPresence(Drawable icon) {
+        if (icon != null) {
+            if (mPresenceIcon == null) {
+                mPresenceIcon = new ImageView(getContext());
+                addView(mPresenceIcon);
+            }
+            mPresenceIcon.setImageDrawable(icon);
+            mPresenceIcon.setScaleType(ScaleType.CENTER);
+            mPresenceIcon.setVisibility(View.VISIBLE);
+        } else {
+            if (mPresenceIcon != null) {
+                mPresenceIcon.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    /**
+     * Set to display work profile icon or not
+     *
+     * @param enabled set to display work profile icon or not
+     */
+    public void setWorkProfileIconEnabled(boolean enabled) {
+        if (mWorkProfileIcon != null) {
+            mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE);
+        } else if (enabled) {
+            mWorkProfileIcon = new ImageView(getContext());
+            addView(mWorkProfileIcon);
+            mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile);
+            mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE);
+            mWorkProfileIcon.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private TruncateAt getTextEllipsis() {
+        return TruncateAt.MARQUEE;
+    }
+
+    public void showDisplayName(Cursor cursor, int nameColumnIndex, int displayOrder) {
+        CharSequence name = cursor.getString(nameColumnIndex);
+        setDisplayName(name);
+
+        // Since the quick contact content description is derived from the display name and there is
+        // no guarantee that when the quick contact is initialized the display name is already set,
+        // do it here too.
+        if (mQuickContact != null) {
+            mQuickContact.setContentDescription(getContext().getString(
+                    R.string.description_quick_contact_for, mNameTextView.getText()));
+        }
+    }
+
+    public void setDisplayName(CharSequence name, boolean highlight) {
+        if (!TextUtils.isEmpty(name) && highlight) {
+            clearHighlightSequences();
+            addNameHighlightSequence(0, name.length());
+        }
+        setDisplayName(name);
+    }
+
+    public void setDisplayName(CharSequence name) {
+        if (!TextUtils.isEmpty(name)) {
+            // Chooses the available highlighting method for highlighting.
+            if (mHighlightedPrefix != null) {
+                name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix);
+            } else if (mNameHighlightSequence.size() != 0) {
+                final SpannableString spannableName = new SpannableString(name);
+                for (HighlightSequence highlightSequence : mNameHighlightSequence) {
+                    mTextHighlighter.applyMaskingHighlight(spannableName, highlightSequence.start,
+                            highlightSequence.end);
+                }
+                name = spannableName;
+            }
+        } else {
+            name = mUnknownNameText;
+        }
+        setMarqueeText(getNameTextView(), name);
+
+        if (ContactDisplayUtils.isPossiblePhoneNumber(name)) {
+            // Give the text-to-speech engine a hint that it's a phone number
+            mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
+            mNameTextView.setContentDescription(
+                    PhoneNumberUtilsCompat.createTtsSpannable(name.toString()));
+        } else {
+            // Remove span tags of highlighting for talkback to avoid reading highlighting and rest
+            // of the name into two separate parts.
+            mNameTextView.setContentDescription(name.toString());
+        }
+    }
+
+    public void hideCheckBox() {
+        if (mCheckBox != null) {
+            removeView(mCheckBox);
+            mCheckBox = null;
+        }
+    }
+
+    public void hideDeleteImageButton() {
+        if (mDeleteImageButton != null) {
+            removeView(mDeleteImageButton);
+            mDeleteImageButton = null;
+        }
+    }
+
+    public void hideDisplayName() {
+        if (mNameTextView != null) {
+            removeView(mNameTextView);
+            mNameTextView = null;
+        }
+    }
+
+    public void showPhoneticName(Cursor cursor, int phoneticNameColumnIndex) {
+        cursor.copyStringToBuffer(phoneticNameColumnIndex, mPhoneticNameBuffer);
+        int phoneticNameSize = mPhoneticNameBuffer.sizeCopied;
+        if (phoneticNameSize != 0) {
+            setPhoneticName(mPhoneticNameBuffer.data, phoneticNameSize);
+        } else {
+            setPhoneticName(null, 0);
+        }
+    }
+
+    public void hidePhoneticName() {
+        if (mPhoneticNameTextView != null) {
+            removeView(mPhoneticNameTextView);
+            mPhoneticNameTextView = null;
+        }
+    }
+
+    /**
+     * Sets the proper icon (star or presence or nothing) and/or status message.
+     */
+    public void showPresenceAndStatusMessage(Cursor cursor, int presenceColumnIndex,
+            int contactStatusColumnIndex) {
+        Drawable icon = null;
+        int presence = 0;
+        if (!cursor.isNull(presenceColumnIndex)) {
+            presence = cursor.getInt(presenceColumnIndex);
+            icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
+        }
+        setPresence(icon);
+
+        String statusMessage = null;
+        if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
+            statusMessage = cursor.getString(contactStatusColumnIndex);
+        }
+        // If there is no status message from the contact, but there was a presence value, then use
+        // the default status message string
+        if (statusMessage == null && presence != 0) {
+            statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
+        }
+        setStatus(statusMessage);
+    }
+
+    /**
+     * Shows search snippet for email and phone number matches.
+     */
+    public void showSnippet(Cursor cursor, String query, int snippetColumn) {
+        // TODO: this does not properly handle phone numbers with control characters
+        // For example if the phone number is 444-5555, the search query 4445 will match the
+        // number since we normalize it before querying CP2 but the snippet will fail since
+        // the portion to be highlighted is 444-5 not 4445.
+        final String snippet = cursor.getString(snippetColumn);
+        if (snippet == null) {
+            setSnippet(null);
+            return;
+        }
+        final String displayName = cursor.getColumnIndex(Contacts.DISPLAY_NAME) >= 0
+                ? cursor.getString(cursor.getColumnIndex(Contacts.DISPLAY_NAME)) : null;
+        if (snippet.equals(displayName)) {
+            // If the snippet exactly matches the display name (i.e. the phone number or email
+            // address is being used as the display name) then no snippet is necessary
+            setSnippet(null);
+            return;
+        }
+        // Show the snippet with the part of the query that matched it
+        setSnippet(updateSnippet(snippet, query, displayName));
+    }
+
+    /**
+     * Shows search snippet.
+     */
+    public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
+        if (cursor.getColumnCount() <= summarySnippetColumnIndex
+            || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) {
+            setSnippet(null);
+            return;
+        }
+
+        String snippet = cursor.getString(summarySnippetColumnIndex);
+
+        // Do client side snippeting if provider didn't do it
+        final Bundle extras = cursor.getExtras();
+        if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
+
+            final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY);
+
+            String displayName = null;
+            int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
+            if (displayNameIndex >= 0) {
+                displayName = cursor.getString(displayNameIndex);
+            }
+
+            snippet = updateSnippet(snippet, query, displayName);
+
+        } else {
+            if (snippet != null) {
+                int from = 0;
+                int to = snippet.length();
+                int start = snippet.indexOf(DefaultContactListAdapter.SNIPPET_START_MATCH);
+                if (start == -1) {
+                    snippet = null;
+                } else {
+                    int firstNl = snippet.lastIndexOf('\n', start);
+                    if (firstNl != -1) {
+                        from = firstNl + 1;
+                    }
+                    int end = snippet.lastIndexOf(DefaultContactListAdapter.SNIPPET_END_MATCH);
+                    if (end != -1) {
+                        int lastNl = snippet.indexOf('\n', end);
+                        if (lastNl != -1) {
+                            to = lastNl;
+                        }
+                    }
+
+                    StringBuilder sb = new StringBuilder();
+                    for (int i = from; i < to; i++) {
+                        char c = snippet.charAt(i);
+                        if (c != DefaultContactListAdapter.SNIPPET_START_MATCH &&
+                                c != DefaultContactListAdapter.SNIPPET_END_MATCH) {
+                            sb.append(c);
+                        }
+                    }
+                    snippet = sb.toString();
+                }
+            }
+        }
+
+        setSnippet(snippet);
+    }
+
+    /**
+     * Used for deferred snippets from the database. The contents come back as large strings which
+     * need to be extracted for display.
+     *
+     * @param snippet The snippet from the database.
+     * @param query The search query substring.
+     * @param displayName The contact display name.
+     * @return The proper snippet to display.
+     */
+    private String updateSnippet(String snippet, String query, String displayName) {
+
+        if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) {
+            return null;
+        }
+        query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase());
+
+        // If the display name already contains the query term, return empty - snippets should
+        // not be needed in that case.
+        if (!TextUtils.isEmpty(displayName)) {
+            final String lowerDisplayName = displayName.toLowerCase();
+            final List<String> nameTokens = split(lowerDisplayName);
+            for (String nameToken : nameTokens) {
+                if (nameToken.startsWith(query)) {
+                    return null;
+                }
+            }
+        }
+
+        // The snippet may contain multiple data lines.
+        // Show the first line that matches the query.
+        final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query);
+
+        if (matched != null && matched.line != null) {
+            // Tokenize for long strings since the match may be at the end of it.
+            // Skip this part for short strings since the whole string will be displayed.
+            // Most contact strings are short so the snippetize method will be called infrequently.
+            final int lengthThreshold = getResources().getInteger(
+                    R.integer.snippet_length_before_tokenize);
+            if (matched.line.length() > lengthThreshold) {
+                return snippetize(matched.line, matched.startIndex, lengthThreshold);
+            } else {
+                return matched.line;
+            }
+        }
+
+        // No match found.
+        return null;
+    }
+
+    private String snippetize(String line, int matchIndex, int maxLength) {
+        // Show up to maxLength characters. But we only show full tokens so show the last full token
+        // up to maxLength characters. So as many starting tokens as possible before trying ending
+        // tokens.
+        int remainingLength = maxLength;
+        int tempRemainingLength = remainingLength;
+
+        // Start the end token after the matched query.
+        int index = matchIndex;
+        int endTokenIndex = index;
+
+        // Find the match token first.
+        while (index < line.length()) {
+            if (!Character.isLetterOrDigit(line.charAt(index))) {
+                endTokenIndex = index;
+                remainingLength = tempRemainingLength;
+                break;
+            }
+            tempRemainingLength--;
+            index++;
+        }
+
+        // Find as much content before the match.
+        index = matchIndex - 1;
+        tempRemainingLength = remainingLength;
+        int startTokenIndex = matchIndex;
+        while (index > -1 && tempRemainingLength > 0) {
+            if (!Character.isLetterOrDigit(line.charAt(index))) {
+                startTokenIndex = index;
+                remainingLength = tempRemainingLength;
+            }
+            tempRemainingLength--;
+            index--;
+        }
+
+        index = endTokenIndex;
+        tempRemainingLength = remainingLength;
+        // Find remaining content at after match.
+        while (index < line.length() && tempRemainingLength > 0) {
+            if (!Character.isLetterOrDigit(line.charAt(index))) {
+                endTokenIndex = index;
+            }
+            tempRemainingLength--;
+            index++;
+        }
+        // Append ellipse if there is content before or after.
+        final StringBuilder sb = new StringBuilder();
+        if (startTokenIndex > 0) {
+            sb.append("...");
+        }
+        sb.append(line.substring(startTokenIndex, endTokenIndex));
+        if (endTokenIndex < line.length()) {
+            sb.append("...");
+        }
+        return sb.toString();
+    }
+
+    private static final Pattern SPLIT_PATTERN = Pattern.compile(
+            "([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
+
+    /**
+     * Helper method for splitting a string into tokens.  The lists passed in are populated with
+     * the
+     * tokens and offsets into the content of each token.  The tokenization function parses e-mail
+     * addresses as a single token; otherwise it splits on any non-alphanumeric character.
+     *
+     * @param content Content to split.
+     * @return List of token strings.
+     */
+    private static List<String> split(String content) {
+        final Matcher matcher = SPLIT_PATTERN.matcher(content);
+        final ArrayList<String> tokens = Lists.newArrayList();
+        while (matcher.find()) {
+            tokens.add(matcher.group());
+        }
+        return tokens;
+    }
+
+    /**
+     * Shows data element.
+     */
+    public void showData(Cursor cursor, int dataColumnIndex) {
+        cursor.copyStringToBuffer(dataColumnIndex, mDataBuffer);
+        setData(mDataBuffer.data, mDataBuffer.sizeCopied);
+    }
+
+    public void setActivatedStateSupported(boolean flag) {
+        this.mActivatedStateSupported = flag;
+    }
+
+    public void setAdjustSelectionBoundsEnabled(boolean enabled) {
+        mAdjustSelectionBoundsEnabled = enabled;
+    }
+
+    @Override
+    public void requestLayout() {
+        // We will assume that once measured this will not need to resize
+        // itself, so there is no need to pass the layout request to the parent
+        // view (ListView).
+        forceLayout();
+    }
+
+    public void setPhotoPosition(PhotoPosition photoPosition) {
+        mPhotoPosition = photoPosition;
+    }
+
+    public PhotoPosition getPhotoPosition() {
+        return mPhotoPosition;
+    }
+
+    /**
+     * Set drawable resources directly for the drawable resource of the photo view.
+     *
+     * @param drawableId Id of drawable resource.
+     */
+    public void setDrawableResource(int drawableId) {
+        ImageView photo = getPhotoView();
+        photo.setScaleType(ImageView.ScaleType.CENTER);
+        final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId);
+        final int iconColor =
+                ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color);
+        if (CompatUtils.isLollipopCompatible()) {
+            photo.setImageDrawable(drawable);
+            photo.setImageTintList(ColorStateList.valueOf(iconColor));
+        } else {
+            final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate();
+            DrawableCompat.setTint(drawableWrapper, iconColor);
+            photo.setImageDrawable(drawableWrapper);
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        final float x = event.getX();
+        final float y = event.getY();
+        // If the touch event's coordinates are not within the view's header, then delegate
+        // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume
+        // and ignore the touch event.
+        if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) {
+            return super.onTouchEvent(event);
+        } else {
+            return true;
+        }
+    }
+
+    private final boolean pointIsInView(float localX, float localY) {
+        return localX >= mLeftOffset && localX < mRightOffset
+                && localY >= 0 && localY < (getBottom() - getTop());
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactListPinnedHeaderView.java b/src/com/android/contacts/common/list/ContactListPinnedHeaderView.java
new file mode 100644
index 0000000..6e8e738
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactListPinnedHeaderView.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.ViewUtil;
+
+/**
+ * A custom view for the pinned section header shown at the top of the contact list.
+ */
+public class ContactListPinnedHeaderView extends TextView {
+
+    public ContactListPinnedHeaderView(Context context, AttributeSet attrs, View parent) {
+        super(context, attrs);
+
+        if (R.styleable.ContactListItemView == null) {
+            return;
+        }
+        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
+        int backgroundColor = a.getColor(
+                R.styleable.ContactListItemView_list_item_background_color, Color.WHITE);
+        int textOffsetTop = a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_text_offset_top, 0);
+        int paddingStartOffset = a.getDimensionPixelSize(
+                R.styleable.ContactListItemView_list_item_padding_left, 0);
+        int textWidth = getResources().getDimensionPixelSize(
+                R.dimen.contact_list_section_header_width);
+        int widthIncludingPadding = paddingStartOffset + textWidth;
+        a.recycle();
+
+        setBackgroundColor(backgroundColor);
+        setTextAppearance(getContext(), R.style.SectionHeaderStyle);
+        setLayoutParams(new LayoutParams(widthIncludingPadding, LayoutParams.WRAP_CONTENT));
+        setLayoutDirection(parent.getLayoutDirection());
+        setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
+
+        // Apply text top offset. Multiply by two, because we are implementing this by padding for a
+        // vertically centered view, rather than adjusting the position directly via a layout.
+        setPaddingRelative(
+                getPaddingStart() + paddingStartOffset,
+                getPaddingTop() + (textOffsetTop * 2),
+                getPaddingEnd(),
+                getPaddingBottom());
+    }
+
+    /**
+     * Sets section header or makes it invisible if the title is null.
+     */
+    public void setSectionHeaderTitle(String title) {
+        if (!TextUtils.isEmpty(title)) {
+            setText(title);
+            setVisibility(View.VISIBLE);
+        } else {
+            setVisibility(View.GONE);
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactTileAdapter.java b/src/com/android/contacts/common/list/ContactTileAdapter.java
new file mode 100644
index 0000000..6ce4efb
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactTileAdapter.java
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPresenceIconUtil;
+import com.android.contacts.common.ContactStatusUtil;
+import com.android.contacts.common.ContactTileLoaderFactory;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.ViewUtil;
+
+import java.util.ArrayList;
+
+/**
+ * Arranges contacts favorites according to provided {@link DisplayType}.
+ * Also allows for a configurable number of columns and {@link DisplayType}
+ */
+public class ContactTileAdapter extends BaseAdapter {
+    private static final String TAG = ContactTileAdapter.class.getSimpleName();
+
+    private DisplayType mDisplayType;
+    private ContactTileView.Listener mListener;
+    private Context mContext;
+    private Resources mResources;
+    protected Cursor mContactCursor = null;
+    private ContactPhotoManager mPhotoManager;
+    protected int mNumFrequents;
+
+    /**
+     * Index of the first NON starred contact in the {@link Cursor}
+     * Only valid when {@link DisplayType#STREQUENT} is true
+     */
+    private int mDividerPosition;
+    protected int mColumnCount;
+    private int mStarredIndex;
+
+    protected int mIdIndex;
+    protected int mLookupIndex;
+    protected int mPhotoUriIndex;
+    protected int mNameIndex;
+    protected int mPresenceIndex;
+    protected int mStatusIndex;
+
+    private boolean mIsQuickContactEnabled = false;
+    private final int mPaddingInPixels;
+    private final int mWhitespaceStartEnd;
+
+    /**
+     * Configures the adapter to filter and display contacts using different view types.
+     * TODO: Create Uris to support getting Starred_only and Frequent_only cursors.
+     */
+    public enum DisplayType {
+        /**
+         * Displays a mixed view type of starred and frequent contacts
+         */
+        STREQUENT,
+
+        /**
+         * Display only starred contacts
+         */
+        STARRED_ONLY,
+
+        /**
+         * Display only most frequently contacted
+         */
+        FREQUENT_ONLY,
+
+        /**
+         * Display all contacts from a group in the cursor
+         */
+        GROUP_MEMBERS
+    }
+
+    public ContactTileAdapter(Context context, ContactTileView.Listener listener, int numCols,
+            DisplayType displayType) {
+        mListener = listener;
+        mContext = context;
+        mResources = context.getResources();
+        mColumnCount = (displayType == DisplayType.FREQUENT_ONLY ? 1 : numCols);
+        mDisplayType = displayType;
+        mNumFrequents = 0;
+
+        // Converting padding in dips to padding in pixels
+        mPaddingInPixels = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.contact_tile_divider_padding);
+        mWhitespaceStartEnd = mContext.getResources()
+                .getDimensionPixelSize(R.dimen.contact_tile_start_end_whitespace);
+
+        bindColumnIndices();
+    }
+
+    public void setPhotoLoader(ContactPhotoManager photoLoader) {
+        mPhotoManager = photoLoader;
+    }
+
+    public void setColumnCount(int columnCount) {
+        mColumnCount = columnCount;
+    }
+
+    public void setDisplayType(DisplayType displayType) {
+        mDisplayType = displayType;
+    }
+
+    public void enableQuickContact(boolean enableQuickContact) {
+        mIsQuickContactEnabled = enableQuickContact;
+    }
+
+    /**
+     * Sets the column indices for expected {@link Cursor}
+     * based on {@link DisplayType}.
+     */
+    protected void bindColumnIndices() {
+        mIdIndex = ContactTileLoaderFactory.CONTACT_ID;
+        mLookupIndex = ContactTileLoaderFactory.LOOKUP_KEY;
+        mPhotoUriIndex = ContactTileLoaderFactory.PHOTO_URI;
+        mNameIndex = ContactTileLoaderFactory.DISPLAY_NAME;
+        mStarredIndex = ContactTileLoaderFactory.STARRED;
+        mPresenceIndex = ContactTileLoaderFactory.CONTACT_PRESENCE;
+        mStatusIndex = ContactTileLoaderFactory.CONTACT_STATUS;
+    }
+
+    private static boolean cursorIsValid(Cursor cursor) {
+        return cursor != null && !cursor.isClosed();
+    }
+
+    /**
+     * Gets the number of frequents from the passed in cursor.
+     *
+     * This methods is needed so the GroupMemberTileAdapter can override this.
+     *
+     * @param cursor The cursor to get number of frequents from.
+     */
+    protected void saveNumFrequentsFromCursor(Cursor cursor) {
+
+        // count the number of frequents
+        switch (mDisplayType) {
+            case STARRED_ONLY:
+                mNumFrequents = 0;
+                break;
+            case STREQUENT:
+                mNumFrequents = cursorIsValid(cursor) ?
+                    cursor.getCount() - mDividerPosition : 0;
+                break;
+            case FREQUENT_ONLY:
+                mNumFrequents = cursorIsValid(cursor) ? cursor.getCount() : 0;
+                break;
+            default:
+                throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType);
+        }
+    }
+
+    /**
+     * Creates {@link ContactTileView}s for each item in {@link Cursor}.
+     *
+     * Else use {@link ContactTileLoaderFactory}
+     */
+    public void setContactCursor(Cursor cursor) {
+        mContactCursor = cursor;
+        mDividerPosition = getDividerPosition(cursor);
+
+        saveNumFrequentsFromCursor(cursor);
+
+        // cause a refresh of any views that rely on this data
+        notifyDataSetChanged();
+    }
+
+    /**
+     * Iterates over the {@link Cursor}
+     * Returns position of the first NON Starred Contact
+     * Returns -1 if {@link DisplayType#STARRED_ONLY}
+     * Returns 0 if {@link DisplayType#FREQUENT_ONLY}
+     */
+    protected int getDividerPosition(Cursor cursor) {
+        switch (mDisplayType) {
+            case STREQUENT:
+                if (!cursorIsValid(cursor)) {
+                    return 0;
+                }
+                cursor.moveToPosition(-1);
+                while (cursor.moveToNext()) {
+                    if (cursor.getInt(mStarredIndex) == 0) {
+                        return cursor.getPosition();
+                    }
+                }
+
+                // There are not NON Starred contacts in cursor
+                // Set divider positon to end
+                return cursor.getCount();
+            case STARRED_ONLY:
+                // There is no divider
+                return -1;
+            case FREQUENT_ONLY:
+                // Divider is first
+                return 0;
+            default:
+                throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType);
+        }
+    }
+
+    protected ContactEntry createContactEntryFromCursor(Cursor cursor, int position) {
+        // If the loader was canceled we will be given a null cursor.
+        // In that case, show an empty list of contacts.
+        if (!cursorIsValid(cursor) || cursor.getCount() <= position) {
+            return null;
+        }
+
+        cursor.moveToPosition(position);
+        long id = cursor.getLong(mIdIndex);
+        String photoUri = cursor.getString(mPhotoUriIndex);
+        String lookupKey = cursor.getString(mLookupIndex);
+
+        ContactEntry contact = new ContactEntry();
+        String name = cursor.getString(mNameIndex);
+        contact.namePrimary = (name != null) ? name : mResources.getString(R.string.missing_name);
+        contact.status = cursor.getString(mStatusIndex);
+        contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null);
+        contact.lookupKey = lookupKey;
+        contact.lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id);
+        contact.isFavorite = cursor.getInt(mStarredIndex) > 0;
+
+        // Set presence icon and status message
+        Drawable icon = null;
+        int presence = 0;
+        if (!cursor.isNull(mPresenceIndex)) {
+            presence = cursor.getInt(mPresenceIndex);
+            icon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence);
+        }
+        contact.presenceIcon = icon;
+
+        String statusMessage = null;
+        if (mStatusIndex != 0 && !cursor.isNull(mStatusIndex)) {
+            statusMessage = cursor.getString(mStatusIndex);
+        }
+        // If there is no status message from the contact, but there was a presence value,
+        // then use the default status message string
+        if (statusMessage == null && presence != 0) {
+            statusMessage = ContactStatusUtil.getStatusString(mContext, presence);
+        }
+        contact.status = statusMessage;
+
+        return contact;
+    }
+
+    /**
+     * Returns the number of frequents that will be displayed in the list.
+     */
+    public int getNumFrequents() {
+        return mNumFrequents;
+    }
+
+    @Override
+    public int getCount() {
+        if (!cursorIsValid(mContactCursor)) {
+            return 0;
+        }
+
+        switch (mDisplayType) {
+            case STARRED_ONLY:
+                return getRowCount(mContactCursor.getCount());
+            case STREQUENT:
+                // Takes numbers of rows the Starred Contacts Occupy
+                int starredRowCount = getRowCount(mDividerPosition);
+
+                // Compute the frequent row count which is 1 plus the number of frequents
+                // (to account for the divider) or 0 if there are no frequents.
+                int frequentRowCount = mNumFrequents == 0 ? 0 : mNumFrequents + 1;
+
+                // Return the number of starred plus frequent rows
+                return starredRowCount + frequentRowCount;
+            case FREQUENT_ONLY:
+                // Number of frequent contacts
+                return mContactCursor.getCount();
+            default:
+                throw new IllegalArgumentException("Unrecognized DisplayType " + mDisplayType);
+        }
+    }
+
+    /**
+     * Returns the number of rows required to show the provided number of entries
+     * with the current number of columns.
+     */
+    protected int getRowCount(int entryCount) {
+        return entryCount == 0 ? 0 : ((entryCount - 1) / mColumnCount) + 1;
+    }
+
+    public int getColumnCount() {
+        return mColumnCount;
+    }
+
+    /**
+     * Returns an ArrayList of the {@link ContactEntry}s that are to appear
+     * on the row for the given position.
+     */
+    @Override
+    public ArrayList<ContactEntry> getItem(int position) {
+        ArrayList<ContactEntry> resultList = new ArrayList<ContactEntry>(mColumnCount);
+        int contactIndex = position * mColumnCount;
+
+        switch (mDisplayType) {
+            case FREQUENT_ONLY:
+                resultList.add(createContactEntryFromCursor(mContactCursor, position));
+                break;
+            case STARRED_ONLY:
+                for (int columnCounter = 0; columnCounter < mColumnCount; columnCounter++) {
+                    resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex));
+                    contactIndex++;
+                }
+                break;
+            case STREQUENT:
+                if (position < getRowCount(mDividerPosition)) {
+                    for (int columnCounter = 0; columnCounter < mColumnCount &&
+                            contactIndex != mDividerPosition; columnCounter++) {
+                        resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex));
+                        contactIndex++;
+                    }
+                } else {
+                    /*
+                     * Current position minus how many rows are before the divider and
+                     * Minus 1 for the divider itself provides the relative index of the frequent
+                     * contact being displayed. Then add the dividerPostion to give the offset
+                     * into the contacts cursor to get the absoulte index.
+                     */
+                    contactIndex = position - getRowCount(mDividerPosition) - 1 + mDividerPosition;
+                    resultList.add(createContactEntryFromCursor(mContactCursor, contactIndex));
+                }
+                break;
+            default:
+                throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType);
+        }
+        return resultList;
+    }
+
+    @Override
+    public long getItemId(int position) {
+        // As we show several selectable items for each ListView row,
+        // we can not determine a stable id. But as we don't rely on ListView's selection,
+        // this should not be a problem.
+        return position;
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        return (mDisplayType != DisplayType.STREQUENT);
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        return position != getRowCount(mDividerPosition);
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        int itemViewType = getItemViewType(position);
+
+        if (itemViewType == ViewTypes.DIVIDER) {
+            // Checking For Divider First so not to cast convertView
+            final TextView textView = (TextView) (convertView == null ? getDivider() : convertView);
+            setDividerPadding(textView, position == 0);
+            return textView;
+        }
+
+        ContactTileRow contactTileRowView = (ContactTileRow) convertView;
+        ArrayList<ContactEntry> contactList = getItem(position);
+
+        if (contactTileRowView == null) {
+            // Creating new row if needed
+            contactTileRowView = new ContactTileRow(mContext, itemViewType);
+        }
+
+        contactTileRowView.configureRow(contactList, position == getCount() - 1);
+        return contactTileRowView;
+    }
+
+    /**
+     * Divider uses a list_seperator.xml along with text to denote
+     * the most frequently contacted contacts.
+     */
+    private TextView getDivider() {
+        return MoreContactUtils.createHeaderView(mContext, R.string.favoritesFrequentContacted);
+    }
+
+    private void setDividerPadding(TextView headerTextView, boolean isFirstRow) {
+        MoreContactUtils.setHeaderViewBottomPadding(mContext, headerTextView, isFirstRow);
+    }
+
+    private int getLayoutResourceId(int viewType) {
+        switch (viewType) {
+            case ViewTypes.STARRED:
+                return mIsQuickContactEnabled ?
+                        R.layout.contact_tile_starred_quick_contact : R.layout.contact_tile_starred;
+            case ViewTypes.FREQUENT:
+                return R.layout.contact_tile_frequent;
+            default:
+                throw new IllegalArgumentException("Unrecognized viewType " + viewType);
+        }
+    }
+    @Override
+    public int getViewTypeCount() {
+        return ViewTypes.COUNT;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        /*
+         * Returns view type based on {@link DisplayType}.
+         * {@link DisplayType#STARRED_ONLY} and {@link DisplayType#GROUP_MEMBERS}
+         * are {@link ViewTypes#STARRED}.
+         * {@link DisplayType#FREQUENT_ONLY} is {@link ViewTypes#FREQUENT}.
+         * {@link DisplayType#STREQUENT} mixes both {@link ViewTypes}
+         * and also adds in {@link ViewTypes#DIVIDER}.
+         */
+        switch (mDisplayType) {
+            case STREQUENT:
+                if (position < getRowCount(mDividerPosition)) {
+                    return ViewTypes.STARRED;
+                } else if (position == getRowCount(mDividerPosition)) {
+                    return ViewTypes.DIVIDER;
+                } else {
+                    return ViewTypes.FREQUENT;
+                }
+            case STARRED_ONLY:
+                return ViewTypes.STARRED;
+            case FREQUENT_ONLY:
+                return ViewTypes.FREQUENT;
+            default:
+                throw new IllegalStateException("Unrecognized DisplayType " + mDisplayType);
+        }
+    }
+
+    /**
+     * Returns the "frequent header" position. Only available when STREQUENT or
+     * STREQUENT_PHONE_ONLY is used for its display type.
+     */
+    public int getFrequentHeaderPosition() {
+        return getRowCount(mDividerPosition);
+    }
+
+    /**
+     * Acts as a row item composed of {@link ContactTileView}
+     *
+     * TODO: FREQUENT doesn't really need it.  Just let {@link #getView} return
+     */
+    private class ContactTileRow extends FrameLayout {
+        private int mItemViewType;
+        private int mLayoutResId;
+
+        public ContactTileRow(Context context, int itemViewType) {
+            super(context);
+            mItemViewType = itemViewType;
+            mLayoutResId = getLayoutResourceId(mItemViewType);
+
+            // Remove row (but not children) from accessibility node tree.
+            setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+        }
+
+        /**
+         * Configures the row to add {@link ContactEntry}s information to the views
+         */
+        public void configureRow(ArrayList<ContactEntry> list, boolean isLastRow) {
+            int columnCount = mItemViewType == ViewTypes.FREQUENT ? 1 : mColumnCount;
+
+            // Adding tiles to row and filling in contact information
+            for (int columnCounter = 0; columnCounter < columnCount; columnCounter++) {
+                ContactEntry entry =
+                        columnCounter < list.size() ? list.get(columnCounter) : null;
+                addTileFromEntry(entry, columnCounter, isLastRow);
+            }
+        }
+
+        private void addTileFromEntry(ContactEntry entry, int childIndex, boolean isLastRow) {
+            final ContactTileView contactTile;
+
+            if (getChildCount() <= childIndex) {
+                contactTile = (ContactTileView) inflate(mContext, mLayoutResId, null);
+                // Note: the layoutparam set here is only actually used for FREQUENT.
+                // We override onMeasure() for STARRED and we don't care the layout param there.
+                Resources resources = mContext.getResources();
+                FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT);
+                params.setMargins(
+                        mWhitespaceStartEnd,
+                        0,
+                        mWhitespaceStartEnd,
+                        0);
+                contactTile.setLayoutParams(params);
+                contactTile.setPhotoManager(mPhotoManager);
+                contactTile.setListener(mListener);
+                addView(contactTile);
+            } else {
+                contactTile = (ContactTileView) getChildAt(childIndex);
+            }
+            contactTile.loadFromContact(entry);
+
+            switch (mItemViewType) {
+                case ViewTypes.STARRED:
+                    // Set padding between tiles. Divide mPaddingInPixels between left and right
+                    // tiles as evenly as possible.
+                    contactTile.setPaddingRelative(
+                            (mPaddingInPixels + 1) / 2, 0,
+                            mPaddingInPixels
+                            / 2, 0);
+                    break;
+                case ViewTypes.FREQUENT:
+                    contactTile.setHorizontalDividerVisibility(
+                            isLastRow ? View.GONE : View.VISIBLE);
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        @Override
+        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+            switch (mItemViewType) {
+                case ViewTypes.STARRED:
+                    onLayoutForTiles();
+                    return;
+                default:
+                    super.onLayout(changed, left, top, right, bottom);
+                    return;
+            }
+        }
+
+        private void onLayoutForTiles() {
+            final int count = getChildCount();
+
+            // Amount of margin needed on the left is based on difference between offset and padding
+            int childLeft = mWhitespaceStartEnd - (mPaddingInPixels + 1) / 2;
+
+            // Just line up children horizontally.
+            for (int i = 0; i < count; i++) {
+                final int rtlAdjustedIndex = ViewUtil.isViewLayoutRtl(this) ? count - i - 1 : i;
+                final View child = getChildAt(rtlAdjustedIndex);
+
+                // Note MeasuredWidth includes the padding.
+                final int childWidth = child.getMeasuredWidth();
+                child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
+                childLeft += childWidth;
+            }
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            switch (mItemViewType) {
+                case ViewTypes.STARRED:
+                    onMeasureForTiles(widthMeasureSpec);
+                    return;
+                default:
+                    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+                    return;
+            }
+        }
+
+        private void onMeasureForTiles(int widthMeasureSpec) {
+            final int width = MeasureSpec.getSize(widthMeasureSpec);
+
+            final int childCount = getChildCount();
+            if (childCount == 0) {
+                // Just in case...
+                setMeasuredDimension(width, 0);
+                return;
+            }
+
+            // 1. Calculate image size.
+            //      = ([total width] - [total whitespace]) / [child count]
+            //
+            // 2. Set it to width/height of each children.
+            //    If we have a remainder, some tiles will have 1 pixel larger width than its height.
+            //
+            // 3. Set the dimensions of itself.
+            //    Let width = given width.
+            //    Let height = wrap content.
+
+            final int totalWhitespaceInPixels = (mColumnCount - 1) * mPaddingInPixels
+                    + mWhitespaceStartEnd * 2;
+
+            // Preferred width / height for images (excluding the padding).
+            // The actual width may be 1 pixel larger than this if we have a remainder.
+            final int imageSize = (width - totalWhitespaceInPixels) / mColumnCount;
+            final int remainder = width - (imageSize * mColumnCount) - totalWhitespaceInPixels;
+
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final int childWidth = imageSize + child.getPaddingRight() + child.getPaddingLeft()
+                        // Compensate for the remainder
+                        + (i < remainder ? 1 : 0);
+
+                child.measure(
+                        MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
+                        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+                        );
+            }
+            setMeasuredDimension(width, getChildAt(0).getMeasuredHeight());
+        }
+    }
+
+    protected static class ViewTypes {
+        public static final int COUNT = 4;
+        public static final int STARRED = 0;
+        public static final int DIVIDER = 1;
+        public static final int FREQUENT = 2;
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactTileFrequentView.java b/src/com/android/contacts/common/list/ContactTileFrequentView.java
new file mode 100644
index 0000000..7dcd0a1
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactTileFrequentView.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.contacts.common.util.ViewUtil;
+
+/**
+ * A {@link com.android.contacts.common.list.ContactTileView} that is used for most frequently contacted in the People app
+ */
+public class ContactTileFrequentView extends ContactTileView {
+    public ContactTileFrequentView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected boolean isDarkTheme() {
+        return false;
+    }
+
+    @Override
+    protected int getApproximateImageSize() {
+        return ViewUtil.getConstantPreLayoutWidth(getPhotoView());
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactTilePhoneFrequentView.java b/src/com/android/contacts/common/list/ContactTilePhoneFrequentView.java
new file mode 100644
index 0000000..aec93ab
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactTilePhoneFrequentView.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.util.ViewUtil;
+
+/**
+ * A dark version of the {@link com.android.contacts.common.list.ContactTileView} that is used in Dialtacts
+ * for frequently called contacts. Slightly different behavior from superclass...
+ * when you tap it, you want to call the frequently-called number for the
+ * contact, even if that is not the default number for that contact.
+ */
+public class ContactTilePhoneFrequentView extends ContactTileView {
+    private String mPhoneNumberString;
+
+    public ContactTilePhoneFrequentView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected boolean isDarkTheme() {
+        return true;
+    }
+
+    @Override
+    protected int getApproximateImageSize() {
+        return ViewUtil.getConstantPreLayoutWidth(getQuickContact());
+    }
+
+    @Override
+    public void loadFromContact(ContactEntry entry) {
+        super.loadFromContact(entry);
+        mPhoneNumberString = null; // ... in case we're reusing the view
+        if (entry != null) {
+            // Grab the phone-number to call directly... see {@link onClick()}
+            mPhoneNumberString = entry.phoneNumber;
+        }
+    }
+
+    @Override
+    protected OnClickListener createClickListener() {
+        return new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mListener == null) return;
+                if (TextUtils.isEmpty(mPhoneNumberString)) {
+                    // Copy "superclass" implementation
+                    mListener.onContactSelected(getLookupUri(), MoreContactUtils
+                            .getTargetRectFromView(ContactTilePhoneFrequentView.this));
+                } else {
+                    // When you tap a frequently-called contact, you want to
+                    // call them at the number that you usually talk to them
+                    // at (i.e. the one displayed in the UI), regardless of
+                    // whether that's their default number.
+                    mListener.onCallNumberDirectly(mPhoneNumberString);
+                }
+            }
+        };
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactTileStarredView.java b/src/com/android/contacts/common/list/ContactTileStarredView.java
new file mode 100644
index 0000000..59ef81e
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactTileStarredView.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+/**
+ * A {@link ContactTileStarredView} displays the contact's picture overlayed with their name
+ * in a square. The actual dimensions are set by
+ * {@link com.android.contacts.common.list.ContactTileAdapter.ContactTileRow}.
+ */
+public class ContactTileStarredView extends ContactTileView {
+
+    /**
+     * The photo manager should display the default image/letter at 80% of its normal size.
+     */
+    private static final float DEFAULT_IMAGE_LETTER_SCALE = 0.8f;
+
+    public ContactTileStarredView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected boolean isDarkTheme() {
+        return false;
+    }
+
+    @Override
+    protected int getApproximateImageSize() {
+        // The picture is the full size of the tile (minus some padding, but we can be generous)
+        return mListener.getApproximateTileWidth();
+    }
+
+    @Override
+    protected DefaultImageRequest getDefaultImageRequest(String displayName, String lookupKey) {
+        return new DefaultImageRequest(displayName, lookupKey, ContactPhotoManager.TYPE_DEFAULT,
+                DEFAULT_IMAGE_LETTER_SCALE, /* offset = */ 0, /* isCircular = */ true);
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactTileView.java b/src/com/android/contacts/common/list/ContactTileView.java
new file mode 100644
index 0000000..172d720
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactTileView.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.R;
+
+/**
+ * A ContactTile displays a contact's picture and name
+ */
+public abstract class ContactTileView extends FrameLayout {
+    private final static String TAG = ContactTileView.class.getSimpleName();
+
+    private Uri mLookupUri;
+    private ImageView mPhoto;
+    private QuickContactBadge mQuickContact;
+    private TextView mName;
+    private TextView mStatus;
+    private TextView mPhoneLabel;
+    private TextView mPhoneNumber;
+    private ContactPhotoManager mPhotoManager = null;
+    private View mPushState;
+    private View mHorizontalDivider;
+    protected Listener mListener;
+
+    public ContactTileView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mName = (TextView) findViewById(R.id.contact_tile_name);
+
+        mQuickContact = (QuickContactBadge) findViewById(R.id.contact_tile_quick);
+        mPhoto = (ImageView) findViewById(R.id.contact_tile_image);
+        mStatus = (TextView) findViewById(R.id.contact_tile_status);
+        mPhoneLabel = (TextView) findViewById(R.id.contact_tile_phone_type);
+        mPhoneNumber = (TextView) findViewById(R.id.contact_tile_phone_number);
+        mPushState = findViewById(R.id.contact_tile_push_state);
+        mHorizontalDivider = findViewById(R.id.contact_tile_horizontal_divider);
+
+        OnClickListener listener = createClickListener();
+        setOnClickListener(listener);
+    }
+
+    protected OnClickListener createClickListener() {
+        return new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mListener == null) return;
+                mListener.onContactSelected(
+                        getLookupUri(),
+                        MoreContactUtils.getTargetRectFromView(ContactTileView.this));
+            }
+        };
+    }
+
+    public void setPhotoManager(ContactPhotoManager photoManager) {
+        mPhotoManager = photoManager;
+    }
+
+    /**
+     * Populates the data members to be displayed from the
+     * fields in {@link com.android.contacts.common.list.ContactEntry}
+     */
+    public void loadFromContact(ContactEntry entry) {
+
+        if (entry != null) {
+            mName.setText(getNameForView(entry));
+            mLookupUri = entry.lookupUri;
+
+            if (mStatus != null) {
+                if (entry.status == null) {
+                    mStatus.setVisibility(View.GONE);
+                } else {
+                    mStatus.setText(entry.status);
+                    mStatus.setCompoundDrawablesWithIntrinsicBounds(entry.presenceIcon,
+                            null, null, null);
+                    mStatus.setVisibility(View.VISIBLE);
+                }
+            }
+
+            if (mPhoneLabel != null) {
+                if (TextUtils.isEmpty(entry.phoneLabel)) {
+                    mPhoneLabel.setVisibility(View.GONE);
+                } else {
+                    mPhoneLabel.setVisibility(View.VISIBLE);
+                    mPhoneLabel.setText(entry.phoneLabel);
+                }
+            }
+
+            if (mPhoneNumber != null) {
+                // TODO: Format number correctly
+                mPhoneNumber.setText(entry.phoneNumber);
+            }
+
+            setVisibility(View.VISIBLE);
+
+            if (mPhotoManager != null) {
+                DefaultImageRequest request = getDefaultImageRequest(entry.namePrimary,
+                        entry.lookupKey);
+                configureViewForImage(entry.photoUri == null);
+                if (mPhoto != null) {
+                    mPhotoManager.loadPhoto(mPhoto, entry.photoUri, getApproximateImageSize(),
+                            isDarkTheme(), isContactPhotoCircular(), request);
+
+                    if (mQuickContact != null) {
+                        mQuickContact.assignContactUri(mLookupUri);
+                    }
+                } else if (mQuickContact != null) {
+                    mQuickContact.assignContactUri(mLookupUri);
+                    mPhotoManager.loadPhoto(mQuickContact, entry.photoUri,
+                            getApproximateImageSize(), isDarkTheme(), isContactPhotoCircular(),
+                            request);
+                }
+            } else {
+                Log.w(TAG, "contactPhotoManager not set");
+            }
+
+            if (mPushState != null) {
+                mPushState.setContentDescription(entry.namePrimary);
+            } else if (mQuickContact != null) {
+                mQuickContact.setContentDescription(entry.namePrimary);
+            }
+        } else {
+            setVisibility(View.INVISIBLE);
+        }
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+    public void setHorizontalDividerVisibility(int visibility) {
+        if (mHorizontalDivider != null) mHorizontalDivider.setVisibility(visibility);
+    }
+
+    public Uri getLookupUri() {
+        return mLookupUri;
+    }
+
+    protected QuickContactBadge getQuickContact() {
+        return mQuickContact;
+    }
+
+    protected View getPhotoView() {
+        return mPhoto;
+    }
+
+    /**
+     * Returns the string that should actually be displayed as the contact's name. Subclasses
+     * can override this to return formatted versions of the name - i.e. first name only.
+     */
+    protected String getNameForView(ContactEntry contactEntry) {
+        return contactEntry.namePrimary;
+    }
+
+    /**
+     * Implemented by subclasses to estimate the size of the picture. This can return -1 if only
+     * a thumbnail is shown anyway
+     */
+    protected abstract int getApproximateImageSize();
+
+    protected abstract boolean isDarkTheme();
+
+    /**
+     * Implemented by subclasses to reconfigure the view's layout and subviews, based on whether
+     * or not the contact has a user-defined photo.
+     *
+     * @param isDefaultImage True if the contact does not have a user-defined contact photo
+     * (which means a default contact image will be applied by the {@link ContactPhotoManager}
+     */
+    protected void configureViewForImage(boolean isDefaultImage) {
+        // No-op by default.
+    }
+
+    /**
+     * Implemented by subclasses to allow them to return a {@link DefaultImageRequest} with the
+     * various image parameters defined to match their own layouts.
+     *
+     * @param displayName The display name of the contact
+     * @param lookupKey The lookup key of the contact
+     * @return A {@link DefaultImageRequest} object with each field configured by the subclass
+     * as desired, or {@code null}.
+     */
+    protected DefaultImageRequest getDefaultImageRequest(String displayName, String lookupKey) {
+        return new DefaultImageRequest(displayName, lookupKey, isContactPhotoCircular());
+    }
+
+    /**
+     * Whether contact photo should be displayed as a circular image. Implemented by subclasses
+     * so they can change which drawables to fetch.
+     */
+    protected boolean isContactPhotoCircular() {
+        return true;
+    }
+
+    public interface Listener {
+        /**
+         * Notification that the contact was selected; no specific action is dictated.
+         */
+        void onContactSelected(Uri contactLookupUri, Rect viewRect);
+        /**
+         * Notification that the specified number is to be called.
+         */
+        void onCallNumberDirectly(String phoneNumber);
+        /**
+         * @return The width of each tile. This doesn't have to be a precise number (e.g. paddings
+         *         can be ignored), but is used to load the correct picture size from the database
+         */
+        int getApproximateTileWidth();
+    }
+}
diff --git a/src/com/android/contacts/common/list/ContactsSectionIndexer.java b/src/com/android/contacts/common/list/ContactsSectionIndexer.java
new file mode 100644
index 0000000..db64010
--- /dev/null
+++ b/src/com/android/contacts/common/list/ContactsSectionIndexer.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.text.TextUtils;
+import android.widget.SectionIndexer;
+
+import java.util.Arrays;
+
+/**
+ * A section indexer that is configured with precomputed section titles and
+ * their respective counts.
+ */
+public class ContactsSectionIndexer implements SectionIndexer {
+
+    protected static final String BLANK_HEADER_STRING = "\u2026"; // ellipsis
+
+    private String[] mSections;
+    private int[] mPositions;
+    private int mCount;
+
+    /**
+     * Constructor.
+     *
+     * @param sections a non-null array
+     * @param counts a non-null array of the same size as <code>sections</code>
+     */
+    public ContactsSectionIndexer(String[] sections, int[] counts) {
+        if (sections == null || counts == null) {
+            throw new NullPointerException();
+        }
+
+        if (sections.length != counts.length) {
+            throw new IllegalArgumentException(
+                    "The sections and counts arrays must have the same length");
+        }
+
+        // TODO process sections/counts based on current locale and/or specific section titles
+
+        this.mSections = sections;
+        mPositions = new int[counts.length];
+        int position = 0;
+        for (int i = 0; i < counts.length; i++) {
+            if (TextUtils.isEmpty(mSections[i])) {
+                mSections[i] = BLANK_HEADER_STRING;
+            } else if (!mSections[i].equals(BLANK_HEADER_STRING)) {
+                mSections[i] = mSections[i].trim();
+            }
+
+            mPositions[i] = position;
+            position += counts[i];
+        }
+        mCount = position;
+    }
+
+    public Object[] getSections() {
+        return mSections;
+    }
+
+    public int[] getPositions() {
+        return mPositions;
+    }
+
+    public int getPositionForSection(int section) {
+        if (section < 0 || section >= mSections.length) {
+            return -1;
+        }
+
+        return mPositions[section];
+    }
+
+    public int getSectionForPosition(int position) {
+        if (position < 0 || position >= mCount) {
+            return -1;
+        }
+
+        int index = Arrays.binarySearch(mPositions, position);
+
+        /*
+         * Consider this example: section positions are 0, 3, 5; the supplied
+         * position is 4. The section corresponding to position 4 starts at
+         * position 3, so the expected return value is 1. Binary search will not
+         * find 4 in the array and thus will return -insertPosition-1, i.e. -3.
+         * To get from that number to the expected value of 1 we need to negate
+         * and subtract 2.
+         */
+        return index >= 0 ? index : -index - 2;
+    }
+
+    public void setProfileAndFavoritesHeader(String header, int numberOfItemsToAdd) {
+        if (mSections != null) {
+            // Don't do anything if the header is already set properly.
+            if (mSections.length > 0 && header.equals(mSections[0])) {
+                return;
+            }
+
+            // Since the section indexer isn't aware of the profile at the top, we need to add a
+            // special section at the top for it and shift everything else down.
+            String[] tempSections = new String[mSections.length + 1];
+            int[] tempPositions = new int[mPositions.length + 1];
+            tempSections[0] = header;
+            tempPositions[0] = 0;
+            for (int i = 1; i <= mPositions.length; i++) {
+                tempSections[i] = mSections[i - 1];
+                tempPositions[i] = mPositions[i - 1] + numberOfItemsToAdd;
+            }
+            mSections = tempSections;
+            mPositions = tempPositions;
+            mCount = mCount + numberOfItemsToAdd;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/CustomContactListFilterActivity.java b/src/com/android/contacts/common/list/CustomContactListFilterActivity.java
new file mode 100644
index 0000000..04337b8
--- /dev/null
+++ b/src/com/android/contacts/common/list/CustomContactListFilterActivity.java
@@ -0,0 +1,948 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.app.ProgressDialog;
+import android.content.AsyncTaskLoader;
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.OperationApplicationException;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.Settings;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.CheckBox;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.util.EmptyService;
+import com.android.contacts.common.util.LocalizedNameResolver;
+import com.android.contacts.common.util.WeakAsyncTask;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+
+/**
+ * Shows a list of all available {@link Groups} available, letting the user
+ * select which ones they want to be visible.
+ */
+public class CustomContactListFilterActivity extends Activity implements
+        ExpandableListView.OnChildClickListener,
+        LoaderCallbacks<CustomContactListFilterActivity.AccountSet> {
+    private static final String TAG = "CustomContactListFilterActivity";
+
+    private static final int ACCOUNT_SET_LOADER_ID = 1;
+
+    private ExpandableListView mList;
+    private DisplayAdapter mAdapter;
+
+    private SharedPreferences mPrefs;
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.contact_list_filter_custom);
+
+        mList = (ExpandableListView) findViewById(android.R.id.list);
+        mList.setOnChildClickListener(this);
+        mList.setHeaderDividersEnabled(true);
+        mList.setChildDivider(new ColorDrawable(Color.TRANSPARENT));
+
+        mList.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+            @Override
+            public void onLayoutChange(final View v, final int left, final int top, final int right,
+                    final int bottom, final int oldLeft, final int oldTop, final int oldRight,
+                    final int oldBottom) {
+                mList.setIndicatorBounds(
+                        mList.getWidth() - getResources().getDimensionPixelSize(
+                                R.dimen.contact_filter_indicator_padding_end),
+                        mList.getWidth() - getResources().getDimensionPixelSize(
+                                R.dimen.contact_filter_indicator_padding_start));
+            }
+        });
+
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+        mAdapter = new DisplayAdapter(this);
+
+        mList.setOnCreateContextMenuListener(this);
+
+        mList.setAdapter(mAdapter);
+
+        ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            // android.R.id.home will be triggered in onOptionsItemSelected()
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+    }
+
+    public static class CustomFilterConfigurationLoader extends AsyncTaskLoader<AccountSet> {
+
+        private AccountSet mAccountSet;
+
+        public CustomFilterConfigurationLoader(Context context) {
+            super(context);
+        }
+
+        @Override
+        public AccountSet loadInBackground() {
+            Context context = getContext();
+            final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
+            final ContentResolver resolver = context.getContentResolver();
+
+            final AccountSet accounts = new AccountSet();
+            for (AccountWithDataSet account : accountTypes.getAccounts(false)) {
+                final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
+                if (accountType.isExtension() && !account.hasData(context)) {
+                    // Extension with no data -- skip.
+                    continue;
+                }
+
+                AccountDisplay accountDisplay =
+                        new AccountDisplay(resolver, account.name, account.type, account.dataSet);
+
+                final Uri.Builder groupsUri = Groups.CONTENT_URI.buildUpon()
+                        .appendQueryParameter(Groups.ACCOUNT_NAME, account.name)
+                        .appendQueryParameter(Groups.ACCOUNT_TYPE, account.type);
+                if (account.dataSet != null) {
+                    groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build();
+                }
+                final Cursor cursor = resolver.query(groupsUri.build(), null, null, null, null);
+                if (cursor == null) {
+                    continue;
+                }
+                android.content.EntityIterator iterator =
+                        ContactsContract.Groups.newEntityIterator(cursor);
+                try {
+                    boolean hasGroups = false;
+
+                    // Create entries for each known group
+                    while (iterator.hasNext()) {
+                        final ContentValues values = iterator.next().getEntityValues();
+                        final GroupDelta group = GroupDelta.fromBefore(values);
+                        accountDisplay.addGroup(group);
+                        hasGroups = true;
+                    }
+                    // Create single entry handling ungrouped status
+                    accountDisplay.mUngrouped =
+                        GroupDelta.fromSettings(resolver, account.name, account.type,
+                                account.dataSet, hasGroups);
+                    accountDisplay.addGroup(accountDisplay.mUngrouped);
+                } finally {
+                    iterator.close();
+                }
+
+                accounts.add(accountDisplay);
+            }
+
+            return accounts;
+        }
+
+        @Override
+        public void deliverResult(AccountSet cursor) {
+            if (isReset()) {
+                return;
+            }
+
+            mAccountSet = cursor;
+
+            if (isStarted()) {
+                super.deliverResult(cursor);
+            }
+        }
+
+        @Override
+        protected void onStartLoading() {
+            if (mAccountSet != null) {
+                deliverResult(mAccountSet);
+            }
+            if (takeContentChanged() || mAccountSet == null) {
+                forceLoad();
+            }
+        }
+
+        @Override
+        protected void onStopLoading() {
+            cancelLoad();
+        }
+
+        @Override
+        protected void onReset() {
+            super.onReset();
+            onStopLoading();
+            mAccountSet = null;
+        }
+    }
+
+    @Override
+    protected void onStart() {
+        getLoaderManager().initLoader(ACCOUNT_SET_LOADER_ID, null, this);
+        super.onStart();
+    }
+
+    @Override
+    public Loader<AccountSet> onCreateLoader(int id, Bundle args) {
+        return new CustomFilterConfigurationLoader(this);
+    }
+
+    @Override
+    public void onLoadFinished(Loader<AccountSet> loader, AccountSet data) {
+        mAdapter.setAccounts(data);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<AccountSet> loader) {
+        mAdapter.setAccounts(null);
+    }
+
+    private static final int DEFAULT_SHOULD_SYNC = 1;
+    private static final int DEFAULT_VISIBLE = 0;
+
+    /**
+     * Entry holding any changes to {@link Groups} or {@link Settings} rows,
+     * such as {@link Groups#SHOULD_SYNC} or {@link Groups#GROUP_VISIBLE}.
+     */
+    protected static class GroupDelta extends ValuesDelta {
+        private boolean mUngrouped = false;
+        private boolean mAccountHasGroups;
+
+        private GroupDelta() {
+            super();
+        }
+
+        /**
+         * Build {@link GroupDelta} from the {@link Settings} row for the given
+         * {@link Settings#ACCOUNT_NAME}, {@link Settings#ACCOUNT_TYPE}, and
+         * {@link Settings#DATA_SET}.
+         */
+        public static GroupDelta fromSettings(ContentResolver resolver, String accountName,
+                String accountType, String dataSet, boolean accountHasGroups) {
+            final Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon()
+                    .appendQueryParameter(Settings.ACCOUNT_NAME, accountName)
+                    .appendQueryParameter(Settings.ACCOUNT_TYPE, accountType);
+            if (dataSet != null) {
+                settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet);
+            }
+            final Cursor cursor = resolver.query(settingsUri.build(), new String[] {
+                    Settings.SHOULD_SYNC, Settings.UNGROUPED_VISIBLE
+            }, null, null, null);
+
+            try {
+                final ContentValues values = new ContentValues();
+                values.put(Settings.ACCOUNT_NAME, accountName);
+                values.put(Settings.ACCOUNT_TYPE, accountType);
+                values.put(Settings.DATA_SET, dataSet);
+
+                if (cursor != null && cursor.moveToFirst()) {
+                    // Read existing values when present
+                    values.put(Settings.SHOULD_SYNC, cursor.getInt(0));
+                    values.put(Settings.UNGROUPED_VISIBLE, cursor.getInt(1));
+                    return fromBefore(values).setUngrouped(accountHasGroups);
+                } else {
+                    // Nothing found, so treat as create
+                    values.put(Settings.SHOULD_SYNC, DEFAULT_SHOULD_SYNC);
+                    values.put(Settings.UNGROUPED_VISIBLE, DEFAULT_VISIBLE);
+                    return fromAfter(values).setUngrouped(accountHasGroups);
+                }
+            } finally {
+                if (cursor != null) cursor.close();
+            }
+        }
+
+        public static GroupDelta fromBefore(ContentValues before) {
+            final GroupDelta entry = new GroupDelta();
+            entry.mBefore = before;
+            entry.mAfter = new ContentValues();
+            return entry;
+        }
+
+        public static GroupDelta fromAfter(ContentValues after) {
+            final GroupDelta entry = new GroupDelta();
+            entry.mBefore = null;
+            entry.mAfter = after;
+            return entry;
+        }
+
+        protected GroupDelta setUngrouped(boolean accountHasGroups) {
+            mUngrouped = true;
+            mAccountHasGroups = accountHasGroups;
+            return this;
+        }
+
+        @Override
+        public boolean beforeExists() {
+            return mBefore != null;
+        }
+
+        public boolean getShouldSync() {
+            return getAsInteger(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC,
+                    DEFAULT_SHOULD_SYNC) != 0;
+        }
+
+        public boolean getVisible() {
+            return getAsInteger(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE,
+                    DEFAULT_VISIBLE) != 0;
+        }
+
+        public void putShouldSync(boolean shouldSync) {
+            put(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, shouldSync ? 1 : 0);
+        }
+
+        public void putVisible(boolean visible) {
+            put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0);
+        }
+
+        private String getAccountType() {
+            return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE);
+        }
+
+        public CharSequence getTitle(Context context) {
+            if (mUngrouped) {
+                final String customAllContactsName =
+                        LocalizedNameResolver.getAllContactsName(context, getAccountType());
+                if (customAllContactsName != null) {
+                    return customAllContactsName;
+                }
+                if (mAccountHasGroups) {
+                    return context.getText(R.string.display_ungrouped);
+                } else {
+                    return context.getText(R.string.display_all_contacts);
+                }
+            } else {
+                final Integer titleRes = getAsInteger(Groups.TITLE_RES);
+                if (titleRes != null) {
+                    final String packageName = getAsString(Groups.RES_PACKAGE);
+                    return context.getPackageManager().getText(packageName, titleRes, null);
+                } else {
+                    return getAsString(Groups.TITLE);
+                }
+            }
+        }
+
+        /**
+         * Build a possible {@link ContentProviderOperation} to persist any
+         * changes to the {@link Groups} or {@link Settings} row described by
+         * this {@link GroupDelta}.
+         */
+        public ContentProviderOperation buildDiff() {
+            if (isInsert()) {
+                // Only allow inserts for Settings
+                if (mUngrouped) {
+                    mAfter.remove(mIdColumn);
+                    return ContentProviderOperation.newInsert(Settings.CONTENT_URI)
+                            .withValues(mAfter)
+                            .build();
+                }
+                else {
+                    throw new IllegalStateException("Unexpected diff");
+                }
+            } else if (isUpdate()) {
+                if (mUngrouped) {
+                    String accountName = this.getAsString(Settings.ACCOUNT_NAME);
+                    String accountType = this.getAsString(Settings.ACCOUNT_TYPE);
+                    String dataSet = this.getAsString(Settings.DATA_SET);
+                    StringBuilder selection = new StringBuilder(Settings.ACCOUNT_NAME + "=? AND "
+                            + Settings.ACCOUNT_TYPE + "=?");
+                    String[] selectionArgs;
+                    if (dataSet == null) {
+                        selection.append(" AND " + Settings.DATA_SET + " IS NULL");
+                        selectionArgs = new String[] {accountName, accountType};
+                    } else {
+                        selection.append(" AND " + Settings.DATA_SET + "=?");
+                        selectionArgs = new String[] {accountName, accountType, dataSet};
+                    }
+                    return ContentProviderOperation.newUpdate(Settings.CONTENT_URI)
+                            .withSelection(selection.toString(), selectionArgs)
+                            .withValues(mAfter)
+                            .build();
+                } else {
+                    return ContentProviderOperation.newUpdate(
+                                    addCallerIsSyncAdapterParameter(Groups.CONTENT_URI))
+                            .withSelection(Groups._ID + "=" + this.getId(), null)
+                            .withValues(mAfter)
+                            .build();
+                }
+            } else {
+                return null;
+            }
+        }
+    }
+
+    private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
+        return uri.buildUpon()
+            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+            .build();
+    }
+
+    /**
+     * {@link Comparator} to sort by {@link Groups#_ID}.
+     */
+    private static Comparator<GroupDelta> sIdComparator = new Comparator<GroupDelta>() {
+        public int compare(GroupDelta object1, GroupDelta object2) {
+            final Long id1 = object1.getId();
+            final Long id2 = object2.getId();
+            if (id1 == null && id2 == null) {
+                return 0;
+            } else if (id1 == null) {
+                return -1;
+            } else if (id2 == null) {
+                return 1;
+            } else if (id1 < id2) {
+                return -1;
+            } else if (id1 > id2) {
+                return 1;
+            } else {
+                return 0;
+            }
+        }
+    };
+
+    /**
+     * Set of all {@link AccountDisplay} entries, one for each source.
+     */
+    protected static class AccountSet extends ArrayList<AccountDisplay> {
+        public ArrayList<ContentProviderOperation> buildDiff() {
+            final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+            for (AccountDisplay account : this) {
+                account.buildDiff(diff);
+            }
+            return diff;
+        }
+    }
+
+    /**
+     * {@link GroupDelta} details for a single {@link AccountWithDataSet}, usually shown as
+     * children under a single expandable group.
+     */
+    protected static class AccountDisplay {
+        public final String mName;
+        public final String mType;
+        public final String mDataSet;
+
+        public GroupDelta mUngrouped;
+        public ArrayList<GroupDelta> mSyncedGroups = Lists.newArrayList();
+        public ArrayList<GroupDelta> mUnsyncedGroups = Lists.newArrayList();
+
+        /**
+         * Build an {@link AccountDisplay} covering all {@link Groups} under the
+         * given {@link AccountWithDataSet}.
+         */
+        public AccountDisplay(ContentResolver resolver, String accountName, String accountType,
+                String dataSet) {
+            mName = accountName;
+            mType = accountType;
+            mDataSet = dataSet;
+        }
+
+        /**
+         * Add the given {@link GroupDelta} internally, filing based on its
+         * {@link GroupDelta#getShouldSync()} status.
+         */
+        private void addGroup(GroupDelta group) {
+            if (group.getShouldSync()) {
+                mSyncedGroups.add(group);
+            } else {
+                mUnsyncedGroups.add(group);
+            }
+        }
+
+        /**
+         * Set the {@link GroupDelta#putShouldSync(boolean)} value for all
+         * children {@link GroupDelta} rows.
+         */
+        public void setShouldSync(boolean shouldSync) {
+            final Iterator<GroupDelta> oppositeChildren = shouldSync ?
+                    mUnsyncedGroups.iterator() : mSyncedGroups.iterator();
+            while (oppositeChildren.hasNext()) {
+                final GroupDelta child = oppositeChildren.next();
+                setShouldSync(child, shouldSync, false);
+                oppositeChildren.remove();
+            }
+        }
+
+        public void setShouldSync(GroupDelta child, boolean shouldSync) {
+            setShouldSync(child, shouldSync, true);
+        }
+
+        /**
+         * Set {@link GroupDelta#putShouldSync(boolean)}, and file internally
+         * based on updated state.
+         */
+        public void setShouldSync(GroupDelta child, boolean shouldSync, boolean attemptRemove) {
+            child.putShouldSync(shouldSync);
+            if (shouldSync) {
+                if (attemptRemove) {
+                    mUnsyncedGroups.remove(child);
+                }
+                mSyncedGroups.add(child);
+                Collections.sort(mSyncedGroups, sIdComparator);
+            } else {
+                if (attemptRemove) {
+                    mSyncedGroups.remove(child);
+                }
+                mUnsyncedGroups.add(child);
+            }
+        }
+
+        /**
+         * Build set of {@link ContentProviderOperation} to persist any user
+         * changes to {@link GroupDelta} rows under this {@link AccountWithDataSet}.
+         */
+        public void buildDiff(ArrayList<ContentProviderOperation> diff) {
+            for (GroupDelta group : mSyncedGroups) {
+                final ContentProviderOperation oper = group.buildDiff();
+                if (oper != null) diff.add(oper);
+            }
+            for (GroupDelta group : mUnsyncedGroups) {
+                final ContentProviderOperation oper = group.buildDiff();
+                if (oper != null) diff.add(oper);
+            }
+        }
+    }
+
+    /**
+     * {@link ExpandableListAdapter} that shows {@link GroupDelta} settings,
+     * grouped by {@link AccountWithDataSet} type. Shows footer row when any groups are
+     * unsynced, as determined through {@link AccountDisplay#mUnsyncedGroups}.
+     */
+    protected static class DisplayAdapter extends BaseExpandableListAdapter {
+        private Context mContext;
+        private LayoutInflater mInflater;
+        private AccountTypeManager mAccountTypes;
+        private AccountSet mAccounts;
+
+        private boolean mChildWithPhones = false;
+
+        public DisplayAdapter(Context context) {
+            mContext = context;
+            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            mAccountTypes = AccountTypeManager.getInstance(context);
+        }
+
+        public void setAccounts(AccountSet accounts) {
+            mAccounts = accounts;
+            notifyDataSetChanged();
+        }
+
+        /**
+         * In group descriptions, show the number of contacts with phone
+         * numbers, in addition to the total contacts.
+         */
+        public void setChildDescripWithPhones(boolean withPhones) {
+            mChildWithPhones = withPhones;
+        }
+
+        @Override
+        public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+                ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(
+                        R.layout.custom_contact_list_filter_account, parent, false);
+            }
+
+            final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+            final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+
+            final AccountDisplay account = (AccountDisplay)this.getGroup(groupPosition);
+
+            final AccountType accountType = mAccountTypes.getAccountType(
+                    account.mType, account.mDataSet);
+
+            text1.setText(account.mName);
+            text1.setVisibility(account.mName == null ? View.GONE : View.VISIBLE);
+            text2.setText(accountType.getDisplayLabel(mContext));
+
+            final int textColor = mContext.getResources().getColor(isExpanded
+                    ? R.color.dialtacts_theme_color
+                    : R.color.account_filter_text_color);
+            text1.setTextColor(textColor);
+            text2.setTextColor(textColor);
+
+            return convertView;
+        }
+
+        @Override
+        public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+                View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(
+                        R.layout.custom_contact_list_filter_group, parent, false);
+            }
+
+            final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+            final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+            final CheckBox checkbox = (CheckBox)convertView.findViewById(android.R.id.checkbox);
+
+            final AccountDisplay account = mAccounts.get(groupPosition);
+            final GroupDelta child = (GroupDelta)this.getChild(groupPosition, childPosition);
+            if (child != null) {
+                // Handle normal group, with title and checkbox
+                final boolean groupVisible = child.getVisible();
+                checkbox.setVisibility(View.VISIBLE);
+                checkbox.setChecked(groupVisible);
+
+                final CharSequence groupTitle = child.getTitle(mContext);
+                text1.setText(groupTitle);
+                text2.setVisibility(View.GONE);
+            } else {
+                // When unknown child, this is "more" footer view
+                checkbox.setVisibility(View.GONE);
+                text1.setText(R.string.display_more_groups);
+                text2.setVisibility(View.GONE);
+            }
+
+            // Show divider at bottom only for the last child.
+            final View dividerBottom = convertView.findViewById(R.id.adapter_divider_bottom);
+            dividerBottom.setVisibility(isLastChild ? View.VISIBLE : View.GONE);
+
+            return convertView;
+        }
+
+        @Override
+        public Object getChild(int groupPosition, int childPosition) {
+            final AccountDisplay account = mAccounts.get(groupPosition);
+            final boolean validChild = childPosition >= 0
+                    && childPosition < account.mSyncedGroups.size();
+            if (validChild) {
+                return account.mSyncedGroups.get(childPosition);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        public long getChildId(int groupPosition, int childPosition) {
+            final GroupDelta child = (GroupDelta)getChild(groupPosition, childPosition);
+            if (child != null) {
+                final Long childId = child.getId();
+                return childId != null ? childId : Long.MIN_VALUE;
+            } else {
+                return Long.MIN_VALUE;
+            }
+        }
+
+        @Override
+        public int getChildrenCount(int groupPosition) {
+            // Count is any synced groups, plus possible footer
+            final AccountDisplay account = mAccounts.get(groupPosition);
+            final boolean anyHidden = account.mUnsyncedGroups.size() > 0;
+            return account.mSyncedGroups.size() + (anyHidden ? 1 : 0);
+        }
+
+        @Override
+        public Object getGroup(int groupPosition) {
+            return mAccounts.get(groupPosition);
+        }
+
+        @Override
+        public int getGroupCount() {
+            if (mAccounts == null) {
+                return 0;
+            }
+            return mAccounts.size();
+        }
+
+        @Override
+        public long getGroupId(int groupPosition) {
+            return groupPosition;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return true;
+        }
+
+        @Override
+        public boolean isChildSelectable(int groupPosition, int childPosition) {
+            return true;
+        }
+    }
+
+    /**
+     * Handle any clicks on {@link ExpandableListAdapter} children, which
+     * usually mean toggling its visible state.
+     */
+    @Override
+    public boolean onChildClick(ExpandableListView parent, View view, int groupPosition,
+            int childPosition, long id) {
+        final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox);
+
+        final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition);
+        final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition);
+        if (child != null) {
+            checkbox.toggle();
+            child.putVisible(checkbox.isChecked());
+        } else {
+            // Open context menu for bringing back unsynced
+            this.openContextMenu(view);
+        }
+        return true;
+    }
+
+    // TODO: move these definitions to framework constants when we begin
+    // defining this mode through <sync-adapter> tags
+    private static final int SYNC_MODE_UNSUPPORTED = 0;
+    private static final int SYNC_MODE_UNGROUPED = 1;
+    private static final int SYNC_MODE_EVERYTHING = 2;
+
+    protected int getSyncMode(AccountDisplay account) {
+        // TODO: read sync mode through <sync-adapter> definition
+        if (GoogleAccountType.ACCOUNT_TYPE.equals(account.mType) && account.mDataSet == null) {
+            return SYNC_MODE_EVERYTHING;
+        } else {
+            return SYNC_MODE_UNSUPPORTED;
+        }
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View view,
+            ContextMenu.ContextMenuInfo menuInfo) {
+        super.onCreateContextMenu(menu, view, menuInfo);
+
+        // Bail if not working with expandable long-press, or if not child
+        if (!(menuInfo instanceof ExpandableListContextMenuInfo)) return;
+
+        final ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo;
+        final int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+        final int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition);
+
+        // Skip long-press on expandable parents
+        if (childPosition == -1) return;
+
+        final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition);
+        final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition);
+
+        // Ignore when selective syncing unsupported
+        final int syncMode = getSyncMode(account);
+        if (syncMode == SYNC_MODE_UNSUPPORTED) return;
+
+        if (child != null) {
+            showRemoveSync(menu, account, child, syncMode);
+        } else {
+            showAddSync(menu, account, syncMode);
+        }
+    }
+
+    protected void showRemoveSync(ContextMenu menu, final AccountDisplay account,
+            final GroupDelta child, final int syncMode) {
+        final CharSequence title = child.getTitle(this);
+
+        menu.setHeaderTitle(title);
+        menu.add(R.string.menu_sync_remove).setOnMenuItemClickListener(
+                new OnMenuItemClickListener() {
+                    public boolean onMenuItemClick(MenuItem item) {
+                        handleRemoveSync(account, child, syncMode, title);
+                        return true;
+                    }
+                });
+    }
+
+    protected void handleRemoveSync(final AccountDisplay account, final GroupDelta child,
+            final int syncMode, CharSequence title) {
+        final boolean shouldSyncUngrouped = account.mUngrouped.getShouldSync();
+        if (syncMode == SYNC_MODE_EVERYTHING && shouldSyncUngrouped
+                && !child.equals(account.mUngrouped)) {
+            // Warn before removing this group when it would cause ungrouped to stop syncing
+            final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+            final CharSequence removeMessage = this.getString(
+                    R.string.display_warn_remove_ungrouped, title);
+            builder.setTitle(R.string.menu_sync_remove);
+            builder.setMessage(removeMessage);
+            builder.setNegativeButton(android.R.string.cancel, null);
+            builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                public void onClick(DialogInterface dialog, int which) {
+                    // Mark both this group and ungrouped to stop syncing
+                    account.setShouldSync(account.mUngrouped, false);
+                    account.setShouldSync(child, false);
+                    mAdapter.notifyDataSetChanged();
+                }
+            });
+            builder.show();
+        } else {
+            // Mark this group to not sync
+            account.setShouldSync(child, false);
+            mAdapter.notifyDataSetChanged();
+        }
+    }
+
+    protected void showAddSync(ContextMenu menu, final AccountDisplay account, final int syncMode) {
+        menu.setHeaderTitle(R.string.dialog_sync_add);
+
+        // Create item for each available, unsynced group
+        for (final GroupDelta child : account.mUnsyncedGroups) {
+            if (!child.getShouldSync()) {
+                final CharSequence title = child.getTitle(this);
+                menu.add(title).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+                    public boolean onMenuItemClick(MenuItem item) {
+                        // Adding specific group for syncing
+                        if (child.mUngrouped && syncMode == SYNC_MODE_EVERYTHING) {
+                            account.setShouldSync(true);
+                        } else {
+                            account.setShouldSync(child, true);
+                        }
+                        mAdapter.notifyDataSetChanged();
+                        return true;
+                    }
+                });
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void doSaveAction() {
+        if (mAdapter == null || mAdapter.mAccounts == null) {
+            finish();
+            return;
+        }
+
+        setResult(RESULT_OK);
+
+        final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff();
+        if (diff.isEmpty()) {
+            finish();
+            return;
+        }
+
+        new UpdateTask(this).execute(diff);
+    }
+
+    /**
+     * Background task that persists changes to {@link Groups#GROUP_VISIBLE},
+     * showing spinner dialog to user while updating.
+     */
+    public static class UpdateTask extends
+            WeakAsyncTask<ArrayList<ContentProviderOperation>, Void, Void, Activity> {
+        private ProgressDialog mProgress;
+
+        public UpdateTask(Activity target) {
+            super(target);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void onPreExecute(Activity target) {
+            final Context context = target;
+
+            mProgress = ProgressDialog.show(
+                    context, null, context.getText(R.string.savingDisplayGroups));
+
+            // Before starting this task, start an empty service to protect our
+            // process from being reclaimed by the system.
+            context.startService(new Intent(context, EmptyService.class));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected Void doInBackground(
+                Activity target, ArrayList<ContentProviderOperation>... params) {
+            final Context context = target;
+            final ContentValues values = new ContentValues();
+            final ContentResolver resolver = context.getContentResolver();
+
+            try {
+                final ArrayList<ContentProviderOperation> diff = params[0];
+                resolver.applyBatch(ContactsContract.AUTHORITY, diff);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Problem saving display groups", e);
+            } catch (OperationApplicationException e) {
+                Log.e(TAG, "Problem saving display groups", e);
+            }
+
+            return null;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void onPostExecute(Activity target, Void result) {
+            final Context context = target;
+
+            try {
+                mProgress.dismiss();
+            } catch (Exception e) {
+                Log.e(TAG, "Error dismissing progress dialog", e);
+            }
+
+            target.finish();
+
+            // Stop the service that was protecting us
+            context.stopService(new Intent(context, EmptyService.class));
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+
+        final MenuItem menuItem = menu.add(Menu.NONE, R.id.menu_save, Menu.NONE,
+                R.string.menu_custom_filter_save);
+        menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                // Pretend cancel.
+                setResult(Activity.RESULT_CANCELED);
+                finish();
+                return true;
+            case R.id.menu_save:
+                this.doSaveAction();
+                return true;
+            default:
+                break;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+}
diff --git a/src/com/android/contacts/common/list/DefaultContactListAdapter.java b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
new file mode 100644
index 0000000..994fe1a
--- /dev/null
+++ b/src/com/android/contacts/common/list/DefaultContactListAdapter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.SearchSnippets;
+import android.text.TextUtils;
+import android.view.View;
+
+import com.android.contacts.common.Experiments;
+import com.android.contacts.common.compat.ContactsCompat;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.commonbind.experiments.Flags;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
+ */
+public class DefaultContactListAdapter extends ContactListAdapter {
+
+    public static final char SNIPPET_START_MATCH = '[';
+    public static final char SNIPPET_END_MATCH = ']';
+
+    // Contacts contacted within the last 3 days (in seconds)
+    private static final long LAST_TIME_USED_3_DAYS_SEC = 3L * 24 * 60 * 60;
+
+    // Contacts contacted within the last 7 days (in seconds)
+    private static final long LAST_TIME_USED_7_DAYS_SEC = 7L * 24 * 60 * 60;
+
+    // Contacts contacted within the last 14 days (in seconds)
+    private static final long LAST_TIME_USED_14_DAYS_SEC = 14L * 24 * 60 * 60;
+
+    // Contacts contacted within the last 30 days (in seconds)
+    private static final long LAST_TIME_USED_30_DAYS_SEC = 30L * 24 * 60 * 60;
+
+    private static final String TIME_SINCE_LAST_USED_SEC =
+            "(strftime('%s', 'now') - " + Contacts.LAST_TIME_CONTACTED + "/1000)";
+
+    private static final String STREQUENT_SORT =
+            "(CASE WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_3_DAYS_SEC +
+                    " THEN 0 " +
+                    " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_7_DAYS_SEC +
+                    " THEN 1 " +
+                    " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_14_DAYS_SEC +
+                    " THEN 2 " +
+                    " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_30_DAYS_SEC +
+                    " THEN 3 " +
+                    " ELSE 4 END), " +
+                    Contacts.TIMES_CONTACTED + " DESC, " +
+                    Contacts.STARRED + " DESC";
+
+    public DefaultContactListAdapter(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader, long directoryId) {
+        if (loader instanceof FavoritesAndContactsLoader) {
+            ((FavoritesAndContactsLoader) loader).setLoadFavorites(shouldIncludeFavorites());
+        }
+
+        String sortOrder = null;
+        if (isSearchMode()) {
+            String query = getQueryString();
+            if (query == null) query = "";
+            query = query.trim();
+            if (TextUtils.isEmpty(query)) {
+                // Regardless of the directory, we don't want anything returned,
+                // so let's just send a "nothing" query to the local directory.
+                loader.setUri(Contacts.CONTENT_URI);
+                loader.setProjection(getProjection(false));
+                loader.setSelection("0");
+            } else if (isGroupMembersFilter()) {
+                final ContactListFilter filter = getFilter();
+                configureUri(loader, directoryId, filter);
+                // TODO: This is not the normal type to filter URI so we load the non-search
+                // projection. Consider creating a specific group member adapter to make it more
+                // clear.
+                loader.setProjection(getProjection(/* forSearch */ false));
+                loader.setSelection(
+                        Contacts.DISPLAY_NAME_PRIMARY + " LIKE ?1 OR " +
+                        Contacts.DISPLAY_NAME_ALTERNATIVE + " LIKE ?1");
+                final String[] args = new String[1];
+                args[0] = query + "%";
+                loader.setSelectionArgs(args);
+            } else {
+                final Builder builder = ContactsCompat.getContentUri().buildUpon();
+                appendSearchParameters(builder, query, directoryId);
+                loader.setUri(builder.build());
+                loader.setProjection(getProjection(true));
+                sortOrder = STREQUENT_SORT;
+            }
+        } else {
+            final ContactListFilter filter = getFilter();
+            configureUri(loader, directoryId, filter);
+            if (filter != null
+                    && filter.filterType == ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS) {
+                loader.setProjection(getDataProjectionForContacts(false));
+            } else {
+                loader.setProjection(getProjection(false));
+            }
+            configureSelection(loader, directoryId, filter);
+        }
+
+        if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+            if (sortOrder == null) {
+                sortOrder = Contacts.SORT_KEY_PRIMARY;
+            } else {
+                sortOrder += ", " + Contacts.SORT_KEY_PRIMARY;
+            }
+        } else {
+            if (sortOrder == null) {
+                sortOrder = Contacts.SORT_KEY_ALTERNATIVE;
+            } else {
+                sortOrder += ", " + Contacts.SORT_KEY_ALTERNATIVE;
+            }
+        }
+        loader.setSortOrder(sortOrder);
+    }
+
+    private boolean isGroupMembersFilter() {
+        final ContactListFilter filter = getFilter();
+        return filter != null && filter.filterType == ContactListFilter.FILTER_TYPE_GROUP_MEMBERS;
+    }
+
+    private void appendSearchParameters(Builder builder, String query, long directoryId) {
+        builder.appendPath(query); // Builder will encode the query
+        builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                String.valueOf(directoryId));
+        if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) {
+            builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
+                    String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
+        }
+        builder.appendQueryParameter(SearchSnippets.DEFERRED_SNIPPETING_KEY, "1");
+    }
+
+    protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) {
+        Uri uri = Contacts.CONTENT_URI;
+        if (filter != null) {
+            if (filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
+                String lookupKey = getSelectedContactLookupKey();
+                if (lookupKey != null) {
+                    uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+                } else {
+                    uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, getSelectedContactId());
+                }
+            } else if (filter.filterType == ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS) {
+                uri = Data.CONTENT_URI;
+            }
+        }
+
+        if (directoryId == Directory.DEFAULT && isSectionHeaderDisplayEnabled()) {
+            uri = ContactListAdapter.buildSectionIndexerUri(uri);
+        }
+
+        // The "All accounts" filter is the same as the entire contents of Directory.DEFAULT
+        if (filter != null
+                && filter.filterType != ContactListFilter.FILTER_TYPE_CUSTOM
+                && filter.filterType != ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
+            final Uri.Builder builder = uri.buildUpon();
+            builder.appendQueryParameter(
+                    ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
+            if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT
+                || filter.filterType == ContactListFilter.FILTER_TYPE_GROUP_MEMBERS) {
+                filter.addAccountQueryParameterToUrl(builder);
+            }
+            uri = builder.build();
+        }
+
+        loader.setUri(uri);
+    }
+
+    private void configureSelection(
+            CursorLoader loader, long directoryId, ContactListFilter filter) {
+        if (filter == null) {
+            return;
+        }
+
+        if (directoryId != Directory.DEFAULT) {
+            return;
+        }
+
+        StringBuilder selection = new StringBuilder();
+        List<String> selectionArgs = new ArrayList<String>();
+
+        switch (filter.filterType) {
+            case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: {
+                // We have already added directory=0 to the URI, which takes care of this
+                // filter
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: {
+                // We have already added the lookup key to the URI, which takes care of this
+                // filter
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_STARRED: {
+                selection.append(Contacts.STARRED + "!=0");
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: {
+                selection.append(Contacts.HAS_PHONE_NUMBER + "=1");
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_CUSTOM: {
+                selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
+                if (isCustomFilterForPhoneNumbersOnly()) {
+                    selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
+                }
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_ACCOUNT: {
+                // We use query parameters for account filter, so no selection to add here.
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_GROUP_MEMBERS: {
+                // TODO(wjang): check if we need it
+                // selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS: {
+                selection.append(AccountWithDataSet.LOCAL_ACCOUNT_SELECTION);
+                break;
+            }
+        }
+        loader.setSelection(selection.toString());
+        loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
+    }
+
+    @Override
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+        super.bindView(itemView, partition, cursor, position);
+        final ContactListItemView view = (ContactListItemView)itemView;
+
+        view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
+
+        if (isSelectionVisible()) {
+            view.setActivated(isSelectedContact(partition, cursor));
+        }
+
+        bindSectionHeaderAndDivider(view, position, cursor);
+
+        if (isQuickContactEnabled()) {
+            bindQuickContact(view, partition, cursor, ContactQuery.CONTACT_PHOTO_ID,
+                    ContactQuery.CONTACT_PHOTO_URI, ContactQuery.CONTACT_ID,
+                    ContactQuery.CONTACT_LOOKUP_KEY, ContactQuery.CONTACT_DISPLAY_NAME);
+        } else {
+            if (getDisplayPhotos()) {
+                bindPhoto(view, partition, cursor);
+            }
+        }
+
+        bindNameAndViewId(view, cursor);
+        bindPresenceAndStatusMessage(view, cursor);
+
+        if (isSearchMode()) {
+            bindSearchSnippet(view, cursor);
+        } else {
+            view.setSnippet(null);
+        }
+    }
+
+    private boolean isCustomFilterForPhoneNumbersOnly() {
+        // TODO: this flag should not be stored in shared prefs.  It needs to be in the db.
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
+        return prefs.getBoolean(ContactsPreferences.PREF_DISPLAY_ONLY_PHONES,
+                ContactsPreferences.PREF_DISPLAY_ONLY_PHONES_DEFAULT);
+    }
+}
diff --git a/src/com/android/contacts/common/list/DirectoryListLoader.java b/src/com/android/contacts/common/list/DirectoryListLoader.java
new file mode 100644
index 0000000..c45a3ca
--- /dev/null
+++ b/src/com/android/contacts/common/list/DirectoryListLoader.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.DirectoryCompat;
+
+/**
+ * A specialized loader for the list of directories, see {@link Directory}.
+ */
+public class DirectoryListLoader extends AsyncTaskLoader<Cursor> {
+
+    private static final String TAG = "ContactEntryListAdapter";
+
+    public static final int SEARCH_MODE_NONE = 0;
+    public static final int SEARCH_MODE_DEFAULT = 1;
+    public static final int SEARCH_MODE_CONTACT_SHORTCUT = 2;
+    public static final int SEARCH_MODE_DATA_SHORTCUT = 3;
+
+    private static final class DirectoryQuery {
+        public static final String ORDER_BY = Directory._ID;
+
+        public static final String[] PROJECTION = {
+            Directory._ID,
+            Directory.PACKAGE_NAME,
+            Directory.TYPE_RESOURCE_ID,
+            Directory.DISPLAY_NAME,
+            Directory.PHOTO_SUPPORT,
+        };
+
+        public static final int ID = 0;
+        public static final int PACKAGE_NAME = 1;
+        public static final int TYPE_RESOURCE_ID = 2;
+        public static final int DISPLAY_NAME = 3;
+        public static final int PHOTO_SUPPORT = 4;
+
+        public static Uri getDirectoryUri(int mode) {
+            if (mode == SEARCH_MODE_DATA_SHORTCUT || mode == SEARCH_MODE_CONTACT_SHORTCUT) {
+                return Directory.CONTENT_URI;
+            } else {
+                return DirectoryCompat.getContentUri();
+            }
+        }
+    }
+
+    // This is a virtual column created for a MatrixCursor.
+    public static final String DIRECTORY_TYPE = "directoryType";
+
+    private static final String[] RESULT_PROJECTION = {
+        Directory._ID,
+        DIRECTORY_TYPE,
+        Directory.DISPLAY_NAME,
+        Directory.PHOTO_SUPPORT,
+    };
+
+    private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+        @Override
+        public void onChange(boolean selfChange) {
+            forceLoad();
+        }
+    };
+
+    private int mDirectorySearchMode;
+    private boolean mLocalInvisibleDirectoryEnabled;
+
+    private MatrixCursor mDefaultDirectoryList;
+
+    public DirectoryListLoader(Context context) {
+        super(context);
+    }
+
+    public void setDirectorySearchMode(int mode) {
+        mDirectorySearchMode = mode;
+    }
+
+    /**
+     * A flag that indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should
+     * be included in the results.
+     */
+    public void setLocalInvisibleDirectoryEnabled(boolean flag) {
+        this.mLocalInvisibleDirectoryEnabled = flag;
+    }
+
+    @Override
+    protected void onStartLoading() {
+        getContext().getContentResolver().
+                registerContentObserver(DirectoryQuery.getDirectoryUri(mDirectorySearchMode),
+                        false, mObserver);
+        forceLoad();
+    }
+
+    @Override
+    protected void onStopLoading() {
+        getContext().getContentResolver().unregisterContentObserver(mObserver);
+    }
+
+    @Override
+    public Cursor loadInBackground() {
+        if (mDirectorySearchMode == SEARCH_MODE_NONE) {
+            return getDefaultDirectories();
+        }
+
+        MatrixCursor result = new MatrixCursor(RESULT_PROJECTION);
+        Context context = getContext();
+        PackageManager pm = context.getPackageManager();
+        String selection;
+        switch (mDirectorySearchMode) {
+            case SEARCH_MODE_DEFAULT:
+                selection = null;
+                break;
+
+            case SEARCH_MODE_CONTACT_SHORTCUT:
+                selection = Directory.SHORTCUT_SUPPORT + "=" + Directory.SHORTCUT_SUPPORT_FULL;
+                break;
+
+            case SEARCH_MODE_DATA_SHORTCUT:
+                selection = Directory.SHORTCUT_SUPPORT + " IN ("
+                        + Directory.SHORTCUT_SUPPORT_FULL + ", "
+                        + Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY + ")";
+                break;
+
+            default:
+                throw new RuntimeException(
+                        "Unsupported directory search mode: " + mDirectorySearchMode);
+        }
+        Cursor cursor = null;
+        try {
+            cursor = context.getContentResolver().query(
+                    DirectoryQuery.getDirectoryUri(mDirectorySearchMode),
+                    DirectoryQuery.PROJECTION, selection, null, DirectoryQuery.ORDER_BY);
+
+            if (cursor == null) {
+                return result;
+            }
+
+            while(cursor.moveToNext()) {
+                long directoryId = cursor.getLong(DirectoryQuery.ID);
+                if (!mLocalInvisibleDirectoryEnabled
+                        && DirectoryCompat.isInvisibleDirectory(directoryId)) {
+                    continue;
+                }
+                String directoryType = null;
+
+                String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+                int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+                if (!TextUtils.isEmpty(packageName) && typeResourceId != 0) {
+                    try {
+                        directoryType = pm.getResourcesForApplication(packageName)
+                                .getString(typeResourceId);
+                    } catch (Exception e) {
+                        Log.e(TAG, "Cannot obtain directory type from package: " + packageName);
+                    }
+                }
+                String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+                int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT);
+                result.addRow(new Object[]{directoryId, directoryType, displayName, photoSupport});
+            }
+        } catch (RuntimeException e) {
+            Log.w(TAG, "Runtime Exception when querying directory");
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        return result;
+    }
+
+    private Cursor getDefaultDirectories() {
+        if (mDefaultDirectoryList == null) {
+            mDefaultDirectoryList = new MatrixCursor(RESULT_PROJECTION);
+            mDefaultDirectoryList.addRow(new Object[] {
+                    Directory.DEFAULT,
+                    getContext().getString(R.string.contactsList),
+                    null
+            });
+            mDefaultDirectoryList.addRow(new Object[] {
+                    Directory.LOCAL_INVISIBLE,
+                    getContext().getString(R.string.local_invisible_directory),
+                    null
+            });
+        }
+        return mDefaultDirectoryList;
+    }
+
+    @Override
+    protected void onReset() {
+        stopLoading();
+    }
+}
diff --git a/src/com/android/contacts/common/list/DirectoryPartition.java b/src/com/android/contacts/common/list/DirectoryPartition.java
new file mode 100644
index 0000000..ca0dc11
--- /dev/null
+++ b/src/com/android/contacts/common/list/DirectoryPartition.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.provider.ContactsContract.Directory;
+
+import com.android.common.widget.CompositeCursorAdapter;
+
+/**
+ * Model object for a {@link Directory} row.
+ */
+public final class DirectoryPartition extends CompositeCursorAdapter.Partition {
+
+    public static final int STATUS_NOT_LOADED = 0;
+    public static final int STATUS_LOADING = 1;
+    public static final int STATUS_LOADED = 2;
+
+    public static final int RESULT_LIMIT_DEFAULT = -1;
+
+    private long mDirectoryId;
+    private String mContentUri;
+    private String mDirectoryType;
+    private String mDisplayName;
+    private int mStatus;
+    private boolean mPriorityDirectory;
+    private boolean mPhotoSupported;
+    private int mResultLimit = RESULT_LIMIT_DEFAULT;
+    private boolean mDisplayNumber = true;
+
+    private String mLabel;
+
+    public DirectoryPartition(boolean showIfEmpty, boolean hasHeader) {
+        super(showIfEmpty, hasHeader);
+    }
+
+    /**
+     * Directory ID, see {@link Directory}.
+     */
+    public long getDirectoryId() {
+        return mDirectoryId;
+    }
+
+    public void setDirectoryId(long directoryId) {
+        this.mDirectoryId = directoryId;
+    }
+
+    /**
+     * Directory type resolved from {@link Directory#PACKAGE_NAME} and
+     * {@link Directory#TYPE_RESOURCE_ID};
+     */
+    public String getDirectoryType() {
+        return mDirectoryType;
+    }
+
+    public void setDirectoryType(String directoryType) {
+        this.mDirectoryType = directoryType;
+    }
+
+    /**
+     * See {@link Directory#DISPLAY_NAME}.
+     */
+    public String getDisplayName() {
+        return mDisplayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.mDisplayName = displayName;
+    }
+
+    public int getStatus() {
+        return mStatus;
+    }
+
+    public void setStatus(int status) {
+        mStatus = status;
+    }
+
+    public boolean isLoading() {
+        return mStatus == STATUS_NOT_LOADED || mStatus == STATUS_LOADING;
+    }
+
+    /**
+     * Returns true if this directory should be loaded before non-priority directories.
+     */
+    public boolean isPriorityDirectory() {
+        return mPriorityDirectory;
+    }
+
+    public void setPriorityDirectory(boolean priorityDirectory) {
+        mPriorityDirectory = priorityDirectory;
+    }
+
+    /**
+     * Returns true if this directory supports photos.
+     */
+    public boolean isPhotoSupported() {
+        return mPhotoSupported;
+    }
+
+    public void setPhotoSupported(boolean flag) {
+        this.mPhotoSupported = flag;
+    }
+
+    /**
+     * Max number of results for this directory. Defaults to {@link #RESULT_LIMIT_DEFAULT} which
+     * implies using the adapter's
+     * {@link com.android.contacts.common.list.ContactListAdapter#getDirectoryResultLimit()}
+     */
+    public int getResultLimit() {
+        return mResultLimit;
+    }
+
+    public void setResultLimit(int resultLimit) {
+        mResultLimit = resultLimit;
+    }
+
+    /**
+     * Used by extended directories to specify a custom content URI. Extended directories MUST have
+     * a content URI
+     */
+    public String getContentUri() {
+        return mContentUri;
+    }
+
+    public void setContentUri(String contentUri) {
+        mContentUri = contentUri;
+    }
+
+    /**
+     * A label to display in the header next to the display name.
+     */
+    public String getLabel() {
+        return mLabel;
+    }
+
+    public void setLabel(String label) {
+        mLabel = label;
+    }
+
+    @Override
+    public String toString() {
+        return "DirectoryPartition{" +
+                "mDirectoryId=" + mDirectoryId +
+                ", mContentUri='" + mContentUri + '\'' +
+                ", mDirectoryType='" + mDirectoryType + '\'' +
+                ", mDisplayName='" + mDisplayName + '\'' +
+                ", mStatus=" + mStatus +
+                ", mPriorityDirectory=" + mPriorityDirectory +
+                ", mPhotoSupported=" + mPhotoSupported +
+                ", mResultLimit=" + mResultLimit +
+                ", mLabel='" + mLabel + '\'' +
+                '}';
+    }
+
+    /**
+     * Whether or not to display the phone number in app that have that option - Dialer. If false,
+     * Phone Label should be used instead of Phone Number.
+     */
+    public boolean isDisplayNumber() {
+        return mDisplayNumber;
+    }
+
+    public void setDisplayNumber(boolean displayNumber) {
+        mDisplayNumber = displayNumber;
+    }
+}
diff --git a/src/com/android/contacts/common/list/FavoritesAndContactsLoader.java b/src/com/android/contacts/common/list/FavoritesAndContactsLoader.java
new file mode 100644
index 0000000..d1ae911
--- /dev/null
+++ b/src/com/android/contacts/common/list/FavoritesAndContactsLoader.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * A loader for use in the default contact list, which will also query for favorite contacts
+ * if configured to do so.
+ */
+public class FavoritesAndContactsLoader extends CursorLoader {
+
+    private boolean mLoadFavorites;
+
+    private String[] mProjection;
+
+    private Uri mExtraUri;
+    private String[] mExtraProjection;
+    private String mExtraSelection;
+    private String[] mExtraSelectionArgs;
+    private boolean mMergeExtraContactsAfterPrimary;
+
+    public FavoritesAndContactsLoader(Context context) {
+        super(context);
+    }
+
+    /** Whether to load favorites and merge results in before any other results. */
+    public void setLoadFavorites(boolean flag) {
+        mLoadFavorites = flag;
+    }
+
+    public void setProjection(String[] projection) {
+        super.setProjection(projection);
+        mProjection = projection;
+    }
+
+    /** Configure an extra query and merge results in before the primary results. */
+    public void setLoadExtraContactsFirst(Uri uri, String[] projection) {
+        mExtraUri = uri;
+        mExtraProjection = projection;
+        mMergeExtraContactsAfterPrimary = false;
+    }
+
+    /** Configure an extra query and merge results in after the primary results. */
+    public void setLoadExtraContactsLast(Uri uri, String[] projection, String selection,
+            String[] selectionArgs) {
+        mExtraUri = uri;
+        mExtraProjection = projection;
+        mExtraSelection = selection;
+        mExtraSelectionArgs = selectionArgs;
+        mMergeExtraContactsAfterPrimary = true;
+    }
+
+    private boolean canLoadExtraContacts() {
+        return mExtraUri != null && mExtraProjection != null;
+    }
+
+    @Override
+    public Cursor loadInBackground() {
+        List<Cursor> cursors = Lists.newArrayList();
+        if (mLoadFavorites) {
+            cursors.add(loadFavoritesContacts());
+        }
+        if (canLoadExtraContacts() && !mMergeExtraContactsAfterPrimary) {
+            cursors.add(loadExtraContacts());
+        }
+        // ContactsCursor.loadInBackground() can return null; MergeCursor
+        // correctly handles null cursors.
+        Cursor cursor = null;
+        try {
+            cursor = super.loadInBackground();
+        } catch (NullPointerException | SecurityException e) {
+            // Ignore NPEs and SecurityExceptions thrown by providers
+        }
+        final Cursor contactsCursor = cursor;
+        cursors.add(contactsCursor);
+        if (canLoadExtraContacts() && mMergeExtraContactsAfterPrimary) {
+            cursors.add(loadExtraContacts());
+        }
+        return new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) {
+            @Override
+            public Bundle getExtras() {
+                // Need to get the extras from the contacts cursor.
+                return contactsCursor == null ? new Bundle() : contactsCursor.getExtras();
+            }
+        };
+    }
+
+    private Cursor loadExtraContacts() {
+        return getContext().getContentResolver().query(
+                mExtraUri, mExtraProjection, mExtraSelection, mExtraSelectionArgs, null);
+    }
+
+    private Cursor loadFavoritesContacts() {
+        return getContext().getContentResolver().query(
+                Contacts.CONTENT_URI, mProjection, Contacts.STARRED + "=?", new String[]{"1"},
+                getSortOrder());
+    }
+}
diff --git a/src/com/android/contacts/common/list/IndexerListAdapter.java b/src/com/android/contacts/common/list/IndexerListAdapter.java
new file mode 100644
index 0000000..032bb53
--- /dev/null
+++ b/src/com/android/contacts/common/list/IndexerListAdapter.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+
+/**
+ * A list adapter that supports section indexer and a pinned header.
+ */
+public abstract class IndexerListAdapter extends PinnedHeaderListAdapter implements SectionIndexer {
+
+    protected Context mContext;
+    private SectionIndexer mIndexer;
+    private int mIndexedPartition = 0;
+    private boolean mSectionHeaderDisplayEnabled;
+    private View mHeader;
+
+    /**
+     * An item view is displayed differently depending on whether it is placed
+     * at the beginning, middle or end of a section. It also needs to know the
+     * section header when it is at the beginning of a section. This object
+     * captures all this configuration.
+     */
+    public static final class Placement {
+        private int position = ListView.INVALID_POSITION;
+        public boolean firstInSection;
+        public boolean lastInSection;
+        public String sectionHeader;
+
+        public void invalidate() {
+            position = ListView.INVALID_POSITION;
+        }
+    }
+
+    private Placement mPlacementCache = new Placement();
+
+    /**
+     * Constructor.
+     */
+    public IndexerListAdapter(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    /**
+     * Creates a section header view that will be pinned at the top of the list
+     * as the user scrolls.
+     */
+    protected abstract View createPinnedSectionHeaderView(Context context, ViewGroup parent);
+
+    /**
+     * Sets the title in the pinned header as the user scrolls.
+     */
+    protected abstract void setPinnedSectionTitle(View pinnedHeaderView, String title);
+
+    public boolean isSectionHeaderDisplayEnabled() {
+        return mSectionHeaderDisplayEnabled;
+    }
+
+    public void setSectionHeaderDisplayEnabled(boolean flag) {
+        this.mSectionHeaderDisplayEnabled = flag;
+    }
+
+    public int getIndexedPartition() {
+        return mIndexedPartition;
+    }
+
+    public void setIndexedPartition(int partition) {
+        this.mIndexedPartition = partition;
+    }
+
+    public SectionIndexer getIndexer() {
+        return mIndexer;
+    }
+
+    public void setIndexer(SectionIndexer indexer) {
+        mIndexer = indexer;
+        mPlacementCache.invalidate();
+    }
+
+    public Object[] getSections() {
+        if (mIndexer == null) {
+            return new String[] { " " };
+        } else {
+            return mIndexer.getSections();
+        }
+    }
+
+    /**
+     * @return relative position of the section in the indexed partition
+     */
+    public int getPositionForSection(int sectionIndex) {
+        if (mIndexer == null) {
+            return -1;
+        }
+
+        return mIndexer.getPositionForSection(sectionIndex);
+    }
+
+    /**
+     * @param position relative position in the indexed partition
+     */
+    public int getSectionForPosition(int position) {
+        if (mIndexer == null) {
+            return -1;
+        }
+
+        return mIndexer.getSectionForPosition(position);
+    }
+
+    @Override
+    public int getPinnedHeaderCount() {
+        if (isSectionHeaderDisplayEnabled()) {
+            return super.getPinnedHeaderCount() + 1;
+        } else {
+            return super.getPinnedHeaderCount();
+        }
+    }
+
+    @Override
+    public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) {
+        if (isSectionHeaderDisplayEnabled() && viewIndex == getPinnedHeaderCount() - 1) {
+            if (mHeader == null) {
+                mHeader = createPinnedSectionHeaderView(mContext, parent);
+            }
+            return mHeader;
+        } else {
+            return super.getPinnedHeaderView(viewIndex, convertView, parent);
+        }
+    }
+
+    @Override
+    public void configurePinnedHeaders(PinnedHeaderListView listView) {
+        super.configurePinnedHeaders(listView);
+
+        if (!isSectionHeaderDisplayEnabled()) {
+            return;
+        }
+
+        int index = getPinnedHeaderCount() - 1;
+        if (mIndexer == null || getCount() == 0) {
+            listView.setHeaderInvisible(index, false);
+        } else {
+            int listPosition = listView.getPositionAt(listView.getTotalTopPinnedHeaderHeight());
+            int position = listPosition - listView.getHeaderViewsCount();
+
+            int section = -1;
+            int partition = getPartitionForPosition(position);
+            if (partition == mIndexedPartition) {
+                int offset = getOffsetInPartition(position);
+                if (offset != -1) {
+                    section = getSectionForPosition(offset);
+                }
+            }
+
+            if (section == -1) {
+                listView.setHeaderInvisible(index, false);
+            } else {
+                View topChild = listView.getChildAt(listPosition);
+                if (topChild != null) {
+                    // Match the pinned header's height to the height of the list item.
+                    mHeader.setMinimumHeight(topChild.getMeasuredHeight());
+                }
+                setPinnedSectionTitle(mHeader, (String)mIndexer.getSections()[section]);
+
+                // Compute the item position where the current partition begins
+                int partitionStart = getPositionForPartition(mIndexedPartition);
+                if (hasHeader(mIndexedPartition)) {
+                    partitionStart++;
+                }
+
+                // Compute the item position where the next section begins
+                int nextSectionPosition = partitionStart + getPositionForSection(section + 1);
+                boolean isLastInSection = position == nextSectionPosition - 1;
+                listView.setFadingHeader(index, listPosition, isLastInSection);
+            }
+        }
+    }
+
+    /**
+     * Computes the item's placement within its section and populates the {@code placement}
+     * object accordingly.  Please note that the returned object is volatile and should be
+     * copied if the result needs to be used later.
+     */
+    public Placement getItemPlacementInSection(int position) {
+        if (mPlacementCache.position == position) {
+            return mPlacementCache;
+        }
+
+        mPlacementCache.position = position;
+        if (isSectionHeaderDisplayEnabled()) {
+            int section = getSectionForPosition(position);
+            if (section != -1 && getPositionForSection(section) == position) {
+                mPlacementCache.firstInSection = true;
+                mPlacementCache.sectionHeader = (String)getSections()[section];
+            } else {
+                mPlacementCache.firstInSection = false;
+                mPlacementCache.sectionHeader = null;
+            }
+
+            mPlacementCache.lastInSection = (getPositionForSection(section + 1) - 1 == position);
+        } else {
+            mPlacementCache.firstInSection = false;
+            mPlacementCache.lastInSection = false;
+            mPlacementCache.sectionHeader = null;
+        }
+        return mPlacementCache;
+    }
+}
diff --git a/src/com/android/contacts/common/list/MultiSelectEntryContactListAdapter.java b/src/com/android/contacts/common/list/MultiSelectEntryContactListAdapter.java
new file mode 100644
index 0000000..9ab6e1c
--- /dev/null
+++ b/src/com/android/contacts/common/list/MultiSelectEntryContactListAdapter.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.ContactsContract;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.CheckBox;
+
+import java.util.TreeSet;
+
+/**
+ * An extension of the default contact adapter that adds checkboxes and the ability
+ * to select multiple contacts.
+ */
+public abstract class MultiSelectEntryContactListAdapter extends ContactEntryListAdapter {
+
+    private SelectedContactsListener mSelectedContactsListener;
+    private DeleteContactListener mDeleteContactListener;
+    private TreeSet<Long> mSelectedContactIds = new TreeSet<Long>();
+    private boolean mDisplayCheckBoxes;
+    private final int mContactIdColumnIndex;
+
+    public interface SelectedContactsListener {
+        void onSelectedContactsChanged();
+        void onSelectedContactsChangedViaCheckBox();
+    }
+
+    public interface DeleteContactListener {
+        void onContactDeleteClicked(int position);
+    }
+
+    /**
+     * @param contactIdColumnIndex the column index of the contact ID in the underlying cursor;
+     *         it is passed in so that this adapter can support different kinds of contact
+     *         lists (e.g. aggregate contacts or raw contacts).
+     */
+    public MultiSelectEntryContactListAdapter(Context context, int contactIdColumnIndex) {
+        super(context);
+        mContactIdColumnIndex = contactIdColumnIndex;
+    }
+
+    /**
+     * Returns the column index of the contact ID in the underlying cursor; the contact ID
+     * retrieved using this index is the value that is selected by this adapter (and returned
+     * by {@link #getSelectedContactIds}).
+     */
+    public int getContactColumnIdIndex() {
+        return mContactIdColumnIndex;
+    }
+
+    public DeleteContactListener getDeleteContactListener() {
+        return mDeleteContactListener;
+    }
+
+    public void setDeleteContactListener(DeleteContactListener deleteContactListener) {
+        mDeleteContactListener = deleteContactListener;
+    }
+
+    public void setSelectedContactsListener(SelectedContactsListener listener) {
+        mSelectedContactsListener = listener;
+    }
+
+    /**
+     * Returns set of selected contacts.
+     */
+    public TreeSet<Long> getSelectedContactIds() {
+        return mSelectedContactIds;
+    }
+
+    /**
+     * Returns the selected contacts as an array.
+     */
+    public long[] getSelectedContactIdsArray() {
+        final Long[] contactIds = mSelectedContactIds.toArray(
+                new Long[mSelectedContactIds.size()]);
+        final long[] result = new long[contactIds.length];
+        for (int i = 0; i < contactIds.length; i++) {
+            result[i] = contactIds[i];
+        }
+        return result;
+    }
+
+    /**
+     * Update set of selected contacts. This changes which checkboxes are set.
+     */
+    public void setSelectedContactIds(TreeSet<Long> selectedContactIds) {
+        this.mSelectedContactIds = selectedContactIds;
+        notifyDataSetChanged();
+        if (mSelectedContactsListener != null) {
+            mSelectedContactsListener.onSelectedContactsChanged();
+        }
+    }
+
+    /**
+     * Shows checkboxes beside contacts if {@param displayCheckBoxes} is {@code TRUE}.
+     * Not guaranteed to work with all configurations of this adapter.
+     */
+    public void setDisplayCheckBoxes(boolean showCheckBoxes) {
+        if (!mDisplayCheckBoxes && showCheckBoxes) {
+            setSelectedContactIds(new TreeSet<Long>());
+        }
+        mDisplayCheckBoxes = showCheckBoxes;
+        notifyDataSetChanged();
+        if (mSelectedContactsListener != null) {
+            mSelectedContactsListener.onSelectedContactsChanged();
+        }
+    }
+
+    /**
+     * Checkboxes are being displayed beside contacts.
+     */
+    public boolean isDisplayingCheckBoxes() {
+        return mDisplayCheckBoxes;
+    }
+
+    /**
+     * Toggle the checkbox beside the contact for {@param contactId}.
+     */
+    public void toggleSelectionOfContactId(long contactId) {
+        if (mSelectedContactIds.contains(contactId)) {
+            mSelectedContactIds.remove(contactId);
+        } else {
+            mSelectedContactIds.add(contactId);
+        }
+        notifyDataSetChanged();
+        if (mSelectedContactsListener != null) {
+            mSelectedContactsListener.onSelectedContactsChanged();
+        }
+    }
+
+    @Override
+    public long getItemId(int position) {
+        Cursor cursor = (Cursor) getItem(position);
+        if (cursor != null) {
+            return cursor.getLong(getContactColumnIdIndex());
+        }
+        return 0;
+     }
+
+    @Override
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+        super.bindView(itemView, partition, cursor, position);
+        final ContactListItemView view = (ContactListItemView) itemView;
+        bindViewId(view, cursor, getContactColumnIdIndex());
+        bindCheckBox(view, cursor, position, partition == ContactsContract.Directory.DEFAULT);
+    }
+
+    private void bindCheckBox(ContactListItemView view, Cursor cursor, int position,
+            boolean isLocalDirectory) {
+        // Disable clicking on all contacts from remote directories when showing check boxes. We do
+        // this by telling the view to handle clicking itself.
+        view.setClickable(!isLocalDirectory && mDisplayCheckBoxes);
+        // Only show checkboxes if mDisplayCheckBoxes is enabled. Also, never show the
+        // checkbox for other directory contacts except local directory.
+        if (!mDisplayCheckBoxes || !isLocalDirectory) {
+            view.hideCheckBox();
+            return;
+        }
+        final CheckBox checkBox = view.getCheckBox();
+        final long contactId = cursor.getLong(mContactIdColumnIndex);
+        checkBox.setChecked(mSelectedContactIds.contains(contactId));
+        checkBox.setTag(contactId);
+        checkBox.setOnClickListener(mCheckBoxClickListener);
+    }
+
+    private final OnClickListener mCheckBoxClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            final CheckBox checkBox = (CheckBox) v;
+            final Long contactId = (Long) checkBox.getTag();
+            if (checkBox.isChecked()) {
+                mSelectedContactIds.add(contactId);
+            } else {
+                mSelectedContactIds.remove(contactId);
+            }
+            notifyDataSetChanged();
+            if (mSelectedContactsListener != null) {
+                mSelectedContactsListener.onSelectedContactsChangedViaCheckBox();
+            }
+        }
+    };
+}
diff --git a/src/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java b/src/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java
new file mode 100644
index 0000000..fe23054
--- /dev/null
+++ b/src/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.app.ActionBar;
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Action callbacks that can be sent by a phone number picker.
+ */
+public interface OnPhoneNumberPickerActionListener  {
+    public static final int CALL_INITIATION_UNKNOWN = 0;
+
+    /**
+     * Returns the selected phone number uri to the requester.
+     */
+    void onPickDataUri(Uri dataUri, boolean isVideoCall, int callInitiationType);
+
+    /**
+     * Returns the specified phone number to the requester.
+     * May call the specified phone number, either as an audio or video call.
+     */
+    void onPickPhoneNumber(String phoneNumber, boolean isVideoCall, int callInitiationType);
+
+    /**
+     * Returns the selected number as a shortcut intent.
+     */
+    void onShortcutIntentCreated(Intent intent);
+
+    /**
+     * Called when home menu in {@link ActionBar} is clicked by the user.
+     */
+    void onHomeInActionBarSelected();
+}
diff --git a/src/com/android/contacts/common/list/PhoneNumberListAdapter.java b/src/com/android/contacts/common/list/PhoneNumberListAdapter.java
new file mode 100644
index 0000000..348cac2
--- /dev/null
+++ b/src/com/android/contacts/common/list/PhoneNumberListAdapter.java
@@ -0,0 +1,657 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.CallableCompat;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.compat.PhoneCompat;
+import com.android.contacts.common.extensions.ExtendedPhoneDirectoriesManager;
+import com.android.contacts.common.extensions.ExtensionsFactory;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.Constants;
+
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and
+ * {@link SipAddress#CONTENT_ITEM_TYPE}.
+ *
+ * By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} is
+ * called with "true", this adapter starts handling SIP addresses too, by using {@link Callable}
+ * API instead of {@link Phone}.
+ */
+public class PhoneNumberListAdapter extends ContactEntryListAdapter {
+
+    private static final String TAG = PhoneNumberListAdapter.class.getSimpleName();
+
+    public interface Listener {
+        void onVideoCallIconClicked(int position);
+    }
+
+    // A list of extended directories to add to the directories from the database
+    private final List<DirectoryPartition> mExtendedDirectories;
+
+    // Extended directories will have ID's that are higher than any of the id's from the database,
+    // so that we can identify them and set them up properly. If no extended directories
+    // exist, this will be Long.MAX_VALUE
+    private long mFirstExtendedDirectoryId = Long.MAX_VALUE;
+
+    public static class PhoneQuery {
+
+        /**
+         * Optional key used as part of a JSON lookup key to specify an analytics category
+         * associated with the row.
+         */
+        public static final String ANALYTICS_CATEGORY = "analytics_category";
+
+        /**
+         * Optional key used as part of a JSON lookup key to specify an analytics action associated
+         * with the row.
+         */
+        public static final String ANALYTICS_ACTION = "analytics_action";
+
+        /**
+         * Optional key used as part of a JSON lookup key to specify an analytics value associated
+         * with the row.
+         */
+        public static final String ANALYTICS_VALUE = "analytics_value";
+
+        public static final String[] PROJECTION_PRIMARY_INTERNAL = new String[] {
+            Phone._ID,                          // 0
+            Phone.TYPE,                         // 1
+            Phone.LABEL,                        // 2
+            Phone.NUMBER,                       // 3
+            Phone.CONTACT_ID,                   // 4
+            Phone.LOOKUP_KEY,                   // 5
+            Phone.PHOTO_ID,                     // 6
+            Phone.DISPLAY_NAME_PRIMARY,         // 7
+            Phone.PHOTO_THUMBNAIL_URI,          // 8
+        };
+
+        public static final String[] PROJECTION_PRIMARY;
+
+        static {
+            final List<String> projectionList = Lists.newArrayList(PROJECTION_PRIMARY_INTERNAL);
+            if (CompatUtils.isMarshmallowCompatible()) {
+                projectionList.add(Phone.CARRIER_PRESENCE); // 9
+            }
+            PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]);
+        }
+
+        public static final String[] PROJECTION_ALTERNATIVE_INTERNAL = new String[] {
+            Phone._ID,                          // 0
+            Phone.TYPE,                         // 1
+            Phone.LABEL,                        // 2
+            Phone.NUMBER,                       // 3
+            Phone.CONTACT_ID,                   // 4
+            Phone.LOOKUP_KEY,                   // 5
+            Phone.PHOTO_ID,                     // 6
+            Phone.DISPLAY_NAME_ALTERNATIVE,     // 7
+            Phone.PHOTO_THUMBNAIL_URI,          // 8
+        };
+
+        public static final String[] PROJECTION_ALTERNATIVE;
+
+        static {
+            final List<String> projectionList = Lists.newArrayList(PROJECTION_ALTERNATIVE_INTERNAL);
+            if (CompatUtils.isMarshmallowCompatible()) {
+                projectionList.add(Phone.CARRIER_PRESENCE); // 9
+            }
+            PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]);
+        }
+
+        public static final int PHONE_ID                = 0;
+        public static final int PHONE_TYPE              = 1;
+        public static final int PHONE_LABEL             = 2;
+        public static final int PHONE_NUMBER            = 3;
+        public static final int CONTACT_ID              = 4;
+        public static final int LOOKUP_KEY              = 5;
+        public static final int PHOTO_ID                = 6;
+        public static final int DISPLAY_NAME            = 7;
+        public static final int PHOTO_URI               = 8;
+        public static final int CARRIER_PRESENCE        = 9;
+    }
+
+    private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE =
+            "length(" + Phone.NUMBER + ") < 1000";
+
+    private final CharSequence mUnknownNameText;
+    private final String mCountryIso;
+
+    private ContactListItemView.PhotoPosition mPhotoPosition;
+
+    private boolean mUseCallableUri;
+
+    private Listener mListener;
+
+    private boolean mIsVideoEnabled;
+    private boolean mIsPresenceEnabled;
+
+    public PhoneNumberListAdapter(Context context) {
+        super(context);
+        setDefaultFilterHeaderText(R.string.list_filter_phones);
+        mUnknownNameText = context.getText(android.R.string.unknownName);
+        mCountryIso = GeoUtil.getCurrentCountryIso(context);
+
+        final ExtendedPhoneDirectoriesManager manager
+                = ExtensionsFactory.getExtendedPhoneDirectoriesManager();
+        if (manager != null) {
+            mExtendedDirectories = manager.getExtendedDirectories(mContext);
+        } else {
+            // Empty list to avoid sticky NPE's
+            mExtendedDirectories = new ArrayList<DirectoryPartition>();
+        }
+
+        int videoCapabilities = CallUtil.getVideoCallingAvailability(context);
+        mIsVideoEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_ENABLED) != 0;
+        mIsPresenceEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_PRESENCE) != 0;
+    }
+
+    protected CharSequence getUnknownNameText() {
+        return mUnknownNameText;
+    }
+
+    @Override
+    public void configureLoader(CursorLoader loader, long directoryId) {
+        String query = getQueryString();
+        if (query == null) {
+            query = "";
+        }
+        if (isExtendedDirectory(directoryId)) {
+            final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId);
+            final String contentUri = directory.getContentUri();
+            if (contentUri == null) {
+                throw new IllegalStateException("Extended directory must have a content URL: "
+                        + directory);
+            }
+            final Builder builder = Uri.parse(contentUri).buildUpon();
+            builder.appendPath(query);
+            builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
+                    String.valueOf(getDirectoryResultLimit(directory)));
+            loader.setUri(builder.build());
+            loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
+        } else {
+            final boolean isRemoteDirectoryQuery
+                    = DirectoryCompat.isRemoteDirectoryId(directoryId);
+            final Builder builder;
+            if (isSearchMode()) {
+                final Uri baseUri;
+                if (isRemoteDirectoryQuery) {
+                    baseUri = PhoneCompat.getContentFilterUri();
+                } else if (mUseCallableUri) {
+                    baseUri = CallableCompat.getContentFilterUri();
+                } else {
+                    baseUri = PhoneCompat.getContentFilterUri();
+                }
+                builder = baseUri.buildUpon();
+                builder.appendPath(query);      // Builder will encode the query
+                builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                        String.valueOf(directoryId));
+                if (isRemoteDirectoryQuery) {
+                    builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
+                            String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
+                }
+            } else {
+                Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
+                builder = baseUri.buildUpon().appendQueryParameter(
+                        ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
+                if (isSectionHeaderDisplayEnabled()) {
+                    builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true");
+                }
+                applyFilter(loader, builder, directoryId, getFilter());
+            }
+
+            // Ignore invalid phone numbers that are too long. These can potentially cause freezes
+            // in the UI and there is no reason to display them.
+            final String prevSelection = loader.getSelection();
+            final String newSelection;
+            if (!TextUtils.isEmpty(prevSelection)) {
+                newSelection = prevSelection + " AND " + IGNORE_NUMBER_TOO_LONG_CLAUSE;
+            } else {
+                newSelection = IGNORE_NUMBER_TOO_LONG_CLAUSE;
+            }
+            loader.setSelection(newSelection);
+
+            // Remove duplicates when it is possible.
+            builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
+            loader.setUri(builder.build());
+
+            // TODO a projection that includes the search snippet
+            if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+                loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
+            } else {
+                loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
+            }
+
+            if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+                loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
+            } else {
+                loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
+            }
+        }
+    }
+
+    protected boolean isExtendedDirectory(long directoryId) {
+        return directoryId >= mFirstExtendedDirectoryId;
+    }
+
+    private DirectoryPartition getExtendedDirectoryFromId(long directoryId) {
+        final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId);
+        return mExtendedDirectories.get(directoryIndex);
+    }
+
+    /**
+     * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code
+     * filter}.
+     */
+    private void applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId,
+            ContactListFilter filter) {
+        if (filter == null || directoryId != Directory.DEFAULT) {
+            return;
+        }
+
+        final StringBuilder selection = new StringBuilder();
+        final List<String> selectionArgs = new ArrayList<String>();
+
+        switch (filter.filterType) {
+            case ContactListFilter.FILTER_TYPE_CUSTOM: {
+                selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
+                selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_ACCOUNT: {
+                filter.addAccountQueryParameterToUrl(uriBuilder);
+                break;
+            }
+            case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
+            case ContactListFilter.FILTER_TYPE_DEFAULT:
+                break; // No selection needed.
+            case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+                break; // This adapter is always "phone only", so no selection needed either.
+            default:
+                Log.w(TAG, "Unsupported filter type came " +
+                        "(type: " + filter.filterType + ", toString: " + filter + ")" +
+                        " showing all contacts.");
+                // No selection.
+                break;
+        }
+        loader.setSelection(selection.toString());
+        loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
+    }
+
+    @Override
+    public String getContactDisplayName(int position) {
+        return ((Cursor) getItem(position)).getString(PhoneQuery.DISPLAY_NAME);
+    }
+
+    public String getPhoneNumber(int position) {
+        final Cursor item = (Cursor)getItem(position);
+        return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null;
+    }
+
+    /**
+     * Builds a {@link Data#CONTENT_URI} for the given cursor position.
+     *
+     * @return Uri for the data. may be null if the cursor is not ready.
+     */
+    public Uri getDataUri(int position) {
+        final int partitionIndex = getPartitionForPosition(position);
+        final Cursor item = (Cursor)getItem(position);
+        return item != null ? getDataUri(partitionIndex, item) : null;
+    }
+
+    public Uri getDataUri(int partitionIndex, Cursor cursor) {
+        final long directoryId =
+                ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
+        if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
+            return null;
+        } else if (DirectoryCompat.isEnterpriseDirectoryId(directoryId)) {
+            /*
+             * ContentUris.withAppendedId(Data.CONTENT_URI, phoneId), is invalid if
+             * isEnterpriseDirectoryId returns true, because the uri itself will fail since the
+             * ContactsProvider in Android Framework currently doesn't support it. return null until
+             * Android framework has enterprise version of Data.CONTENT_URI
+             */
+            return null;
+        } else {
+            final long phoneId = cursor.getLong(PhoneQuery.PHONE_ID);
+            return ContentUris.withAppendedId(Data.CONTENT_URI, phoneId);
+        }
+    }
+
+    /**
+     * Retrieves the lookup key for the given cursor position.
+     *
+     * @param position The cursor position.
+     * @return The lookup key.
+     */
+    public String getLookupKey(int position) {
+        final Cursor item = (Cursor)getItem(position);
+        return item != null ? item.getString(PhoneQuery.LOOKUP_KEY) : null;
+    }
+
+    @Override
+    protected ContactListItemView newView(
+            Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+        ContactListItemView view = super.newView(context, partition, cursor, position, parent);
+        view.setUnknownNameText(mUnknownNameText);
+        view.setQuickContactEnabled(isQuickContactEnabled());
+        view.setPhotoPosition(mPhotoPosition);
+        return view;
+    }
+
+    protected void setHighlight(ContactListItemView view, Cursor cursor) {
+        view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
+    }
+
+    // Override default, which would return number of phone numbers, so we
+    // instead return number of contacts.
+    @Override
+    protected int getResultCount(Cursor cursor) {
+        if (cursor == null) {
+            return 0;
+        }
+        cursor.moveToPosition(-1);
+        long curContactId = -1;
+        int numContacts = 0;
+        while(cursor.moveToNext()) {
+            final long contactId = cursor.getLong(PhoneQuery.CONTACT_ID);
+            if (contactId != curContactId) {
+                curContactId = contactId;
+                ++numContacts;
+            }
+        }
+        return numContacts;
+    }
+
+    @Override
+    protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+        super.bindView(itemView, partition, cursor, position);
+        ContactListItemView view = (ContactListItemView)itemView;
+
+        setHighlight(view, cursor);
+
+        // Look at elements before and after this position, checking if contact IDs are same.
+        // If they have one same contact ID, it means they can be grouped.
+        //
+        // In one group, only the first entry will show its photo and its name, and the other
+        // entries in the group show just their data (e.g. phone number, email address).
+        cursor.moveToPosition(position);
+        boolean isFirstEntry = true;
+        boolean showBottomDivider = true;
+        final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
+        if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
+            final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
+            if (currentContactId == previousContactId) {
+                isFirstEntry = false;
+            }
+        }
+        cursor.moveToPosition(position);
+        if (cursor.moveToNext() && !cursor.isAfterLast()) {
+            final long nextContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
+            if (currentContactId == nextContactId) {
+                // The following entry should be in the same group, which means we don't want a
+                // divider between them.
+                // TODO: we want a different divider than the divider between groups. Just hiding
+                // this divider won't be enough.
+                showBottomDivider = false;
+            }
+        }
+        cursor.moveToPosition(position);
+
+        bindViewId(view, cursor, PhoneQuery.PHONE_ID);
+
+        bindSectionHeaderAndDivider(view, position);
+        if (isFirstEntry) {
+            bindName(view, cursor);
+            if (isQuickContactEnabled()) {
+                bindQuickContact(view, partition, cursor, PhoneQuery.PHOTO_ID,
+                        PhoneQuery.PHOTO_URI, PhoneQuery.CONTACT_ID,
+                        PhoneQuery.LOOKUP_KEY, PhoneQuery.DISPLAY_NAME);
+            } else {
+                if (getDisplayPhotos()) {
+                    bindPhoto(view, partition, cursor);
+                }
+            }
+        } else {
+            unbindName(view);
+
+            view.removePhotoView(true, false);
+        }
+
+        final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
+        bindPhoneNumber(view, cursor, directory.isDisplayNumber(), position);
+    }
+
+    protected void bindPhoneNumber(ContactListItemView view, Cursor cursor, boolean displayNumber,
+            int position) {
+        CharSequence label = null;
+        if (displayNumber &&  !cursor.isNull(PhoneQuery.PHONE_TYPE)) {
+            final int type = cursor.getInt(PhoneQuery.PHONE_TYPE);
+            final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
+
+            // TODO cache
+            label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
+        }
+        view.setLabel(label);
+        final String text;
+        if (displayNumber) {
+            text = cursor.getString(PhoneQuery.PHONE_NUMBER);
+        } else {
+            // Display phone label. If that's null, display geocoded location for the number
+            final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
+            if (phoneLabel != null) {
+                text = phoneLabel;
+            } else {
+                final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER);
+                text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber);
+            }
+        }
+        view.setPhoneNumber(text, mCountryIso);
+
+        if (CompatUtils.isVideoCompatible()) {
+            // Determine if carrier presence indicates the number supports video calling.
+            int carrierPresence = cursor.getInt(PhoneQuery.CARRIER_PRESENCE);
+            boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0;
+
+            boolean isVideoIconShown = mIsVideoEnabled && (
+                    mIsPresenceEnabled && isPresent || !mIsPresenceEnabled);
+            view.setShowVideoCallIcon(isVideoIconShown, mListener, position);
+        }
+    }
+
+    protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
+        if (isSectionHeaderDisplayEnabled()) {
+            Placement placement = getItemPlacementInSection(position);
+            view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
+        } else {
+            view.setSectionHeader(null);
+        }
+    }
+
+    protected void bindName(final ContactListItemView view, Cursor cursor) {
+        view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME, getContactNameDisplayOrder());
+        // Note: we don't show phonetic names any more (see issue 5265330)
+    }
+
+    protected void unbindName(final ContactListItemView view) {
+        view.hideDisplayName();
+    }
+
+    @Override
+    protected void bindWorkProfileIcon(final ContactListItemView view, int partition) {
+        final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
+        final long directoryId = directory.getDirectoryId();
+        final long userType = ContactsUtils.determineUserType(directoryId, null);
+        // Work directory must not be a extended directory. An extended directory is custom
+        // directory in the app, but not a directory provided by framework. So it can't be
+        // USER_TYPE_WORK.
+        view.setWorkProfileIconEnabled(
+                !isExtendedDirectory(directoryId) && userType == ContactsUtils.USER_TYPE_WORK);
+    }
+
+    protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
+        if (!isPhotoSupported(partitionIndex)) {
+            view.removePhotoView();
+            return;
+        }
+
+        long photoId = 0;
+        if (!cursor.isNull(PhoneQuery.PHOTO_ID)) {
+            photoId = cursor.getLong(PhoneQuery.PHOTO_ID);
+        }
+
+        if (photoId != 0) {
+            getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false,
+                    getCircularPhotos(), null);
+        } else {
+            final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI);
+            final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+
+            DefaultImageRequest request = null;
+            if (photoUri == null) {
+                final String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME);
+                final String lookupKey = cursor.getString(PhoneQuery.LOOKUP_KEY);
+                request = new DefaultImageRequest(displayName, lookupKey, getCircularPhotos());
+            }
+            getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false,
+                    getCircularPhotos(), request);
+        }
+    }
+
+    public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
+        mPhotoPosition = photoPosition;
+    }
+
+    public ContactListItemView.PhotoPosition getPhotoPosition() {
+        return mPhotoPosition;
+    }
+
+    public void setUseCallableUri(boolean useCallableUri) {
+        mUseCallableUri = useCallableUri;
+    }
+
+    public boolean usesCallableUri() {
+        return mUseCallableUri;
+    }
+
+    /**
+     * Override base implementation to inject extended directories between local & remote
+     * directories. This is done in the following steps:
+     * 1. Call base implementation to add directories from the cursor.
+     * 2. Iterate all base directories and establish the following information:
+     *   a. The highest directory id so that we can assign unused id's to the extended directories.
+     *   b. The index of the last non-remote directory. This is where we will insert extended
+     *      directories.
+     * 3. Iterate the extended directories and for each one, assign an ID and insert it in the
+     *    proper location.
+     */
+    @Override
+    public void changeDirectories(Cursor cursor) {
+        super.changeDirectories(cursor);
+        if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) {
+            return;
+        }
+        final int numExtendedDirectories = mExtendedDirectories.size();
+        if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) {
+            // already added all directories;
+            return;
+        }
+        //
+        mFirstExtendedDirectoryId = Long.MAX_VALUE;
+        if (numExtendedDirectories > 0) {
+            // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's
+            // "special" ID.
+            long maxId = Directory.LOCAL_INVISIBLE;
+            int insertIndex = 0;
+            for (int i = 0, n = getPartitionCount(); i < n; i++) {
+                final DirectoryPartition partition = (DirectoryPartition) getPartition(i);
+                final long id = partition.getDirectoryId();
+                if (id > maxId) {
+                    maxId = id;
+                }
+                if (!DirectoryCompat.isRemoteDirectoryId(id)) {
+                    // assuming remote directories come after local, we will end up with the index
+                    // where we should insert extended directories. This also works if there are no
+                    // remote directories at all.
+                    insertIndex = i + 1;
+                }
+            }
+            // Extended directories ID's cannot collide with base directories
+            mFirstExtendedDirectoryId = maxId + 1;
+            for (int i = 0; i < numExtendedDirectories; i++) {
+                final long id = mFirstExtendedDirectoryId + i;
+                final DirectoryPartition directory = mExtendedDirectories.get(i);
+                if (getPartitionByDirectoryId(id) == -1) {
+                    addPartition(insertIndex, directory);
+                    directory.setDirectoryId(id);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected Uri getContactUri(int partitionIndex, Cursor cursor,
+            int contactIdColumn, int lookUpKeyColumn) {
+        final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex);
+        final long directoryId = directory.getDirectoryId();
+        if (!isExtendedDirectory(directoryId)) {
+            return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn);
+        }
+        return Contacts.CONTENT_LOOKUP_URI.buildUpon()
+                .appendPath(Constants.LOOKUP_URI_ENCODED)
+                .appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel())
+                .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                        String.valueOf(directoryId))
+                .encodedFragment(cursor.getString(lookUpKeyColumn))
+                .build();
+    }
+
+    public Listener getListener() {
+        return mListener;
+    }
+
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+}
diff --git a/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java b/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java
new file mode 100644
index 0000000..3d542eb
--- /dev/null
+++ b/src/com/android/contacts/common/list/PhoneNumberPickerFragment.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
+import com.android.contacts.commonbind.analytics.AnalyticsUtil;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Fragment containing a phone number list for picking.
+ */
+public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter>
+        implements OnShortcutIntentCreatedListener, PhoneNumberListAdapter.Listener {
+    private static final String TAG = PhoneNumberPickerFragment.class.getSimpleName();
+
+    private static final String KEY_SHORTCUT_ACTION = "shortcutAction";
+
+    private OnPhoneNumberPickerActionListener mListener;
+    private String mShortcutAction;
+
+    private ContactListFilter mFilter;
+
+    private static final String KEY_FILTER = "filter";
+
+    /** true if the loader has started at least once. */
+    private boolean mLoaderStarted;
+
+    private boolean mUseCallableUri;
+
+    private ContactListItemView.PhotoPosition mPhotoPosition =
+            ContactListItemView.getDefaultPhotoPosition(false /* normal/non opposite */);
+
+    /**
+     * Handles a click on the video call icon for a row in the list.
+     *
+     * @param position The position in the list where the click ocurred.
+     */
+    @Override
+    public void onVideoCallIconClicked(int position) {
+        callNumber(position, true /* isVideoCall */);
+    }
+
+    public PhoneNumberPickerFragment() {
+        setQuickContactEnabled(false);
+        setPhotoLoaderEnabled(true);
+        setSectionHeaderDisplayEnabled(true);
+        setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE);
+
+        // Show nothing instead of letting caller Activity show something.
+        setHasOptionsMenu(true);
+    }
+
+    public void setDirectorySearchEnabled(boolean flag) {
+        setDirectorySearchMode(flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT
+                : DirectoryListLoader.SEARCH_MODE_NONE);
+    }
+
+    public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) {
+        this.mListener = listener;
+    }
+
+    public OnPhoneNumberPickerActionListener getOnPhoneNumberPickerListener() {
+        return mListener;
+    }
+
+    @Override
+    protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+        super.onCreateView(inflater, container);
+
+        setVisibleScrollbarEnabled(getVisibleScrollbarEnabled());
+    }
+
+    protected boolean getVisibleScrollbarEnabled() {
+        return true;
+    }
+
+    @Override
+    public void restoreSavedState(Bundle savedState) {
+        super.restoreSavedState(savedState);
+
+        if (savedState == null) {
+            return;
+        }
+
+        mFilter = savedState.getParcelable(KEY_FILTER);
+        mShortcutAction = savedState.getString(KEY_SHORTCUT_ACTION);
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putParcelable(KEY_FILTER, mFilter);
+        outState.putString(KEY_SHORTCUT_ACTION, mShortcutAction);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        final int itemId = item.getItemId();
+        if (itemId == android.R.id.home) {  // See ActionBar#setDisplayHomeAsUpEnabled()
+            if (mListener != null) {
+                mListener.onHomeInActionBarSelected();
+            }
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    /**
+     * @param shortcutAction either {@link Intent#ACTION_CALL} or
+     *            {@link Intent#ACTION_SENDTO} or null.
+     */
+    public void setShortcutAction(String shortcutAction) {
+        this.mShortcutAction = shortcutAction;
+    }
+
+    @Override
+    protected void onItemClick(int position, long id) {
+        callNumber(position, false /* isVideoCall */);
+    }
+
+    /**
+     * Initiates a call to the number at the specified position.
+     *
+     * @param position The position.
+     * @param isVideoCall {@code true} if the call should be initiated as a video call,
+     *      {@code false} otherwise.
+     */
+    private void callNumber(int position, boolean isVideoCall) {
+        final Uri phoneUri = getPhoneUri(position);
+
+        if (phoneUri != null) {
+            pickPhoneNumber(phoneUri, isVideoCall);
+        } else {
+            final String number = getPhoneNumber(position);
+            if (!TextUtils.isEmpty(number)) {
+                cacheContactInfo(position);
+                mListener.onPickPhoneNumber(number, isVideoCall,
+                        getCallInitiationType(true /* isRemoteDirectory */));
+            } else {
+                Log.w(TAG, "Item at " + position + " was clicked before"
+                        + " adapter is ready. Ignoring");
+            }
+        }
+
+        // Get the lookup key and track any analytics
+        final String lookupKey = getLookupKey(position);
+        if (!TextUtils.isEmpty(lookupKey)) {
+            maybeTrackAnalytics(lookupKey);
+        }
+    }
+
+    protected void cacheContactInfo(int position) {
+        // Not implemented. Hook for child classes
+    }
+
+    protected String getPhoneNumber(int position) {
+        final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+        return adapter.getPhoneNumber(position);
+    }
+
+    protected Uri getPhoneUri(int position) {
+        final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+        return adapter.getDataUri(position);
+    }
+
+    protected String getLookupKey(int position) {
+        final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+        return adapter.getLookupKey(position);
+    }
+
+    @Override
+    protected void startLoading() {
+        mLoaderStarted = true;
+        super.startLoading();
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        super.onLoadFinished(loader, data);
+
+        // disable scroll bar if there is no data
+        setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0);
+    }
+
+    public void setUseCallableUri(boolean useCallableUri) {
+        mUseCallableUri = useCallableUri;
+    }
+
+    public boolean usesCallableUri() {
+        return mUseCallableUri;
+    }
+
+    @Override
+    protected ContactEntryListAdapter createListAdapter() {
+        PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity());
+        adapter.setDisplayPhotos(true);
+        adapter.setUseCallableUri(mUseCallableUri);
+        return adapter;
+    }
+
+    @Override
+    protected void configureAdapter() {
+        super.configureAdapter();
+
+        final ContactEntryListAdapter adapter = getAdapter();
+        if (adapter == null) {
+            return;
+        }
+
+        if (!isSearchMode() && mFilter != null) {
+            adapter.setFilter(mFilter);
+        }
+
+        setPhotoPosition(adapter);
+    }
+
+    protected void setPhotoPosition(ContactEntryListAdapter adapter) {
+        ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition);
+    }
+
+    @Override
+    protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+        return inflater.inflate(R.layout.contact_list_content, null);
+    }
+
+    public void pickPhoneNumber(Uri uri, boolean isVideoCall) {
+        if (mShortcutAction == null) {
+            mListener.onPickDataUri(uri, isVideoCall,
+                    getCallInitiationType(false /* isRemoteDirectory */));
+        } else {
+            startPhoneNumberShortcutIntent(uri, isVideoCall);
+        }
+    }
+
+    protected void startPhoneNumberShortcutIntent(Uri uri, boolean isVideoCall) {
+        ShortcutIntentBuilder builder = new ShortcutIntentBuilder(getActivity(), this);
+        builder.createPhoneNumberShortcutIntent(uri, mShortcutAction);
+    }
+
+    @Override
+    public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
+        mListener.onShortcutIntentCreated(shortcutIntent);
+    }
+
+    @Override
+    public void onPickerResult(Intent data) {
+        mListener.onPickDataUri(data.getData(), false /* isVideoCall */,
+                getCallInitiationType(false /* isRemoteDirectory */));
+    }
+
+    public void setFilter(ContactListFilter filter) {
+        if ((mFilter == null && filter == null) ||
+                (mFilter != null && mFilter.equals(filter))) {
+            return;
+        }
+
+        mFilter = filter;
+        if (mLoaderStarted) {
+            reloadData();
+        }
+    }
+
+    public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
+        mPhotoPosition = photoPosition;
+
+        final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+        if (adapter != null) {
+            adapter.setPhotoPosition(photoPosition);
+        }
+    }
+
+    /**
+     * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number
+     *         not in the local contacts database
+     */
+    protected int getCallInitiationType(boolean isRemoteDirectory) {
+        return OnPhoneNumberPickerActionListener.CALL_INITIATION_UNKNOWN;
+    }
+
+    /**
+     * Where a lookup key contains analytic event information, logs the associated analytics event.
+     *
+     * @param lookupKey The lookup key JSON object.
+     */
+    private void maybeTrackAnalytics(String lookupKey) {
+        try {
+            JSONObject json = new JSONObject(lookupKey);
+
+            String analyticsCategory = json.getString(
+                    PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY);
+            String analyticsAction = json.getString(
+                    PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION);
+            String analyticsValue = json.getString(
+                    PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE);
+
+            if (TextUtils.isEmpty(analyticsCategory) || TextUtils.isEmpty(analyticsAction) ||
+                    TextUtils.isEmpty(analyticsValue)) {
+                return;
+            }
+
+            // Assume that the analytic value being tracked could be a float value, but just cast
+            // to a long so that the analytic server can handle it.
+            long value;
+            try {
+                float floatValue = Float.parseFloat(analyticsValue);
+                value = (long) floatValue;
+            } catch (NumberFormatException nfe) {
+                return;
+            }
+
+            AnalyticsUtil.sendEvent(getActivity().getApplication(), analyticsCategory,
+                    analyticsAction, "" /* label */, value);
+        } catch (JSONException e) {
+            // Not an error; just a lookup key that doesn't have the right information.
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/PinnedHeaderListAdapter.java b/src/com/android/contacts/common/list/PinnedHeaderListAdapter.java
new file mode 100644
index 0000000..72f3f19
--- /dev/null
+++ b/src/com/android/contacts/common/list/PinnedHeaderListAdapter.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.common.widget.CompositeCursorAdapter;
+
+/**
+ * A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers.
+ */
+public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter
+        implements PinnedHeaderListView.PinnedHeaderAdapter {
+
+    public static final int PARTITION_HEADER_TYPE = 0;
+
+    private boolean mPinnedPartitionHeadersEnabled;
+    private boolean mHeaderVisibility[];
+
+    public PinnedHeaderListAdapter(Context context) {
+        super(context);
+    }
+
+    public PinnedHeaderListAdapter(Context context, int initialCapacity) {
+        super(context, initialCapacity);
+    }
+
+    public boolean getPinnedPartitionHeadersEnabled() {
+        return mPinnedPartitionHeadersEnabled;
+    }
+
+    public void setPinnedPartitionHeadersEnabled(boolean flag) {
+        this.mPinnedPartitionHeadersEnabled = flag;
+    }
+
+    @Override
+    public int getPinnedHeaderCount() {
+        if (mPinnedPartitionHeadersEnabled) {
+            return getPartitionCount();
+        } else {
+            return 0;
+        }
+    }
+
+    protected boolean isPinnedPartitionHeaderVisible(int partition) {
+        return getPinnedPartitionHeadersEnabled() && hasHeader(partition)
+                && !isPartitionEmpty(partition);
+    }
+
+    /**
+     * The default implementation creates the same type of view as a normal
+     * partition header.
+     */
+    @Override
+    public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) {
+        if (hasHeader(partition)) {
+            View view = null;
+            if (convertView != null) {
+                Integer headerType = (Integer)convertView.getTag();
+                if (headerType != null && headerType == PARTITION_HEADER_TYPE) {
+                    view = convertView;
+                }
+            }
+            if (view == null) {
+                view = newHeaderView(getContext(), partition, null, parent);
+                view.setTag(PARTITION_HEADER_TYPE);
+                view.setFocusable(false);
+                view.setEnabled(false);
+            }
+            bindHeaderView(view, partition, getCursor(partition));
+            view.setLayoutDirection(parent.getLayoutDirection());
+            return view;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void configurePinnedHeaders(PinnedHeaderListView listView) {
+        if (!getPinnedPartitionHeadersEnabled()) {
+            return;
+        }
+
+        int size = getPartitionCount();
+
+        // Cache visibility bits, because we will need them several times later on
+        if (mHeaderVisibility == null || mHeaderVisibility.length != size) {
+            mHeaderVisibility = new boolean[size];
+        }
+        for (int i = 0; i < size; i++) {
+            boolean visible = isPinnedPartitionHeaderVisible(i);
+            mHeaderVisibility[i] = visible;
+            if (!visible) {
+                listView.setHeaderInvisible(i, true);
+            }
+        }
+
+        int headerViewsCount = listView.getHeaderViewsCount();
+
+        // Starting at the top, find and pin headers for partitions preceding the visible one(s)
+        int maxTopHeader = -1;
+        int topHeaderHeight = 0;
+        for (int i = 0; i < size; i++) {
+            if (mHeaderVisibility[i]) {
+                int position = listView.getPositionAt(topHeaderHeight) - headerViewsCount;
+                int partition = getPartitionForPosition(position);
+                if (i > partition) {
+                    break;
+                }
+
+                listView.setHeaderPinnedAtTop(i, topHeaderHeight, false);
+                topHeaderHeight += listView.getPinnedHeaderHeight(i);
+                maxTopHeader = i;
+            }
+        }
+
+        // Starting at the bottom, find and pin headers for partitions following the visible one(s)
+        int maxBottomHeader = size;
+        int bottomHeaderHeight = 0;
+        int listHeight = listView.getHeight();
+        for (int i = size; --i > maxTopHeader;) {
+            if (mHeaderVisibility[i]) {
+                int position = listView.getPositionAt(listHeight - bottomHeaderHeight)
+                        - headerViewsCount;
+                if (position < 0) {
+                    break;
+                }
+
+                int partition = getPartitionForPosition(position - 1);
+                if (partition == -1 || i <= partition) {
+                    break;
+                }
+
+                int height = listView.getPinnedHeaderHeight(i);
+                bottomHeaderHeight += height;
+
+                listView.setHeaderPinnedAtBottom(i, listHeight - bottomHeaderHeight, false);
+                maxBottomHeader = i;
+            }
+        }
+
+        // Headers in between the top-pinned and bottom-pinned should be hidden
+        for (int i = maxTopHeader + 1; i < maxBottomHeader; i++) {
+            if (mHeaderVisibility[i]) {
+                listView.setHeaderInvisible(i, isPartitionEmpty(i));
+            }
+        }
+    }
+
+    @Override
+    public int getScrollPositionForHeader(int viewIndex) {
+        return getPositionForPartition(viewIndex);
+    }
+}
diff --git a/src/com/android/contacts/common/list/PinnedHeaderListView.java b/src/com/android/contacts/common/list/PinnedHeaderListView.java
new file mode 100644
index 0000000..45ce4b3
--- /dev/null
+++ b/src/com/android/contacts/common/list/PinnedHeaderListView.java
@@ -0,0 +1,584 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.ViewUtil;
+
+/**
+ * A ListView that maintains a header pinned at the top of the list. The
+ * pinned header can be pushed up and dissolved as needed.
+ */
+public class PinnedHeaderListView extends AutoScrollListView
+        implements OnScrollListener, OnItemSelectedListener {
+
+    /**
+     * Adapter interface.  The list adapter must implement this interface.
+     */
+    public interface PinnedHeaderAdapter {
+
+        /**
+         * Returns the overall number of pinned headers, visible or not.
+         */
+        int getPinnedHeaderCount();
+
+        /**
+         * Creates or updates the pinned header view.
+         */
+        View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent);
+
+        /**
+         * Configures the pinned headers to match the visible list items. The
+         * adapter should call {@link PinnedHeaderListView#setHeaderPinnedAtTop},
+         * {@link PinnedHeaderListView#setHeaderPinnedAtBottom},
+         * {@link PinnedHeaderListView#setFadingHeader} or
+         * {@link PinnedHeaderListView#setHeaderInvisible}, for each header that
+         * needs to change its position or visibility.
+         */
+        void configurePinnedHeaders(PinnedHeaderListView listView);
+
+        /**
+         * Returns the list position to scroll to if the pinned header is touched.
+         * Return -1 if the list does not need to be scrolled.
+         */
+        int getScrollPositionForHeader(int viewIndex);
+    }
+
+    private static final int MAX_ALPHA = 255;
+    private static final int TOP = 0;
+    private static final int BOTTOM = 1;
+    private static final int FADING = 2;
+
+    private static final int DEFAULT_ANIMATION_DURATION = 20;
+
+    private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 100;
+
+    private static final class PinnedHeader {
+        View view;
+        boolean visible;
+        int y;
+        int height;
+        int alpha;
+        int state;
+
+        boolean animating;
+        boolean targetVisible;
+        int sourceY;
+        int targetY;
+        long targetTime;
+    }
+
+    private PinnedHeaderAdapter mAdapter;
+    private int mSize;
+    private PinnedHeader[] mHeaders;
+    private RectF mBounds = new RectF();
+    private OnScrollListener mOnScrollListener;
+    private OnItemSelectedListener mOnItemSelectedListener;
+    private int mScrollState;
+
+    private boolean mScrollToSectionOnHeaderTouch = false;
+    private boolean mHeaderTouched = false;
+
+    private int mAnimationDuration = DEFAULT_ANIMATION_DURATION;
+    private boolean mAnimating;
+    private long mAnimationTargetTime;
+    private int mHeaderPaddingStart;
+    private int mHeaderWidth;
+
+    public PinnedHeaderListView(Context context) {
+        this(context, null, android.R.attr.listViewStyle);
+    }
+
+    public PinnedHeaderListView(Context context, AttributeSet attrs) {
+        this(context, attrs, android.R.attr.listViewStyle);
+    }
+
+    public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        super.setOnScrollListener(this);
+        super.setOnItemSelectedListener(this);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        mHeaderPaddingStart = getPaddingStart();
+        mHeaderWidth = r - l - mHeaderPaddingStart - getPaddingEnd();
+    }
+
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        mAdapter = (PinnedHeaderAdapter)adapter;
+        super.setAdapter(adapter);
+    }
+
+    @Override
+    public void setOnScrollListener(OnScrollListener onScrollListener) {
+        mOnScrollListener = onScrollListener;
+        super.setOnScrollListener(this);
+    }
+
+    @Override
+    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+        mOnItemSelectedListener = listener;
+        super.setOnItemSelectedListener(this);
+    }
+
+    public void setScrollToSectionOnHeaderTouch(boolean value) {
+        mScrollToSectionOnHeaderTouch = value;
+    }
+
+    @Override
+    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+            int totalItemCount) {
+        if (mAdapter != null) {
+            int count = mAdapter.getPinnedHeaderCount();
+            if (count != mSize) {
+                mSize = count;
+                if (mHeaders == null) {
+                    mHeaders = new PinnedHeader[mSize];
+                } else if (mHeaders.length < mSize) {
+                    PinnedHeader[] headers = mHeaders;
+                    mHeaders = new PinnedHeader[mSize];
+                    System.arraycopy(headers, 0, mHeaders, 0, headers.length);
+                }
+            }
+
+            for (int i = 0; i < mSize; i++) {
+                if (mHeaders[i] == null) {
+                    mHeaders[i] = new PinnedHeader();
+                }
+                mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this);
+            }
+
+            mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration;
+            mAdapter.configurePinnedHeaders(this);
+            invalidateIfAnimating();
+        }
+        if (mOnScrollListener != null) {
+            mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount);
+        }
+    }
+
+    @Override
+    protected float getTopFadingEdgeStrength() {
+        // Disable vertical fading at the top when the pinned header is present
+        return mSize > 0 ? 0 : super.getTopFadingEdgeStrength();
+    }
+
+    @Override
+    public void onScrollStateChanged(AbsListView view, int scrollState) {
+        mScrollState = scrollState;
+        if (mOnScrollListener != null) {
+            mOnScrollListener.onScrollStateChanged(this, scrollState);
+        }
+    }
+
+    /**
+     * Ensures that the selected item is positioned below the top-pinned headers
+     * and above the bottom-pinned ones.
+     */
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        int height = getHeight();
+
+        int windowTop = 0;
+        int windowBottom = height;
+
+        for (int i = 0; i < mSize; i++) {
+            PinnedHeader header = mHeaders[i];
+            if (header.visible) {
+                if (header.state == TOP) {
+                    windowTop = header.y + header.height;
+                } else if (header.state == BOTTOM) {
+                    windowBottom = header.y;
+                    break;
+                }
+            }
+        }
+
+        View selectedView = getSelectedView();
+        if (selectedView != null) {
+            if (selectedView.getTop() < windowTop) {
+                setSelectionFromTop(position, windowTop);
+            } else if (selectedView.getBottom() > windowBottom) {
+                setSelectionFromTop(position, windowBottom - selectedView.getHeight());
+            }
+        }
+
+        if (mOnItemSelectedListener != null) {
+            mOnItemSelectedListener.onItemSelected(parent, view, position, id);
+        }
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {
+        if (mOnItemSelectedListener != null) {
+            mOnItemSelectedListener.onNothingSelected(parent);
+        }
+    }
+
+    public int getPinnedHeaderHeight(int viewIndex) {
+        ensurePinnedHeaderLayout(viewIndex);
+        return mHeaders[viewIndex].view.getHeight();
+    }
+
+    /**
+     * Set header to be pinned at the top.
+     *
+     * @param viewIndex index of the header view
+     * @param y is position of the header in pixels.
+     * @param animate true if the transition to the new coordinate should be animated
+     */
+    public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) {
+        ensurePinnedHeaderLayout(viewIndex);
+        PinnedHeader header = mHeaders[viewIndex];
+        header.visible = true;
+        header.y = y;
+        header.state = TOP;
+
+        // TODO perhaps we should animate at the top as well
+        header.animating = false;
+    }
+
+    /**
+     * Set header to be pinned at the bottom.
+     *
+     * @param viewIndex index of the header view
+     * @param y is position of the header in pixels.
+     * @param animate true if the transition to the new coordinate should be animated
+     */
+    public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) {
+        ensurePinnedHeaderLayout(viewIndex);
+        PinnedHeader header = mHeaders[viewIndex];
+        header.state = BOTTOM;
+        if (header.animating) {
+            header.targetTime = mAnimationTargetTime;
+            header.sourceY = header.y;
+            header.targetY = y;
+        } else if (animate && (header.y != y || !header.visible)) {
+            if (header.visible) {
+                header.sourceY = header.y;
+            } else {
+                header.visible = true;
+                header.sourceY = y + header.height;
+            }
+            header.animating = true;
+            header.targetVisible = true;
+            header.targetTime = mAnimationTargetTime;
+            header.targetY = y;
+        } else {
+            header.visible = true;
+            header.y = y;
+        }
+    }
+
+    /**
+     * Set header to be pinned at the top of the first visible item.
+     *
+     * @param viewIndex index of the header view
+     * @param position is position of the header in pixels.
+     */
+    public void setFadingHeader(int viewIndex, int position, boolean fade) {
+        ensurePinnedHeaderLayout(viewIndex);
+
+        View child = getChildAt(position - getFirstVisiblePosition());
+        if (child == null) return;
+
+        PinnedHeader header = mHeaders[viewIndex];
+        // Hide header when it's a star.
+        // TODO: try showing the view even when it's a star;
+        // if we have to hide the star view, then try hiding it in some higher layer.
+        header.visible = !TextUtils.equals(
+                ((TextView) header.view).getText(), getContext().getString(R.string.star_sign));
+        header.state = FADING;
+        header.alpha = MAX_ALPHA;
+        header.animating = false;
+
+        int top = getTotalTopPinnedHeaderHeight();
+        header.y = top;
+        if (fade) {
+            int bottom = child.getBottom() - top;
+            int headerHeight = header.height;
+            if (bottom < headerHeight) {
+                int portion = bottom - headerHeight;
+                header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight;
+                header.y = top + portion;
+            }
+        }
+    }
+
+    /**
+     * Makes header invisible.
+     *
+     * @param viewIndex index of the header view
+     * @param animate true if the transition to the new coordinate should be animated
+     */
+    public void setHeaderInvisible(int viewIndex, boolean animate) {
+        PinnedHeader header = mHeaders[viewIndex];
+        if (header.visible && (animate || header.animating) && header.state == BOTTOM) {
+            header.sourceY = header.y;
+            if (!header.animating) {
+                header.visible = true;
+                header.targetY = getBottom() + header.height;
+            }
+            header.animating = true;
+            header.targetTime = mAnimationTargetTime;
+            header.targetVisible = false;
+        } else {
+            header.visible = false;
+        }
+    }
+
+    private void ensurePinnedHeaderLayout(int viewIndex) {
+        View view = mHeaders[viewIndex].view;
+        if (view.isLayoutRequested()) {
+            ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+            int widthSpec;
+            int heightSpec;
+
+            if (layoutParams != null && layoutParams.width > 0) {
+                widthSpec = View.MeasureSpec
+                        .makeMeasureSpec(layoutParams.width, View.MeasureSpec.EXACTLY);
+            } else {
+                widthSpec = View.MeasureSpec
+                        .makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY);
+            }
+
+            if (layoutParams != null && layoutParams.height > 0) {
+                heightSpec = View.MeasureSpec
+                        .makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY);
+            } else {
+                heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+            }
+            view.measure(widthSpec, heightSpec);
+            int height = view.getMeasuredHeight();
+            mHeaders[viewIndex].height = height;
+            view.layout(0, 0, view.getMeasuredWidth(), height);
+        }
+    }
+
+    /**
+     * Returns the sum of heights of headers pinned to the top.
+     */
+    public int getTotalTopPinnedHeaderHeight() {
+        for (int i = mSize; --i >= 0;) {
+            PinnedHeader header = mHeaders[i];
+            if (header.visible && header.state == TOP) {
+                return header.y + header.height;
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Returns the list item position at the specified y coordinate.
+     */
+    public int getPositionAt(int y) {
+        do {
+            int position = pointToPosition(getPaddingLeft() + 1, y);
+            if (position != -1) {
+                return position;
+            }
+            // If position == -1, we must have hit a separator. Let's examine
+            // a nearby pixel
+            y--;
+        } while (y > 0);
+        return 0;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        mHeaderTouched = false;
+        if (super.onInterceptTouchEvent(ev)) {
+            return true;
+        }
+
+        if (mScrollState == SCROLL_STATE_IDLE) {
+            final int y = (int)ev.getY();
+            final int x = (int)ev.getX();
+            for (int i = mSize; --i >= 0;) {
+                PinnedHeader header = mHeaders[i];
+                final int padding = ViewUtil.isViewLayoutRtl(this) ?
+                        getWidth() - mHeaderPaddingStart - header.view.getWidth() :
+                        mHeaderPaddingStart;
+                if (header.visible && header.y <= y && header.y + header.height > y &&
+                        x >= padding && padding + header.view.getWidth() >= x) {
+                    mHeaderTouched = true;
+                    if (mScrollToSectionOnHeaderTouch &&
+                            ev.getAction() == MotionEvent.ACTION_DOWN) {
+                        return smoothScrollToPartition(i);
+                    } else {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mHeaderTouched) {
+            if (ev.getAction() == MotionEvent.ACTION_UP) {
+                mHeaderTouched = false;
+            }
+            return true;
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    private boolean smoothScrollToPartition(int partition) {
+        if (mAdapter == null) {
+            return false;
+        }
+        final int position = mAdapter.getScrollPositionForHeader(partition);
+        if (position == -1) {
+            return false;
+        }
+
+        int offset = 0;
+        for (int i = 0; i < partition; i++) {
+            PinnedHeader header = mHeaders[i];
+            if (header.visible) {
+                offset += header.height;
+            }
+        }
+        smoothScrollToPositionFromTop(position + getHeaderViewsCount(), offset,
+                DEFAULT_SMOOTH_SCROLL_DURATION);
+        return true;
+    }
+
+    private void invalidateIfAnimating() {
+        mAnimating = false;
+        for (int i = 0; i < mSize; i++) {
+            if (mHeaders[i].animating) {
+                mAnimating = true;
+                invalidate();
+                return;
+            }
+        }
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        long currentTime = mAnimating ? System.currentTimeMillis() : 0;
+
+        int top = 0;
+        int right = 0;
+        int bottom = getBottom();
+        boolean hasVisibleHeaders = false;
+        for (int i = 0; i < mSize; i++) {
+            PinnedHeader header = mHeaders[i];
+            if (header.visible) {
+                hasVisibleHeaders = true;
+                if (header.state == BOTTOM && header.y < bottom) {
+                    bottom = header.y;
+                } else if (header.state == TOP || header.state == FADING) {
+                    int newTop = header.y + header.height;
+                    if (newTop > top) {
+                        top = newTop;
+                    }
+                }
+            }
+        }
+
+        if (hasVisibleHeaders) {
+            canvas.save();
+        }
+
+        super.dispatchDraw(canvas);
+
+        if (hasVisibleHeaders) {
+            canvas.restore();
+
+            // If the first item is visible and if it has a positive top that is greater than the
+            // first header's assigned y-value, use that for the first header's y value. This way,
+            // the header inherits any padding applied to the list view.
+            if (mSize > 0 && getFirstVisiblePosition() == 0) {
+                View firstChild = getChildAt(0);
+                PinnedHeader firstHeader = mHeaders[0];
+
+                if (firstHeader != null) {
+                    int firstHeaderTop = firstChild != null ? firstChild.getTop() : 0;
+                    firstHeader.y = Math.max(firstHeader.y, firstHeaderTop);
+                }
+            }
+
+            // First draw top headers, then the bottom ones to handle the Z axis correctly
+            for (int i = mSize; --i >= 0;) {
+                PinnedHeader header = mHeaders[i];
+                if (header.visible && (header.state == TOP || header.state == FADING)) {
+                    drawHeader(canvas, header, currentTime);
+                }
+            }
+
+            for (int i = 0; i < mSize; i++) {
+                PinnedHeader header = mHeaders[i];
+                if (header.visible && header.state == BOTTOM) {
+                    drawHeader(canvas, header, currentTime);
+                }
+            }
+        }
+
+        invalidateIfAnimating();
+    }
+
+    private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) {
+        if (header.animating) {
+            int timeLeft = (int)(header.targetTime - currentTime);
+            if (timeLeft <= 0) {
+                header.y = header.targetY;
+                header.visible = header.targetVisible;
+                header.animating = false;
+            } else {
+                header.y = header.targetY + (header.sourceY - header.targetY) * timeLeft
+                        / mAnimationDuration;
+            }
+        }
+        if (header.visible) {
+            View view = header.view;
+            int saveCount = canvas.save();
+            int translateX = ViewUtil.isViewLayoutRtl(this) ?
+                    getWidth() - mHeaderPaddingStart - view.getWidth() :
+                    mHeaderPaddingStart;
+            canvas.translate(translateX, header.y);
+            if (header.state == FADING) {
+                mBounds.set(0, 0, view.getWidth(), view.getHeight());
+                canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG);
+            }
+            view.draw(canvas);
+            canvas.restoreToCount(saveCount);
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/ProviderStatusWatcher.java b/src/com/android/contacts/common/list/ProviderStatusWatcher.java
new file mode 100644
index 0000000..3e8e2eb
--- /dev/null
+++ b/src/com/android/contacts/common/list/ProviderStatusWatcher.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.provider.ContactsContract.ProviderStatus;
+import android.util.Log;
+
+import com.android.contacts.common.compat.ProviderStatusCompat;
+
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * A singleton that keeps track of the last known provider status.
+ *
+ * All methods must be called on the UI thread unless noted otherwise.
+ *
+ * All members must be set on the UI thread unless noted otherwise.
+ */
+public class ProviderStatusWatcher extends ContentObserver {
+    private static final String TAG = "ProviderStatusWatcher";
+    private static final boolean DEBUG = false;
+
+    /**
+     * Callback interface invoked when the provider status changes.
+     */
+    public interface ProviderStatusListener {
+        public void onProviderStatusChange();
+    }
+
+    private static final String[] PROJECTION = new String[] {
+        ProviderStatus.STATUS
+    };
+
+    /**
+     * We'll wait for this amount of time on the UI thread if the load hasn't finished.
+     */
+    private static final int LOAD_WAIT_TIMEOUT_MS = 1000;
+
+    private static ProviderStatusWatcher sInstance;
+
+    private final Context mContext;
+    private final Handler mHandler = new Handler();
+
+    private final Object mSignal = new Object();
+
+    private int mStartRequestedCount;
+
+    private LoaderTask mLoaderTask;
+
+    /** Last known provider status.  This can be changed on a worker thread.
+     *  See {@link ProviderStatus#STATUS} */
+    private Integer mProviderStatus;
+
+    private final ArrayList<ProviderStatusListener> mListeners = Lists.newArrayList();
+
+    private final Runnable mStartLoadingRunnable = new Runnable() {
+        @Override
+        public void run() {
+            startLoading();
+        }
+    };
+
+    /**
+     * Returns the singleton instance.
+     */
+    public synchronized static ProviderStatusWatcher getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new ProviderStatusWatcher(context);
+        }
+        return sInstance;
+    }
+
+    private ProviderStatusWatcher(Context context) {
+        super(null);
+        mContext = context;
+    }
+
+    /** Add a listener. */
+    public void addListener(ProviderStatusListener listener) {
+        mListeners.add(listener);
+    }
+
+    /** Remove a listener */
+    public void removeListener(ProviderStatusListener listener) {
+        mListeners.remove(listener);
+    }
+
+    private void notifyListeners() {
+        if (DEBUG) {
+            Log.d(TAG, "notifyListeners: " + mListeners.size());
+        }
+        if (isStarted()) {
+            for (ProviderStatusListener listener : mListeners) {
+                listener.onProviderStatusChange();
+            }
+        }
+    }
+
+    private boolean isStarted() {
+        return mStartRequestedCount > 0;
+    }
+
+    /**
+     * Starts watching the provider status.  {@link #start()} and {@link #stop()} calls can be
+     * nested.
+     */
+    public void start() {
+        if (++mStartRequestedCount == 1) {
+            mContext.getContentResolver()
+                .registerContentObserver(ProviderStatus.CONTENT_URI, false, this);
+            startLoading();
+
+            if (DEBUG) {
+                Log.d(TAG, "Start observing");
+            }
+        }
+    }
+
+    /**
+     * Stops watching the provider status.
+     */
+    public void stop() {
+        if (!isStarted()) {
+            Log.e(TAG, "Already stopped");
+            return;
+        }
+        if (--mStartRequestedCount == 0) {
+
+            mHandler.removeCallbacks(mStartLoadingRunnable);
+
+            mContext.getContentResolver().unregisterContentObserver(this);
+            if (DEBUG) {
+                Log.d(TAG, "Stop observing");
+            }
+        }
+    }
+
+    /**
+     * @return last known provider status.
+     *
+     * If this method is called when we haven't started the status query or the query is still in
+     * progress, it will start a query in a worker thread if necessary, and *wait for the result*.
+     *
+     * This means this method is essentially a blocking {@link ProviderStatus#CONTENT_URI} query.
+     * This URI is not backed by the file system, so is usually fast enough to perform on the main
+     * thread, but in extreme cases (when the system takes a while to bring up the contacts
+     * provider?) this may still cause ANRs.
+     *
+     * In order to avoid that, if we can't load the status within {@link #LOAD_WAIT_TIMEOUT_MS},
+     * we'll give up and just returns {@link ProviderStatusCompat#STATUS_BUSY} in order to unblock
+     * the UI thread.  The actual result will be delivered later via {@link ProviderStatusListener}.
+     * (If {@link ProviderStatusCompat#STATUS_BUSY} is returned, the app (should) shows an according
+     * message, like "contacts are being updated".)
+     */
+    public int getProviderStatus() {
+        waitForLoaded();
+
+        if (mProviderStatus == null) {
+            return ProviderStatusCompat.STATUS_BUSY;
+        }
+
+        return mProviderStatus;
+    }
+
+    private void waitForLoaded() {
+        if (mProviderStatus == null) {
+            if (mLoaderTask == null) {
+                // For some reason the loader couldn't load the status.  Let's start it again.
+                startLoading();
+            }
+            synchronized (mSignal) {
+                try {
+                    mSignal.wait(LOAD_WAIT_TIMEOUT_MS);
+                } catch (InterruptedException ignore) {
+                }
+            }
+        }
+    }
+
+    private void startLoading() {
+        if (mLoaderTask != null) {
+            return; // Task already running.
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "Start loading");
+        }
+
+        mLoaderTask = new LoaderTask();
+        mLoaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    private class LoaderTask extends AsyncTask<Void, Void, Boolean> {
+        @Override
+        protected Boolean doInBackground(Void... params) {
+            try {
+                Cursor cursor = mContext.getContentResolver().query(ProviderStatus.CONTENT_URI,
+                        PROJECTION, null, null, null);
+                if (cursor != null) {
+                    try {
+                        if (cursor.moveToFirst()) {
+                            // Note here we can't just say "Status", as AsyncTask has the "Status"
+                            // enum too.
+                            mProviderStatus = cursor.getInt(0);
+                            return true;
+                        }
+                    } finally {
+                        cursor.close();
+                    }
+                }
+                return false;
+            } finally {
+                synchronized (mSignal) {
+                    mSignal.notifyAll();
+                }
+            }
+        }
+
+        @Override
+        protected void onCancelled(Boolean result) {
+            cleanUp();
+        }
+
+        @Override
+        protected void onPostExecute(Boolean loaded) {
+            cleanUp();
+            if (loaded != null && loaded) {
+                notifyListeners();
+            }
+        }
+
+        private void cleanUp() {
+            mLoaderTask = null;
+        }
+    }
+
+    /**
+     * Called when provider status may has changed.
+     *
+     * This method will be called on a worker thread by the framework.
+     */
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        if (!ProviderStatus.CONTENT_URI.equals(uri)) return;
+
+        // Provider status change is rare, so okay to log.
+        Log.i(TAG, "Provider status changed.");
+
+        mHandler.removeCallbacks(mStartLoadingRunnable); // Remove one in the queue, if any.
+        mHandler.post(mStartLoadingRunnable);
+    }
+}
diff --git a/src/com/android/contacts/common/list/ShortcutIntentBuilder.java b/src/com/android/contacts/common/list/ShortcutIntentBuilder.java
new file mode 100644
index 0000000..f30a176
--- /dev/null
+++ b/src/com/android/contacts/common/list/ShortcutIntentBuilder.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import android.telecom.PhoneAccount;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.R;
+
+/**
+ * Constructs shortcut intents.
+ */
+public class ShortcutIntentBuilder {
+
+    private static final String[] CONTACT_COLUMNS = {
+        Contacts.DISPLAY_NAME,
+        Contacts.PHOTO_ID,
+        Contacts.LOOKUP_KEY
+    };
+
+    private static final int CONTACT_DISPLAY_NAME_COLUMN_INDEX = 0;
+    private static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 1;
+    private static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 2;
+
+    private static final String[] PHONE_COLUMNS = {
+        Phone.DISPLAY_NAME,
+        Phone.PHOTO_ID,
+        Phone.NUMBER,
+        Phone.TYPE,
+        Phone.LABEL,
+        Phone.LOOKUP_KEY
+    };
+
+    private static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 0;
+    private static final int PHONE_PHOTO_ID_COLUMN_INDEX = 1;
+    private static final int PHONE_NUMBER_COLUMN_INDEX = 2;
+    private static final int PHONE_TYPE_COLUMN_INDEX = 3;
+    private static final int PHONE_LABEL_COLUMN_INDEX = 4;
+    private static final int PHONE_LOOKUP_KEY_COLUMN_INDEX = 5;
+
+    private static final String[] PHOTO_COLUMNS = {
+        Photo.PHOTO,
+    };
+
+    private static final int PHOTO_PHOTO_COLUMN_INDEX = 0;
+
+    private static final String PHOTO_SELECTION = Photo._ID + "=?";
+
+    private final OnShortcutIntentCreatedListener mListener;
+    private final Context mContext;
+    private int mIconSize;
+    private final int mIconDensity;
+    private final int mOverlayTextBackgroundColor;
+    private final Resources mResources;
+
+    /**
+     * This is a hidden API of the launcher in JellyBean that allows us to disable the animation
+     * that it would usually do, because it interferes with our own animation for QuickContact.
+     * This is needed since some versions of the launcher override the intent flags and therefore
+     * ignore Intent.FLAG_ACTIVITY_NO_ANIMATION.
+     */
+    public static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION =
+            "com.android.launcher.intent.extra.shortcut.INGORE_LAUNCH_ANIMATION";
+
+    /**
+     * Listener interface.
+     */
+    public interface OnShortcutIntentCreatedListener {
+
+        /**
+         * Callback for shortcut intent creation.
+         *
+         * @param uri the original URI for which the shortcut intent has been
+         *            created.
+         * @param shortcutIntent resulting shortcut intent.
+         */
+        void onShortcutIntentCreated(Uri uri, Intent shortcutIntent);
+    }
+
+    public ShortcutIntentBuilder(Context context, OnShortcutIntentCreatedListener listener) {
+        mContext = context;
+        mListener = listener;
+
+        mResources = context.getResources();
+        final ActivityManager am = (ActivityManager) context
+                .getSystemService(Context.ACTIVITY_SERVICE);
+        mIconSize = mResources.getDimensionPixelSize(R.dimen.shortcut_icon_size);
+        if (mIconSize == 0) {
+            mIconSize = am.getLauncherLargeIconSize();
+        }
+        mIconDensity = am.getLauncherLargeIconDensity();
+        mOverlayTextBackgroundColor = mResources.getColor(R.color.shortcut_overlay_text_background);
+    }
+
+    public void createContactShortcutIntent(Uri contactUri) {
+        new ContactLoadingAsyncTask(contactUri).execute();
+    }
+
+    public void createPhoneNumberShortcutIntent(Uri dataUri, String shortcutAction) {
+        new PhoneNumberLoadingAsyncTask(dataUri, shortcutAction).execute();
+    }
+
+    /**
+     * An asynchronous task that loads name, photo and other data from the database.
+     */
+    private abstract class LoadingAsyncTask extends AsyncTask<Void, Void, Void> {
+        protected Uri mUri;
+        protected String mContentType;
+        protected String mDisplayName;
+        protected String mLookupKey;
+        protected byte[] mBitmapData;
+        protected long mPhotoId;
+
+        public LoadingAsyncTask(Uri uri) {
+            mUri = uri;
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            mContentType = mContext.getContentResolver().getType(mUri);
+            loadData();
+            loadPhoto();
+            return null;
+        }
+
+        protected abstract void loadData();
+
+        private void loadPhoto() {
+            if (mPhotoId == 0) {
+                return;
+            }
+
+            ContentResolver resolver = mContext.getContentResolver();
+            Cursor cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLUMNS, PHOTO_SELECTION,
+                    new String[] { String.valueOf(mPhotoId) }, null);
+            if (cursor != null) {
+                try {
+                    if (cursor.moveToFirst()) {
+                        mBitmapData = cursor.getBlob(PHOTO_PHOTO_COLUMN_INDEX);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+    }
+
+    private final class ContactLoadingAsyncTask extends LoadingAsyncTask {
+        public ContactLoadingAsyncTask(Uri uri) {
+            super(uri);
+        }
+
+        @Override
+        protected void loadData() {
+            ContentResolver resolver = mContext.getContentResolver();
+            Cursor cursor = resolver.query(mUri, CONTACT_COLUMNS, null, null, null);
+            if (cursor != null) {
+                try {
+                    if (cursor.moveToFirst()) {
+                        mDisplayName = cursor.getString(CONTACT_DISPLAY_NAME_COLUMN_INDEX);
+                        mPhotoId = cursor.getLong(CONTACT_PHOTO_ID_COLUMN_INDEX);
+                        mLookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+        @Override
+        protected void onPostExecute(Void result) {
+            createContactShortcutIntent(mUri, mContentType, mDisplayName, mLookupKey, mBitmapData);
+        }
+    }
+
+    private final class PhoneNumberLoadingAsyncTask extends LoadingAsyncTask {
+        private final String mShortcutAction;
+        private String mPhoneNumber;
+        private int mPhoneType;
+        private String mPhoneLabel;
+
+        public PhoneNumberLoadingAsyncTask(Uri uri, String shortcutAction) {
+            super(uri);
+            mShortcutAction = shortcutAction;
+        }
+
+        @Override
+        protected void loadData() {
+            ContentResolver resolver = mContext.getContentResolver();
+            Cursor cursor = resolver.query(mUri, PHONE_COLUMNS, null, null, null);
+            if (cursor != null) {
+                try {
+                    if (cursor.moveToFirst()) {
+                        mDisplayName = cursor.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX);
+                        mPhotoId = cursor.getLong(PHONE_PHOTO_ID_COLUMN_INDEX);
+                        mPhoneNumber = cursor.getString(PHONE_NUMBER_COLUMN_INDEX);
+                        mPhoneType = cursor.getInt(PHONE_TYPE_COLUMN_INDEX);
+                        mPhoneLabel = cursor.getString(PHONE_LABEL_COLUMN_INDEX);
+                        mLookupKey = cursor.getString(PHONE_LOOKUP_KEY_COLUMN_INDEX);
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            createPhoneNumberShortcutIntent(mUri, mDisplayName, mLookupKey, mBitmapData,
+                    mPhoneNumber, mPhoneType, mPhoneLabel, mShortcutAction);
+        }
+    }
+
+    private Drawable getPhotoDrawable(byte[] bitmapData, String displayName, String lookupKey) {
+        if (bitmapData != null) {
+            Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length, null);
+            return new BitmapDrawable(mContext.getResources(), bitmap);
+        } else {
+            return ContactPhotoManager.getDefaultAvatarDrawableForContact(mContext.getResources(),
+                    false, new DefaultImageRequest(displayName, lookupKey, false));
+        }
+    }
+
+    private void createContactShortcutIntent(Uri contactUri, String contentType, String displayName,
+            String lookupKey, byte[] bitmapData) {
+        Drawable drawable = getPhotoDrawable(bitmapData, displayName, lookupKey);
+
+        // Use an implicit intent without a package name set. It is reasonable for a disambiguation
+        // dialog to appear when opening QuickContacts from the launcher. Plus, this will be more
+        // resistant to future package name changes done to Contacts.
+        Intent shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
+
+        // When starting from the launcher, start in a new, cleared task.
+        // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
+        // clear the whole thing preemptively here since QuickContactActivity will
+        // finish itself when launching other detail activities. We need to use
+        // Intent.FLAG_ACTIVITY_NO_ANIMATION since not all versions of launcher will respect
+        // the INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION intent extra.
+        shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
+                | Intent.FLAG_ACTIVITY_NO_ANIMATION);
+
+        // Tell the launcher to not do its animation, because we are doing our own
+        shortcutIntent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
+
+        shortcutIntent.setDataAndType(contactUri, contentType);
+        shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
+                (String[]) null);
+
+        final Bitmap icon = generateQuickContactIcon(drawable);
+
+        Intent intent = new Intent();
+        intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
+        intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+        if (TextUtils.isEmpty(displayName)) {
+            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, mContext.getResources().getString(
+                    R.string.missing_name));
+        } else {
+            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, displayName);
+        }
+
+        mListener.onShortcutIntentCreated(contactUri, intent);
+    }
+
+    private void createPhoneNumberShortcutIntent(Uri uri, String displayName, String lookupKey,
+            byte[] bitmapData, String phoneNumber, int phoneType, String phoneLabel,
+            String shortcutAction) {
+        Drawable drawable = getPhotoDrawable(bitmapData, displayName, lookupKey);
+
+        Bitmap bitmap;
+        Uri phoneUri;
+        if (Intent.ACTION_CALL.equals(shortcutAction)) {
+            // Make the URI a direct tel: URI so that it will always continue to work
+            phoneUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
+            bitmap = generatePhoneNumberIcon(drawable, phoneType, phoneLabel,
+                    R.drawable.ic_call);
+        } else {
+            phoneUri = Uri.fromParts(ContactsUtils.SCHEME_SMSTO, phoneNumber, null);
+            bitmap = generatePhoneNumberIcon(drawable, phoneType, phoneLabel,
+                    R.drawable.ic_message_24dp_mirrored);
+        }
+
+        Intent shortcutIntent = new Intent(shortcutAction, phoneUri);
+        shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+        Intent intent = new Intent();
+        intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bitmap);
+        intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+
+        if (TextUtils.isEmpty(displayName)) {
+            displayName = mContext.getResources().getString(R.string.missing_name);
+        }
+        if (TextUtils.equals(shortcutAction, Intent.ACTION_CALL)) {
+            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME,
+                    mContext.getResources().getString(R.string.call_by_shortcut, displayName));
+        } else if (TextUtils.equals(shortcutAction, Intent.ACTION_SENDTO)) {
+            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME,
+                    mContext.getResources().getString(R.string.sms_by_shortcut, displayName));
+        }
+
+        mListener.onShortcutIntentCreated(uri, intent);
+    }
+
+    private Bitmap generateQuickContactIcon(Drawable photo) {
+
+        // Setup the drawing classes
+        Bitmap bitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+
+        // Copy in the photo
+        Rect dst = new Rect(0,0, mIconSize, mIconSize);
+        photo.setBounds(dst);
+        photo.draw(canvas);
+
+        // Draw the icon with a rounded border
+        RoundedBitmapDrawable roundedDrawable =
+                RoundedBitmapDrawableFactory.create(mResources, bitmap);
+        roundedDrawable.setAntiAlias(true);
+        roundedDrawable.setCornerRadius(mIconSize / 2);
+        Bitmap roundedBitmap = Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
+        canvas.setBitmap(roundedBitmap);
+        roundedDrawable.setBounds(dst);
+        roundedDrawable.draw(canvas);
+        canvas.setBitmap(null);
+
+        return roundedBitmap;
+    }
+
+    /**
+     * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
+     * number, and if there is a photo also adds the call action icon.
+     */
+    private Bitmap generatePhoneNumberIcon(Drawable photo, int phoneType, String phoneLabel,
+            int actionResId) {
+        final Resources r = mContext.getResources();
+        final float density = r.getDisplayMetrics().density;
+
+        Bitmap phoneIcon = ((BitmapDrawable) r.getDrawableForDensity(actionResId, mIconDensity))
+                .getBitmap();
+
+        Bitmap icon = generateQuickContactIcon(photo);
+        Canvas canvas = new Canvas(icon);
+
+        // Copy in the photo
+        Paint photoPaint = new Paint();
+        photoPaint.setDither(true);
+        photoPaint.setFilterBitmap(true);
+        Rect dst = new Rect(0, 0, mIconSize, mIconSize);
+
+        // Create an overlay for the phone number type
+        CharSequence overlay = Phone.getTypeLabel(r, phoneType, phoneLabel);
+
+        if (overlay != null) {
+            TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
+            textPaint.setTextSize(r.getDimension(R.dimen.shortcut_overlay_text_size));
+            textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
+            textPaint.setShadowLayer(4f, 0, 2f, r.getColor(R.color.textColorIconOverlayShadow));
+
+            final FontMetricsInt fmi = textPaint.getFontMetricsInt();
+
+            // First fill in a darker background around the text to be drawn
+            final Paint workPaint = new Paint();
+            workPaint.setColor(mOverlayTextBackgroundColor);
+            workPaint.setStyle(Paint.Style.FILL);
+            final int textPadding = r
+                    .getDimensionPixelOffset(R.dimen.shortcut_overlay_text_background_padding);
+            final int textBandHeight = (fmi.descent - fmi.ascent) + textPadding * 2;
+            dst.set(0, mIconSize - textBandHeight, mIconSize, mIconSize);
+            canvas.drawRect(dst, workPaint);
+
+            overlay = TextUtils.ellipsize(overlay, textPaint, mIconSize, TruncateAt.END);
+            final float textWidth = textPaint.measureText(overlay, 0, overlay.length());
+            canvas.drawText(overlay, 0, overlay.length(), (mIconSize - textWidth) / 2, mIconSize
+                    - fmi.descent - textPadding, textPaint);
+        }
+
+        // Draw the phone action icon as an overlay
+        Rect src = new Rect(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
+        int iconWidth = icon.getWidth();
+        dst.set(iconWidth - ((int) (20 * density)), -1,
+                iconWidth, ((int) (19 * density)));
+        canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
+
+        canvas.setBitmap(null);
+
+        return icon;
+    }
+}
diff --git a/src/com/android/contacts/common/list/ViewPagerTabStrip.java b/src/com/android/contacts/common/list/ViewPagerTabStrip.java
new file mode 100644
index 0000000..c8ae21a
--- /dev/null
+++ b/src/com/android/contacts/common/list/ViewPagerTabStrip.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.contacts.common.R;
+
+public class ViewPagerTabStrip extends LinearLayout {
+    private int mSelectedUnderlineThickness;
+    private final Paint mSelectedUnderlinePaint;
+
+    private int mIndexForSelection;
+    private float mSelectionOffset;
+
+    public ViewPagerTabStrip(Context context) {
+        this(context, null);
+    }
+
+    public ViewPagerTabStrip(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        final Resources res = context.getResources();
+
+        mSelectedUnderlineThickness =
+                res.getDimensionPixelSize(R.dimen.tab_selected_underline_height);
+        int underlineColor = res.getColor(R.color.tab_selected_underline_color);
+        int backgroundColor = res.getColor(R.color.actionbar_background_color);
+
+        mSelectedUnderlinePaint = new Paint();
+        mSelectedUnderlinePaint.setColor(underlineColor);
+
+        setBackgroundColor(backgroundColor);
+        setWillNotDraw(false);
+    }
+
+    /**
+     * Notifies this view that view pager has been scrolled. We save the tab index
+     * and selection offset for interpolating the position and width of selection
+     * underline.
+     */
+    void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+        mIndexForSelection = position;
+        mSelectionOffset = positionOffset;
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        int childCount = getChildCount();
+
+        // Thick colored underline below the current selection
+        if (childCount > 0) {
+            View selectedTitle = getChildAt(mIndexForSelection);
+
+            if (selectedTitle == null) {
+                // The view pager's tab count changed but we weren't notified yet. Ignore this draw
+                // pass, when we get a new selection we will update and draw the selection strip in
+                // the correct place.
+                return;
+            }
+            int selectedLeft = selectedTitle.getLeft();
+            int selectedRight = selectedTitle.getRight();
+            final boolean isRtl = isRtl();
+            final boolean hasNextTab = isRtl ? mIndexForSelection > 0
+                    : (mIndexForSelection < (getChildCount() - 1));
+            if ((mSelectionOffset > 0.0f) && hasNextTab) {
+                // Draw the selection partway between the tabs
+                View nextTitle = getChildAt(mIndexForSelection + (isRtl ? -1 : 1));
+                int nextLeft = nextTitle.getLeft();
+                int nextRight = nextTitle.getRight();
+
+                selectedLeft = (int) (mSelectionOffset * nextLeft +
+                        (1.0f - mSelectionOffset) * selectedLeft);
+                selectedRight = (int) (mSelectionOffset * nextRight +
+                        (1.0f - mSelectionOffset) * selectedRight);
+            }
+
+            int height = getHeight();
+            canvas.drawRect(selectedLeft, height - mSelectedUnderlineThickness,
+                    selectedRight, height, mSelectedUnderlinePaint);
+        }
+    }
+
+    private boolean isRtl() {
+        return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/common/list/ViewPagerTabs.java b/src/com/android/contacts/common/list/ViewPagerTabs.java
new file mode 100644
index 0000000..48de6af
--- /dev/null
+++ b/src/com/android/contacts/common/list/ViewPagerTabs.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Outline;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.CompatUtils;
+
+/**
+ * Lightweight implementation of ViewPager tabs. This looks similar to traditional actionBar tabs,
+ * but allows for the view containing the tabs to be placed anywhere on screen. Text-related
+ * attributes can also be assigned in XML - these will get propogated to the child TextViews
+ * automatically.
+ */
+public class ViewPagerTabs extends HorizontalScrollView implements ViewPager.OnPageChangeListener {
+
+    ViewPager mPager;
+    private ViewPagerTabStrip mTabStrip;
+
+    /**
+     * Linearlayout that will contain the TextViews serving as tabs. This is the only child
+     * of the parent HorizontalScrollView.
+     */
+    final int mTextStyle;
+    final ColorStateList mTextColor;
+    final int mTextSize;
+    final boolean mTextAllCaps;
+    int mPrevSelected = -1;
+    int mSidePadding;
+
+    private int[] mTabIcons;
+    // For displaying the unread count next to the tab icon.
+    private int[] mUnreadCounts;
+
+    private static final ViewOutlineProvider VIEW_BOUNDS_OUTLINE_PROVIDER;
+    static {
+        if (CompatUtils.isLollipopCompatible()) {
+            VIEW_BOUNDS_OUTLINE_PROVIDER = new ViewOutlineProvider() {
+                @Override
+                public void getOutline(View view, Outline outline) {
+                    outline.setRect(0, 0, view.getWidth(), view.getHeight());
+                }
+            };
+        } else {
+            VIEW_BOUNDS_OUTLINE_PROVIDER = null;
+        }
+    }
+
+    private static final int TAB_SIDE_PADDING_IN_DPS = 10;
+
+    // TODO: This should use <declare-styleable> in the future
+    private static final int[] ATTRS = new int[] {
+        android.R.attr.textSize,
+        android.R.attr.textStyle,
+        android.R.attr.textColor,
+        android.R.attr.textAllCaps
+    };
+
+    /**
+     * Simulates actionbar tab behavior by showing a toast with the tab title when long clicked.
+     */
+    private class OnTabLongClickListener implements OnLongClickListener {
+        final int mPosition;
+
+        public OnTabLongClickListener(int position) {
+            mPosition = position;
+        }
+
+        @Override
+        public boolean onLongClick(View v) {
+            final int[] screenPos = new int[2];
+            getLocationOnScreen(screenPos);
+
+            final Context context = getContext();
+            final int width = getWidth();
+            final int height = getHeight();
+            final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
+
+            Toast toast = Toast.makeText(context, mPager.getAdapter().getPageTitle(mPosition),
+                    Toast.LENGTH_SHORT);
+
+            // Show the toast under the tab
+            toast.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL,
+                    (screenPos[0] + width / 2) - screenWidth / 2, screenPos[1] + height);
+
+            toast.show();
+            return true;
+        }
+    }
+
+    public ViewPagerTabs(Context context) {
+        this(context, null);
+    }
+
+    public ViewPagerTabs(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ViewPagerTabs(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        setFillViewport(true);
+
+        mSidePadding = (int) (getResources().getDisplayMetrics().density * TAB_SIDE_PADDING_IN_DPS);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
+        mTextSize = a.getDimensionPixelSize(0, 0);
+        mTextStyle = a.getInt(1, 0);
+        mTextColor = a.getColorStateList(2);
+        mTextAllCaps = a.getBoolean(3, false);
+
+        mTabStrip = new ViewPagerTabStrip(context);
+        addView(mTabStrip,
+                new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+        a.recycle();
+
+        if (CompatUtils.isLollipopCompatible()) {
+            // enable shadow casting from view bounds
+            setOutlineProvider(VIEW_BOUNDS_OUTLINE_PROVIDER);
+        }
+    }
+
+    public void setViewPager(ViewPager viewPager) {
+        mPager = viewPager;
+        addTabs(mPager.getAdapter());
+    }
+
+    /**
+     * Set the tab icons and initialize an array for unread counts the same length as the icon
+     * array.
+     *
+     * @param tabIcons An array representing the tab icons in order.
+     */
+    public void configureTabIcons(int[] tabIcons) {
+        mTabIcons = tabIcons;
+        mUnreadCounts = new int[tabIcons.length];
+    }
+
+    public void setUnreadCount(int count, int position) {
+        if (mUnreadCounts == null || position >= mUnreadCounts.length) {
+            return;
+        }
+        mUnreadCounts[position] = count;
+    }
+
+    private void addTabs(PagerAdapter adapter) {
+        mTabStrip.removeAllViews();
+
+        final int count = adapter.getCount();
+        for (int i = 0; i < count; i++) {
+            addTab(adapter.getPageTitle(i), i);
+        }
+    }
+
+    private void addTab(CharSequence tabTitle, final int position) {
+        View tabView;
+        if (mTabIcons != null && position < mTabIcons.length) {
+            View layout = LayoutInflater.from(getContext()).inflate(
+                    R.layout.unread_count_tab, null);
+            View iconView = layout.findViewById(R.id.icon);
+            iconView.setBackgroundResource(mTabIcons[position]);
+            iconView.setContentDescription(tabTitle);
+            TextView textView = (TextView) layout.findViewById(R.id.count);
+            if (mUnreadCounts != null && mUnreadCounts[position] > 0) {
+                textView.setText(Integer.toString(mUnreadCounts[position]));
+                textView.setVisibility(View.VISIBLE);
+                iconView.setContentDescription(getResources().getQuantityString(
+                        R.plurals.tab_title_with_unread_items,
+                        mUnreadCounts[position],
+                        tabTitle.toString(),
+                        mUnreadCounts[position]));
+            } else {
+                textView.setVisibility(View.INVISIBLE);
+                iconView.setContentDescription(tabTitle);
+            }
+            tabView = layout;
+        } else {
+            final TextView textView = new TextView(getContext());
+            textView.setText(tabTitle);
+            textView.setBackgroundResource(R.drawable.view_pager_tab_background);
+
+            // Assign various text appearance related attributes to child views.
+            if (mTextStyle > 0) {
+                textView.setTypeface(textView.getTypeface(), mTextStyle);
+            }
+            if (mTextSize > 0) {
+                textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+            }
+            if (mTextColor != null) {
+                textView.setTextColor(mTextColor);
+            }
+            textView.setAllCaps(mTextAllCaps);
+            textView.setGravity(Gravity.CENTER);
+
+            tabView = textView;
+        }
+
+        tabView.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                mPager.setCurrentItem(getRtlPosition(position));
+            }
+        });
+
+        tabView.setOnLongClickListener(new OnTabLongClickListener(position));
+
+        tabView.setPadding(mSidePadding, 0, mSidePadding, 0);
+
+        mTabStrip.addView(tabView, position, new LinearLayout.LayoutParams(
+                LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT, 1));
+
+        // Default to the first child being selected
+        if (position == 0) {
+            mPrevSelected = 0;
+            tabView.setSelected(true);
+        }
+    }
+
+    /**
+     * Remove a tab at a certain index.
+     *
+     * @param index The index of the tab view we wish to remove.
+     */
+    public void removeTab(int index) {
+        View view = mTabStrip.getChildAt(index);
+        if (view != null) {
+            mTabStrip.removeView(view);
+        }
+    }
+
+    /**
+     * Refresh a tab at a certain index by removing it and reconstructing it.
+     *
+     * @param index The index of the tab view we wish to update.
+     */
+    public void updateTab(int index) {
+        removeTab(index);
+
+        if (index < mPager.getAdapter().getCount()) {
+            addTab(mPager.getAdapter().getPageTitle(index), index);
+        }
+    }
+
+    @Override
+    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+        position = getRtlPosition(position);
+        int tabStripChildCount = mTabStrip.getChildCount();
+        if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
+            return;
+        }
+
+        mTabStrip.onPageScrolled(position, positionOffset, positionOffsetPixels);
+    }
+
+    @Override
+    public void onPageSelected(int position) {
+        position = getRtlPosition(position);
+        int tabStripChildCount = mTabStrip.getChildCount();
+        if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
+            return;
+        }
+
+        if (mPrevSelected >= 0 && mPrevSelected < tabStripChildCount) {
+            mTabStrip.getChildAt(mPrevSelected).setSelected(false);
+        }
+        final View selectedChild = mTabStrip.getChildAt(position);
+        selectedChild.setSelected(true);
+
+        // Update scroll position
+        final int scrollPos = selectedChild.getLeft() - (getWidth() - selectedChild.getWidth()) / 2;
+        smoothScrollTo(scrollPos, 0);
+        mPrevSelected = position;
+    }
+
+    @Override
+    public void onPageScrollStateChanged(int state) {
+    }
+
+    private int getRtlPosition(int position) {
+        if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+            return mTabStrip.getChildCount() - 1 - position;
+        }
+        return position;
+    }
+}
+
diff --git a/src/com/android/contacts/common/location/CountryDetector.java b/src/com/android/contacts/common/location/CountryDetector.java
new file mode 100644
index 0000000..2d29a69
--- /dev/null
+++ b/src/com/android/contacts/common/location/CountryDetector.java
@@ -0,0 +1,130 @@
+package com.android.contacts.common.location;
+
+import android.content.Context;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.testing.NeededForTesting;
+
+import java.util.Locale;
+
+/**
+ * This class is used to detect the country where the user is. It is a simplified version of the
+ * country detector service in the framework. The sources of country location are queried in the
+ * following order of reliability:
+ * <ul>
+ * <li>Mobile network</li>
+ * <li>SIM's country</li>
+ * <li>User's default locale</li>
+ * </ul>
+ *
+ * As far as possible this class tries to replicate the behavior of the system's country detector
+ * service:
+ * 1) Order in priority of sources of country location
+ * 2) Mobile network information provided by CDMA phones is ignored
+ */
+public class CountryDetector {
+    private static final String TAG = "CountryDetector";
+
+    private static CountryDetector sInstance;
+
+    private final Context mContext;
+    private final LocaleProvider mLocaleProvider;
+    private final TelephonyManager mTelephonyManager;
+
+    // Used as a default country code when all the sources of country data have failed in the
+    // exceedingly rare event that the device does not have a default locale set for some reason.
+    private final String DEFAULT_COUNTRY_ISO = "US";
+
+    /**
+     * Class that can be used to return the user's default locale. This is in its own class so that
+     * it can be mocked out.
+     */
+    public static class LocaleProvider {
+        public Locale getDefaultLocale() {
+            return Locale.getDefault();
+        }
+    }
+
+    private CountryDetector(Context context) {
+        this (context, (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE),
+                new LocaleProvider());
+    }
+
+    private CountryDetector(Context context, TelephonyManager telephonyManager,
+            LocaleProvider localeProvider) {
+        mTelephonyManager = telephonyManager;
+        mLocaleProvider = localeProvider;
+        mContext = context;
+    }
+
+    /**
+     * Factory method for {@link CountryDetector} that allows the caller to provide mock objects.
+     */
+    @NeededForTesting
+    public CountryDetector getInstanceForTest(Context context, TelephonyManager telephonyManager,
+            LocaleProvider localeProvider) {
+        return new CountryDetector(context, telephonyManager, localeProvider);
+    }
+
+    /**
+     * Returns the instance of the country detector. {@link #initialize(Context)} must have been
+     * called previously.
+     *
+     * @return the initialized country detector.
+     */
+    public synchronized static CountryDetector getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new CountryDetector(context.getApplicationContext());
+        }
+        return sInstance;
+    }
+
+    public String getCurrentCountryIso() {
+        String result = null;
+        if (isNetworkCountryCodeAvailable()) {
+            result = getNetworkBasedCountryIso();
+        }
+        if (TextUtils.isEmpty(result)) {
+            result = getSimBasedCountryIso();
+        }
+        if (TextUtils.isEmpty(result)) {
+            result = getLocaleBasedCountryIso();
+        }
+        if (TextUtils.isEmpty(result)) {
+            result = DEFAULT_COUNTRY_ISO;
+        }
+        return result.toUpperCase(Locale.US);
+    }
+
+    /**
+     * @return the country code of the current telephony network the user is connected to.
+     */
+    private String getNetworkBasedCountryIso() {
+        return mTelephonyManager.getNetworkCountryIso();
+    }
+
+    /**
+     * @return the country code of the SIM card currently inserted in the device.
+     */
+    private String getSimBasedCountryIso() {
+        return mTelephonyManager.getSimCountryIso();
+    }
+
+    /**
+     * @return the country code of the user's currently selected locale.
+     */
+    private String getLocaleBasedCountryIso() {
+        Locale defaultLocale = mLocaleProvider.getDefaultLocale();
+        if (defaultLocale != null) {
+            return defaultLocale.getCountry();
+        }
+        return null;
+    }
+
+    private boolean isNetworkCountryCodeAvailable() {
+        // On CDMA TelephonyManager.getNetworkCountryIso() just returns the SIM's country code.
+        return mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM;
+    }
+}
diff --git a/src/com/android/contacts/common/logging/ListEvent.java b/src/com/android/contacts/common/logging/ListEvent.java
new file mode 100644
index 0000000..7d019dd
--- /dev/null
+++ b/src/com/android/contacts/common/logging/ListEvent.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 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.logging;
+
+import com.google.common.base.Objects;
+
+/**
+ * Describes how user view and use a list
+ */
+public final class ListEvent {
+
+    /** The type of action taken by the user. **/
+    public int actionType;
+
+    /** The type of list the user is viewing. **/
+    public int listType;
+
+    /** The number of contacts in the list. **/
+    public int count;
+
+    /** The index of contact clicked by user. **/
+    public int clickedIndex = -1;
+
+    /** The number of contact selected when user takes an action (link, delete, share, etc). **/
+    public int numSelected;
+
+    // Should match ContactsExtension.ListEvent.ActionType values in
+    // http://cs/google3/logs/proto/wireless/android/contacts/contacts_extensions.proto
+    public static final class ActionType {
+        public static final int UNKNOWN = 0;
+        public static final int LOAD = 1;
+        public static final int CLICK = 2;
+        public static final int SELECT = 3;
+        public static final int SHARE = 4;
+        public static final int DELETE = 5;
+        public static final int LINK = 6;
+        public static final int REMOVE_LABEL = 7;
+
+        private ActionType() {
+        }
+    }
+
+    // Should match ContactsExtension.ListEvent.ListType values in
+    // http://cs/google3/logs/proto/wireless/android/contacts/contacts_extensions.proto
+    public static final class ListType {
+        public static final int UNKNOWN_LIST = 0;
+        public static final int ALL_CONTACTS = 1;
+        public static final int ACCOUNT = 2;
+        public static final int GROUP = 3;
+        public static final int SEARCH_RESULT = 4;
+        public static final int DEVICE = 5;
+        public static final int CUSTOM = 6;
+        public static final int STARRED = 7;
+        public static final int PHONE_NUMBERS = 8;
+        public static final int SINGLE_CONTACT = 9;
+        public static final int PICK_CONTACT = 10;
+        public static final int PICK_CONTACT_FOR_SHORTCUT = 11;
+        public static final int PICK_PHONE = 12;
+        public static final int PICK_EMAIL = 13;
+        public static final int PICK_POSTAL = 14;
+        public static final int PICK_JOIN = 15;
+        public static final int PICK_GROUP_MEMBERS = 16;
+
+        private ListType() {
+        }
+    }
+
+    public ListEvent() {
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("actionType", actionType)
+                .add("listType", listType)
+                .add("count", count)
+                .add("clickedIndex", clickedIndex)
+                .add("numSelected", numSelected)
+                .toString();
+    }
+}
diff --git a/src/com/android/contacts/common/logging/Logger.java b/src/com/android/contacts/common/logging/Logger.java
new file mode 100644
index 0000000..72d1e2a
--- /dev/null
+++ b/src/com/android/contacts/common/logging/Logger.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 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.logging;
+
+import android.app.Activity;
+
+import com.android.contacts.common.logging.ScreenEvent.ScreenType;
+import com.android.contacts.commonbind.ObjectFactory;
+
+/**
+ * Logs analytics events.
+ */
+public abstract class Logger {
+    public static final String TAG = "Logger";
+
+    private static Logger getInstance() {
+        return ObjectFactory.getLogger();
+    }
+
+    /**
+     * Logs an event indicating that a screen was displayed.
+     *
+     * @param screenType integer identifier of the displayed screen
+     * @param activity Parent activity of the displayed screen.
+     */
+    public static void logScreenView(Activity activity, int screenType) {
+        logScreenView(activity, screenType, ScreenType.UNKNOWN);
+    }
+
+    /**
+     * @param previousScreenType integer identifier of the displayed screen the user came from.
+     */
+    public static void logScreenView(Activity activity, int screenType, int previousScreenType) {
+        final Logger logger = getInstance();
+        if (logger != null) {
+            logger.logScreenViewImpl(screenType, previousScreenType);
+        }
+    }
+
+    /**
+     * Logs the results of a user search for a particular contact.
+     */
+    public static void logSearchEvent(SearchState searchState) {
+        final Logger logger = getInstance();
+        if (logger != null) {
+            logger.logSearchEventImpl(searchState);
+        }
+    }
+
+    /**
+     * Logs how users view and use a contacts list. See {@link ListEvent} for definition of
+     * parameters.
+     */
+    public static void logListEvent(int actionType, int listType, int count, int clickedIndex,
+            int numSelected) {
+        final ListEvent event = new ListEvent();
+        event.actionType = actionType;
+        event.listType = listType;
+        event.count = count;
+        event.clickedIndex = clickedIndex;
+        event.numSelected = numSelected;
+
+        final Logger logger = getInstance();
+        if (logger != null) {
+            logger.logListEventImpl(event);
+        }
+    }
+
+    public abstract void logScreenViewImpl(int screenType, int previousScreenType);
+    public abstract void logSearchEventImpl(SearchState searchState);
+    public abstract void logListEventImpl(ListEvent event);
+}
diff --git a/src/com/android/contacts/common/logging/ScreenEvent.java b/src/com/android/contacts/common/logging/ScreenEvent.java
new file mode 100644
index 0000000..6af020b
--- /dev/null
+++ b/src/com/android/contacts/common/logging/ScreenEvent.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 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.logging;
+
+/**
+ * Stores constants identifying individual screens/dialogs/fragments in the application, and also
+ * provides a mapping of integer id -> screen name mappings for analytics purposes.
+ */
+public class ScreenEvent {
+
+    // Should match ContactsExtension.ScreenEvent.ScreenType values in
+    // http://cs/google3/logs/proto/wireless/android/contacts/contacts_extensions.proto
+    public static class ScreenType {
+        public static final int UNKNOWN = 0;
+        public static final int SEARCH = 1;
+        public static final int SEARCH_EXIT = 2;
+        public static final int FAVORITES = 3;
+        public static final int ALL_CONTACTS = 4;
+        public static final int QUICK_CONTACT = 5;
+        public static final int EDITOR = 6;
+        public static final int LIST_ACCOUNT = 7;
+        public static final int LIST_GROUP = 8;
+        public static final int ME_CONTACT = 9;
+    }
+}
diff --git a/src/com/android/contacts/common/logging/SearchState.java b/src/com/android/contacts/common/logging/SearchState.java
new file mode 100644
index 0000000..f4719e4
--- /dev/null
+++ b/src/com/android/contacts/common/logging/SearchState.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 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.logging;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.google.common.base.Objects;
+
+/**
+ * Describes the results of a user search for a particular contact.
+ */
+public final class SearchState implements Parcelable {
+
+    /** The length of the query string input by the user. */
+    public int queryLength;
+
+    /** The number of partitions (groups of results) presented to the user. */
+    public int numPartitions;
+
+    /** The total number of results (across all partitions) presented to the user. */
+    public int numResults;
+
+    /** The number of results presented to the user in the partition that was selected. */
+    public int numResultsInSelectedPartition = -1;
+
+    /** The zero-based index of the partition in which the clicked query result resides. */
+    public int selectedPartition = -1;
+
+    /** The index of the clicked query result within its partition. */
+    public int selectedIndexInPartition = -1;
+
+    /**
+     * The zero-based index of the clicked query result among all results displayed to the user
+     * (across partitions).
+     */
+    public int selectedIndex = -1;
+
+    public static final Creator<SearchState> CREATOR = new Creator<SearchState>() {
+        @Override
+        public SearchState createFromParcel(Parcel in) {
+            return new SearchState(in);
+        }
+
+        @Override
+        public SearchState[] newArray(int size) {
+            return new SearchState[size];
+        }
+    };
+
+    public SearchState() {
+    }
+
+    protected SearchState(Parcel source) {
+        readFromParcel(source);
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("queryLength", queryLength)
+                .add("numPartitions", numPartitions)
+                .add("numResults", numResults)
+                .add("numResultsInSelectedPartition", numResultsInSelectedPartition)
+                .add("selectedPartition", selectedPartition)
+                .add("selectedIndexInPartition", selectedIndexInPartition)
+                .add("selectedIndex", selectedIndex)
+                .toString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(queryLength);
+        dest.writeInt(numPartitions);
+        dest.writeInt(numResults);
+        dest.writeInt(numResultsInSelectedPartition);
+        dest.writeInt(selectedPartition);
+        dest.writeInt(selectedIndexInPartition);
+        dest.writeInt(selectedIndex);
+    }
+
+    private void readFromParcel(Parcel source) {
+        queryLength = source.readInt();
+        numPartitions = source.readInt();
+        numResults = source.readInt();
+        numResultsInSelectedPartition = source.readInt();
+        selectedPartition = source.readInt();
+        selectedIndexInPartition = source.readInt();
+        selectedIndex = source.readInt();
+    }
+}
diff --git a/src/com/android/contacts/common/model/AccountTypeManager.java b/src/com/android/contacts/common/model/AccountTypeManager.java
new file mode 100644
index 0000000..3ef3502
--- /dev/null
+++ b/src/com/android/contacts/common/model/AccountTypeManager.java
@@ -0,0 +1,846 @@
+/*
+ * 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;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.accounts.OnAccountsUpdateListener;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SyncAdapterType;
+import android.content.SyncStatusObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.TimingLogger;
+
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.list.ContactListFilterController;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.ExchangeAccountType;
+import com.android.contacts.common.model.account.ExternalAccountType;
+import com.android.contacts.common.model.account.FallbackAccountType;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.account.SamsungAccountType;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.testing.NeededForTesting;
+import com.android.contacts.common.util.Constants;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Singleton holder for all parsed {@link AccountType} available on the
+ * system, typically filled through {@link PackageManager} queries.
+ */
+public abstract class AccountTypeManager {
+    static final String TAG = "AccountTypeManager";
+
+    private static final Object mInitializationLock = new Object();
+    private static AccountTypeManager mAccountTypeManager;
+
+    /**
+     * Requests the singleton instance of {@link AccountTypeManager} with data bound from
+     * the available authenticators. This method can safely be called from the UI thread.
+     */
+    public static AccountTypeManager getInstance(Context context) {
+        synchronized (mInitializationLock) {
+            if (mAccountTypeManager == null) {
+                context = context.getApplicationContext();
+                mAccountTypeManager = new AccountTypeManagerImpl(context);
+            }
+        }
+        return mAccountTypeManager;
+    }
+
+    /**
+     * Set the instance of account type manager.  This is only for and should only be used by unit
+     * tests.  While having this method is not ideal, it's simpler than the alternative of
+     * holding this as a service in the ContactsApplication context class.
+     *
+     * @param mockManager The mock AccountTypeManager.
+     */
+    @NeededForTesting
+    public static void setInstanceForTest(AccountTypeManager mockManager) {
+        synchronized (mInitializationLock) {
+            mAccountTypeManager = mockManager;
+        }
+    }
+
+    /**
+     * Returns the list of all accounts (if contactWritableOnly is false) or just the list of
+     * contact writable accounts (if contactWritableOnly is true).
+     */
+    // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts()
+    public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
+
+    /**
+     * Sort accounts based on default account.
+     */
+    public abstract void sortAccounts(AccountWithDataSet defaultAccount);
+
+    /**
+     * Returns the list of accounts that are group writable.
+     */
+    public abstract List<AccountWithDataSet> getGroupWritableAccounts();
+
+    public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
+
+    public final AccountType getAccountType(String accountType, String dataSet) {
+        return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
+    }
+
+    public final AccountType getAccountTypeForAccount(AccountWithDataSet account) {
+        if (account != null) {
+            return getAccountType(account.getAccountTypeWithDataSet());
+        }
+        return getAccountType(null, null);
+    }
+
+    /**
+     * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
+     * which support the "invite" feature and have one or more account.
+     *
+     * This is a filtered down and more "usable" list compared to
+     * {@link #getAllInvitableAccountTypes}, where usable is defined as:
+     * (1) making sure that the app that contributed the account type is not disabled
+     * (in order to avoid presenting the user with an option that does nothing), and
+     * (2) that there is at least one raw contact with that account type in the database
+     * (assuming that the user probably doesn't use that account type).
+     *
+     * Warning: Don't use on the UI thread because this can scan the database.
+     */
+    public abstract Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes();
+
+    /**
+     * Find the best {@link DataKind} matching the requested
+     * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}.
+     * If no direct match found, we try searching {@link FallbackAccountType}.
+     */
+    public DataKind getKindOrFallback(AccountType type, String mimeType) {
+        return type == null ? null : type.getKindForMimetype(mimeType);
+    }
+
+    /**
+     * Returns all registered {@link AccountType}s, including extension ones.
+     *
+     * @param contactWritableOnly if true, it only returns ones that support writing contacts.
+     */
+    public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly);
+
+    /**
+     * @param contactWritableOnly if true, it only returns ones that support writing contacts.
+     * @return true when this instance contains the given account.
+     */
+    public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) {
+        for (AccountWithDataSet account_2 : getAccounts(contactWritableOnly)) {
+            if (account.equals(account_2)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
+
+class AccountComparator implements Comparator<AccountWithDataSet> {
+    private AccountWithDataSet mDefaultAccount;
+
+    public AccountComparator(AccountWithDataSet defaultAccount) {
+        mDefaultAccount = defaultAccount;
+    }
+
+    @Override
+    public int compare(AccountWithDataSet a, AccountWithDataSet b) {
+        if (Objects.equal(a.name, b.name) && Objects.equal(a.type, b.type)
+                && Objects.equal(a.dataSet, b.dataSet)) {
+            return 0;
+        } else if (b.name == null || b.type == null) {
+            return -1;
+        } else if (a.name == null || a.type == null) {
+            return 1;
+        } else if (isWritableGoogleAccount(a) && a.equals(mDefaultAccount)) {
+            return -1;
+        } else if (isWritableGoogleAccount(b) && b.equals(mDefaultAccount)) {
+            return 1;
+        } else if (isWritableGoogleAccount(a) && !isWritableGoogleAccount(b)) {
+            return -1;
+        } else if (isWritableGoogleAccount(b) && !isWritableGoogleAccount(a)) {
+            return 1;
+        } else {
+            int diff = a.name.compareToIgnoreCase(b.name);
+            if (diff != 0) {
+                return diff;
+            }
+            diff = a.type.compareToIgnoreCase(b.type);
+            if (diff != 0) {
+                return diff;
+            }
+
+            // Accounts without data sets get sorted before those that have them.
+            if (a.dataSet != null) {
+                return b.dataSet == null ? 1 : a.dataSet.compareToIgnoreCase(b.dataSet);
+            } else {
+                return -1;
+            }
+        }
+    }
+
+    private static boolean isWritableGoogleAccount(AccountWithDataSet account) {
+        return GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null;
+    }
+}
+
+class AccountTypeManagerImpl extends AccountTypeManager
+        implements OnAccountsUpdateListener, SyncStatusObserver {
+
+    private static final Map<AccountTypeWithDataSet, AccountType>
+            EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP =
+            Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
+
+    /**
+     * A sample contact URI used to test whether any activities will respond to an
+     * invitable intent with the given URI as the intent data. This doesn't need to be
+     * specific to a real contact because an app that intercepts the intent should probably do so
+     * for all types of contact URIs.
+     */
+    private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri(
+            1, "xxx");
+
+    private Context mContext;
+    private AccountManager mAccountManager;
+
+    private AccountType mFallbackAccountType;
+
+    private List<AccountWithDataSet> mAccounts = Lists.newArrayList();
+    private List<AccountWithDataSet> mContactWritableAccounts = Lists.newArrayList();
+    private List<AccountWithDataSet> mGroupWritableAccounts = Lists.newArrayList();
+    private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
+    private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
+            EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
+
+    private final InvitableAccountTypeCache mInvitableAccountTypeCache;
+
+    /**
+     * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been
+     * initialized. False otherwise.
+     */
+    private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false);
+
+    /**
+     * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing.
+     * False otherwise.
+     */
+    private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false);
+
+    private static final int MESSAGE_LOAD_DATA = 0;
+    private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
+
+    private HandlerThread mListenerThread;
+    private Handler mListenerHandler;
+
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+    private final Runnable mCheckFilterValidityRunnable = new Runnable () {
+        @Override
+        public void run() {
+            ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
+        }
+    };
+
+    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent);
+            mListenerHandler.sendMessage(msg);
+        }
+
+    };
+
+    /* A latch that ensures that asynchronous initialization completes before data is used */
+    private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1);
+
+    /**
+     * Internal constructor that only performs initial parsing.
+     */
+    public AccountTypeManagerImpl(Context context) {
+        mContext = context;
+        mFallbackAccountType = new FallbackAccountType(context);
+
+        mAccountManager = AccountManager.get(mContext);
+
+        mListenerThread = new HandlerThread("AccountChangeListener");
+        mListenerThread.start();
+        mListenerHandler = new Handler(mListenerThread.getLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case MESSAGE_LOAD_DATA:
+                        loadAccountsInBackground();
+                        break;
+                    case MESSAGE_PROCESS_BROADCAST_INTENT:
+                        processBroadcastIntent((Intent) msg.obj);
+                        break;
+                }
+            }
+        };
+
+        mInvitableAccountTypeCache = new InvitableAccountTypeCache();
+
+        // Request updates when packages or accounts change
+        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addDataScheme("package");
+        mContext.registerReceiver(mBroadcastReceiver, filter);
+        IntentFilter sdFilter = new IntentFilter();
+        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+        mContext.registerReceiver(mBroadcastReceiver, sdFilter);
+
+        // Request updates when locale is changed so that the order of each field will
+        // be able to be changed on the locale change.
+        filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
+        mContext.registerReceiver(mBroadcastReceiver, filter);
+
+        mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);
+
+        ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
+
+        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+    }
+
+    @Override
+    public void onStatusChanged(int which) {
+        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+    }
+
+    public void processBroadcastIntent(Intent intent) {
+        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+    }
+
+    /* This notification will arrive on the background thread */
+    public void onAccountsUpdated(Account[] accounts) {
+        // Refresh to catch any changed accounts
+        loadAccountsInBackground();
+    }
+
+    /**
+     * Returns instantly if accounts and account types have already been loaded.
+     * Otherwise waits for the background thread to complete the loading.
+     */
+    void ensureAccountsLoaded() {
+        CountDownLatch latch = mInitializationLatch;
+        if (latch == null) {
+            return;
+        }
+        while (true) {
+            try {
+                latch.await();
+                return;
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * Loads account list and corresponding account types (potentially with data sets). Always
+     * called on a background thread.
+     */
+    protected void loadAccountsInBackground() {
+        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
+            Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start");
+        }
+        TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground");
+        final long startTime = SystemClock.currentThreadTimeMillis();
+        final long startTimeWall = SystemClock.elapsedRealtime();
+
+        // Account types, keyed off the account type and data set concatenation.
+        final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet =
+                Maps.newHashMap();
+
+        // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}.  Since there can
+        // be multiple account types (with different data sets) for the same type of account, each
+        // type string may have multiple AccountType entries.
+        final Map<String, List<AccountType>> accountTypesByType = Maps.newHashMap();
+
+        final List<AccountWithDataSet> allAccounts = Lists.newArrayList();
+        final List<AccountWithDataSet> contactWritableAccounts = Lists.newArrayList();
+        final List<AccountWithDataSet> groupWritableAccounts = Lists.newArrayList();
+        final Set<String> extensionPackages = Sets.newHashSet();
+
+        final AccountManager am = mAccountManager;
+
+        final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes();
+        final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
+
+        // First process sync adapters to find any that provide contact data.
+        for (SyncAdapterType sync : syncs) {
+            if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
+                // Skip sync adapters that don't provide contact data.
+                continue;
+            }
+
+            // Look for the formatting details provided by each sync
+            // adapter, using the authenticator to find general resources.
+            final String type = sync.accountType;
+            final AuthenticatorDescription auth = findAuthenticator(auths, type);
+            if (auth == null) {
+                Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it.");
+                continue;
+            }
+
+            AccountType accountType;
+            if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
+                accountType = new GoogleAccountType(mContext, auth.packageName);
+            } else if (ExchangeAccountType.isExchangeType(type)) {
+                accountType = new ExchangeAccountType(mContext, auth.packageName, type);
+            } else if (SamsungAccountType.isSamsungAccountType(mContext, type,
+                    auth.packageName)) {
+                accountType = new SamsungAccountType(mContext, auth.packageName, type);
+            } else {
+                Log.d(TAG, "Registering external account type=" + type
+                        + ", packageName=" + auth.packageName);
+                accountType = new ExternalAccountType(mContext, auth.packageName, false);
+            }
+            if (!accountType.isInitialized()) {
+                if (accountType.isEmbedded()) {
+                    throw new IllegalStateException("Problem initializing embedded type "
+                            + accountType.getClass().getCanonicalName());
+                } else {
+                    // Skip external account types that couldn't be initialized.
+                    continue;
+                }
+            }
+
+            accountType.accountType = auth.type;
+            accountType.titleRes = auth.labelId;
+            accountType.iconRes = auth.iconId;
+
+            addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
+
+            // Check to see if the account type knows of any other non-sync-adapter packages
+            // that may provide other data sets of contact data.
+            extensionPackages.addAll(accountType.getExtensionPackageNames());
+        }
+
+        // If any extension packages were specified, process them as well.
+        if (!extensionPackages.isEmpty()) {
+            Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages");
+            for (String extensionPackage : extensionPackages) {
+                ExternalAccountType accountType =
+                    new ExternalAccountType(mContext, extensionPackage, true);
+                if (!accountType.isInitialized()) {
+                    // Skip external account types that couldn't be initialized.
+                    continue;
+                }
+                if (!accountType.hasContactsMetadata()) {
+                    Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
+                            + " it doesn't have the CONTACTS_STRUCTURE metadata");
+                    continue;
+                }
+                if (TextUtils.isEmpty(accountType.accountType)) {
+                    Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
+                            + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
+                            + " attribute");
+                    continue;
+                }
+                Log.d(TAG, "Registering extension package account type="
+                        + accountType.accountType + ", dataSet=" + accountType.dataSet
+                        + ", packageName=" + extensionPackage);
+
+                addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
+            }
+        }
+        timings.addSplit("Loaded account types");
+
+        // Map in accounts to associate the account names with each account type entry.
+        Account[] accounts = mAccountManager.getAccounts();
+        for (Account account : accounts) {
+            boolean syncable =
+                ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
+
+            if (syncable) {
+                List<AccountType> accountTypes = accountTypesByType.get(account.type);
+                if (accountTypes != null) {
+                    // Add an account-with-data-set entry for each account type that is
+                    // authenticated by this account.
+                    for (AccountType accountType : accountTypes) {
+                        AccountWithDataSet accountWithDataSet = new AccountWithDataSet(
+                                account.name, account.type, accountType.dataSet);
+                        allAccounts.add(accountWithDataSet);
+                        if (accountType.areContactsWritable()) {
+                            contactWritableAccounts.add(accountWithDataSet);
+                        }
+                        if (accountType.isGroupMembershipEditable()) {
+                            groupWritableAccounts.add(accountWithDataSet);
+                        }
+                    }
+                }
+            }
+        }
+
+        final AccountComparator accountComparator = new AccountComparator(null);
+        Collections.sort(allAccounts, accountComparator);
+        Collections.sort(contactWritableAccounts, accountComparator);
+        Collections.sort(groupWritableAccounts, accountComparator);
+
+        timings.addSplit("Loaded accounts");
+
+        synchronized (this) {
+            mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
+            mAccounts = allAccounts;
+            mContactWritableAccounts = contactWritableAccounts;
+            mGroupWritableAccounts = groupWritableAccounts;
+            mInvitableAccountTypes = findAllInvitableAccountTypes(
+                    mContext, allAccounts, accountTypesByTypeAndDataSet);
+        }
+
+        timings.dumpToLog();
+        final long endTimeWall = SystemClock.elapsedRealtime();
+        final long endTime = SystemClock.currentThreadTimeMillis();
+
+        Log.i(TAG, "Loaded meta-data for " + mAccountTypesWithDataSets.size() + " account types, "
+                + mAccounts.size() + " accounts in " + (endTimeWall - startTimeWall) + "ms(wall) "
+                + (endTime - startTime) + "ms(cpu)");
+
+        if (mInitializationLatch != null) {
+            mInitializationLatch.countDown();
+            mInitializationLatch = null;
+        }
+        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
+            Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish");
+        }
+
+        // Check filter validity since filter may become obsolete after account update. It must be
+        // done from UI thread.
+        mMainThreadHandler.post(mCheckFilterValidityRunnable);
+    }
+
+    // Bookkeeping method for tracking the known account types in the given maps.
+    private void addAccountType(AccountType accountType,
+            Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet,
+            Map<String, List<AccountType>> accountTypesByType) {
+        accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
+        List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
+        if (accountsForType == null) {
+            accountsForType = Lists.newArrayList();
+        }
+        accountsForType.add(accountType);
+        accountTypesByType.put(accountType.accountType, accountsForType);
+    }
+
+    /**
+     * Find a specific {@link AuthenticatorDescription} in the provided list
+     * that matches the given account type.
+     */
+    protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths,
+            String accountType) {
+        for (AuthenticatorDescription auth : auths) {
+            if (accountType.equals(auth.type)) {
+                return auth;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return list of all known, contact writable {@link AccountWithDataSet}'s.
+     */
+    @Override
+    public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
+        ensureAccountsLoaded();
+        return contactWritableOnly ? mContactWritableAccounts : mAccounts;
+    }
+
+    /**
+     * Sort accounts based on default account.
+     */
+    @Override
+    public void sortAccounts(AccountWithDataSet defaultAccount) {
+        Collections.sort(mAccounts, new AccountComparator(defaultAccount));
+        Collections.sort(mContactWritableAccounts, new AccountComparator(defaultAccount));
+        Collections.sort(mGroupWritableAccounts, new AccountComparator(defaultAccount));
+    }
+
+    /**
+     * Return the list of all known, group writable {@link AccountWithDataSet}'s.
+     */
+    public List<AccountWithDataSet> getGroupWritableAccounts() {
+        ensureAccountsLoaded();
+        return mGroupWritableAccounts;
+    }
+
+    /**
+     * Find the best {@link DataKind} matching the requested
+     * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}.
+     * If no direct match found, we try searching {@link FallbackAccountType}.
+     */
+    @Override
+    public DataKind getKindOrFallback(AccountType type, String mimeType) {
+        ensureAccountsLoaded();
+        DataKind kind = null;
+
+        // Try finding account type and kind matching request
+        if (type != null) {
+            kind = type.getKindForMimetype(mimeType);
+        }
+
+        if (kind == null) {
+            // Nothing found, so try fallback as last resort
+            kind = mFallbackAccountType.getKindForMimetype(mimeType);
+        }
+
+        if (kind == null) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType);
+            }
+        }
+
+        return kind;
+    }
+
+    /**
+     * Return {@link AccountType} for the given account type and data set.
+     */
+    @Override
+    public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
+        ensureAccountsLoaded();
+        synchronized (this) {
+            AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
+            return type != null ? type : mFallbackAccountType;
+        }
+    }
+
+    /**
+     * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
+     * which support the "invite" feature and have one or more account. This is an unfiltered
+     * list. See {@link #getUsableInvitableAccountTypes()}.
+     */
+    private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
+        ensureAccountsLoaded();
+        return mInvitableAccountTypes;
+    }
+
+    @Override
+    public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
+        ensureAccountsLoaded();
+        // Since this method is not thread-safe, it's possible for multiple threads to encounter
+        // the situation where (1) the cache has not been initialized yet or
+        // (2) an async task to refresh the account type list in the cache has already been
+        // started. Hence we use {@link AtomicBoolean}s and return cached values immediately
+        // while we compute the actual result in the background. We use this approach instead of
+        // using "synchronized" because computing the account type list involves a DB read, and
+        // can potentially cause a deadlock situation if this method is called from code which
+        // holds the DB lock. The trade-off of potentially having an incorrect list of invitable
+        // account types for a short period of time seems more manageable than enforcing the
+        // context in which this method is called.
+
+        // Computing the list of usable invitable account types is done on the fly as requested.
+        // If this method has never been called before, then block until the list has been computed.
+        if (!mInvitablesCacheIsInitialized.get()) {
+            mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext));
+            mInvitablesCacheIsInitialized.set(true);
+        } else {
+            // Otherwise, there is a value in the cache. If the value has expired and
+            // an async task has not already been started by another thread, then kick off a new
+            // async task to compute the list.
+            if (mInvitableAccountTypeCache.isExpired() &&
+                    mInvitablesTaskIsRunning.compareAndSet(false, true)) {
+                new FindInvitablesTask().execute();
+            }
+        }
+
+        return mInvitableAccountTypeCache.getCachedValue();
+    }
+
+    /**
+     * Return all {@link AccountType}s with at least one account which supports "invite", i.e.
+     * its {@link AccountType#getInviteContactActivityClassName()} is not empty.
+     */
+    @VisibleForTesting
+    static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(Context context,
+            Collection<AccountWithDataSet> accounts,
+            Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
+        HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
+        for (AccountWithDataSet account : accounts) {
+            AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet();
+            AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet);
+            if (type == null) continue; // just in case
+            if (result.containsKey(accountTypeWithDataSet)) continue;
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Type " + accountTypeWithDataSet
+                        + " inviteClass=" + type.getInviteContactActivityClassName());
+            }
+            if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) {
+                result.put(accountTypeWithDataSet, type);
+            }
+        }
+        return Collections.unmodifiableMap(result);
+    }
+
+    /**
+     * Return all usable {@link AccountType}s that support the "invite" feature from the
+     * list of all potential invitable account types (retrieved from
+     * {@link #getAllInvitableAccountTypes}). A usable invitable account type means:
+     * (1) there is at least 1 raw contact in the database with that account type, and
+     * (2) the app contributing the account type is not disabled.
+     *
+     * Warning: Don't use on the UI thread because this can scan the database.
+     */
+    private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes(
+            Context context) {
+        Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes();
+        if (allInvitables.isEmpty()) {
+            return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
+        }
+
+        final HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
+        result.putAll(allInvitables);
+
+        final PackageManager packageManager = context.getPackageManager();
+        for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) {
+            AccountType accountType = allInvitables.get(accountTypeWithDataSet);
+
+            // Make sure that account types don't come from apps that are disabled.
+            Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType,
+                    SAMPLE_CONTACT_URI);
+            if (invitableIntent == null) {
+                result.remove(accountTypeWithDataSet);
+                continue;
+            }
+            ResolveInfo resolveInfo = packageManager.resolveActivity(invitableIntent,
+                    PackageManager.MATCH_DEFAULT_ONLY);
+            if (resolveInfo == null) {
+                // If we can't find an activity to start for this intent, then there's no point in
+                // showing this option to the user.
+                result.remove(accountTypeWithDataSet);
+                continue;
+            }
+
+            // Make sure that there is at least 1 raw contact with this account type. This check
+            // is non-trivial and should not be done on the UI thread.
+            if (!accountTypeWithDataSet.hasData(context)) {
+                result.remove(accountTypeWithDataSet);
+            }
+        }
+
+        return Collections.unmodifiableMap(result);
+    }
+
+    @Override
+    public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
+        ensureAccountsLoaded();
+        final List<AccountType> accountTypes = Lists.newArrayList();
+        synchronized (this) {
+            for (AccountType type : mAccountTypesWithDataSets.values()) {
+                if (!contactWritableOnly || type.areContactsWritable()) {
+                    accountTypes.add(type);
+                }
+            }
+        }
+        return accountTypes;
+    }
+
+    /**
+     * Background task to find all usable {@link AccountType}s that support the "invite" feature
+     * from the list of all potential invitable account types. Once the work is completed,
+     * the list of account types is stored in the {@link AccountTypeManager}'s
+     * {@link InvitableAccountTypeCache}.
+     */
+    private class FindInvitablesTask extends AsyncTask<Void, Void,
+            Map<AccountTypeWithDataSet, AccountType>> {
+
+        @Override
+        protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
+            return findUsableInvitableAccountTypes(mContext);
+        }
+
+        @Override
+        protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) {
+            mInvitableAccountTypeCache.setCachedValue(accountTypes);
+            mInvitablesTaskIsRunning.set(false);
+        }
+    }
+
+    /**
+     * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a
+     * {@link Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only
+     * for {@link #TIME_TO_LIVE} milliseconds.
+     */
+    private static final class InvitableAccountTypeCache {
+
+        /**
+         * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds
+         * has elapsed.
+         */
+        private static final long TIME_TO_LIVE = 60000;
+
+        private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes;
+
+        private long mTimeLastSet;
+
+        /**
+         * Returns true if the data in this cache is stale and needs to be refreshed. Returns false
+         * otherwise.
+         */
+        public boolean isExpired() {
+             return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE;
+        }
+
+        /**
+         * Returns the cached value. Note that the caller is responsible for checking
+         * {@link #isExpired()} to ensure that the value is not stale.
+         */
+        public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
+            return mInvitableAccountTypes;
+        }
+
+        public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
+            mInvitableAccountTypes = map;
+            mTimeLastSet = SystemClock.elapsedRealtime();
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/model/BuilderWrapper.java b/src/com/android/contacts/common/model/BuilderWrapper.java
new file mode 100644
index 0000000..325c3df
--- /dev/null
+++ b/src/com/android/contacts/common/model/BuilderWrapper.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 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.content.ContentProviderOperation.Builder;
+
+/**
+ * This class is created for the purpose of compatibility and make the type of
+ * ContentProviderOperation available on pre-M SDKs. Since ContentProviderOperation is
+ * usually created by Builder and we don’t have access to the type via Builder, so we need to
+ * create a wrapper class for Builder first and include type. Then we could use the builder and
+ * the type in this class to create a wrapper of ContentProviderOperation.
+ */
+public class BuilderWrapper {
+    private Builder mBuilder;
+    private int mType;
+
+    public BuilderWrapper(Builder builder, int type) {
+        mBuilder = builder;
+        mType = type;
+    }
+
+    public int getType() {
+        return mType;
+    }
+
+    public void setType(int mType) {
+        this.mType = mType;
+    }
+
+    public Builder getBuilder() {
+        return mBuilder;
+    }
+
+    public void setBuilder(Builder mBuilder) {
+        this.mBuilder = mBuilder;
+    }
+}
diff --git a/src/com/android/contacts/common/model/CPOWrapper.java b/src/com/android/contacts/common/model/CPOWrapper.java
new file mode 100644
index 0000000..4124df8
--- /dev/null
+++ b/src/com/android/contacts/common/model/CPOWrapper.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 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.content.ContentProviderOperation;
+
+/**
+ * This class is created for the purpose of compatibility and make the type of
+ * ContentProviderOperation available on pre-M SDKs.
+ */
+public class CPOWrapper {
+    private ContentProviderOperation mOperation;
+    private int mType;
+
+    public CPOWrapper(ContentProviderOperation builder, int type) {
+        mOperation = builder;
+        mType = type;
+    }
+
+    public int getType() {
+        return mType;
+    }
+
+    public void setType(int type) {
+        this.mType = type;
+    }
+
+    public ContentProviderOperation getOperation() {
+        return mOperation;
+    }
+
+    public void setOperation(ContentProviderOperation operation) {
+        this.mOperation = operation;
+    }
+}
diff --git a/src/com/android/contacts/common/model/Contact.java b/src/com/android/contacts/common/model/Contact.java
new file mode 100644
index 0000000..27bf13e
--- /dev/null
+++ b/src/com/android/contacts/common/model/Contact.java
@@ -0,0 +1,496 @@
+/*
+ * 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.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.util.DataStatus;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * A Contact represents a single person or logical entity as perceived by the user.  The information
+ * about a contact can come from multiple data sources, which are each represented by a RawContact
+ * object.  Thus, a Contact is associated with a collection of RawContact objects.
+ *
+ * The aggregation of raw contacts into a single contact is performed automatically, and it is
+ * also possible for users to manually split and join raw contacts into various contacts.
+ *
+ * Only the {@link ContactLoader} class can create a Contact object with various flags to allow
+ * partial loading of contact data.  Thus, an instance of this class should be treated as
+ * a read-only object.
+ */
+public class Contact {
+    private enum Status {
+        /** Contact is successfully loaded */
+        LOADED,
+        /** There was an error loading the contact */
+        ERROR,
+        /** Contact is not found */
+        NOT_FOUND,
+    }
+
+    private final Uri mRequestedUri;
+    private final Uri mLookupUri;
+    private final Uri mUri;
+    private final long mDirectoryId;
+    private final String mLookupKey;
+    private final long mId;
+    private final long mNameRawContactId;
+    private final int mDisplayNameSource;
+    private final long mPhotoId;
+    private final String mPhotoUri;
+    private final String mDisplayName;
+    private final String mAltDisplayName;
+    private final String mPhoneticName;
+    private final boolean mStarred;
+    private final Integer mPresence;
+    private ImmutableList<RawContact> mRawContacts;
+    private ImmutableMap<Long,DataStatus> mStatuses;
+    private ImmutableList<AccountType> mInvitableAccountTypes;
+
+    private String mDirectoryDisplayName;
+    private String mDirectoryType;
+    private String mDirectoryAccountType;
+    private String mDirectoryAccountName;
+    private int mDirectoryExportSupport;
+
+    private ImmutableList<GroupMetaData> mGroups;
+
+    private byte[] mPhotoBinaryData;
+    /**
+     * Small version of the contact photo loaded from a blob instead of from a file. If a large
+     * contact photo is not available yet, then this has the same value as mPhotoBinaryData.
+     */
+    private byte[] mThumbnailPhotoBinaryData;
+    private final boolean mSendToVoicemail;
+    private final String mCustomRingtone;
+    private final boolean mIsUserProfile;
+
+    private final Contact.Status mStatus;
+    private final Exception mException;
+
+    /**
+     * Constructor for special results, namely "no contact found" and "error".
+     */
+    private Contact(Uri requestedUri, Contact.Status status, Exception exception) {
+        if (status == Status.ERROR && exception == null) {
+            throw new IllegalArgumentException("ERROR result must have exception");
+        }
+        mStatus = status;
+        mException = exception;
+        mRequestedUri = requestedUri;
+        mLookupUri = null;
+        mUri = null;
+        mDirectoryId = -1;
+        mLookupKey = null;
+        mId = -1;
+        mRawContacts = null;
+        mStatuses = null;
+        mNameRawContactId = -1;
+        mDisplayNameSource = DisplayNameSources.UNDEFINED;
+        mPhotoId = -1;
+        mPhotoUri = null;
+        mDisplayName = null;
+        mAltDisplayName = null;
+        mPhoneticName = null;
+        mStarred = false;
+        mPresence = null;
+        mInvitableAccountTypes = null;
+        mSendToVoicemail = false;
+        mCustomRingtone = null;
+        mIsUserProfile = false;
+    }
+
+    public static Contact forError(Uri requestedUri, Exception exception) {
+        return new Contact(requestedUri, Status.ERROR, exception);
+    }
+
+    public static Contact forNotFound(Uri requestedUri) {
+        return new Contact(requestedUri, Status.NOT_FOUND, null);
+    }
+
+    /**
+     * Constructor to call when contact was found
+     */
+    public Contact(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey,
+            long id, long nameRawContactId, int displayNameSource, long photoId,
+            String photoUri, String displayName, String altDisplayName, String phoneticName,
+            boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone,
+            boolean isUserProfile) {
+        mStatus = Status.LOADED;
+        mException = null;
+        mRequestedUri = requestedUri;
+        mLookupUri = lookupUri;
+        mUri = uri;
+        mDirectoryId = directoryId;
+        mLookupKey = lookupKey;
+        mId = id;
+        mRawContacts = null;
+        mStatuses = null;
+        mNameRawContactId = nameRawContactId;
+        mDisplayNameSource = displayNameSource;
+        mPhotoId = photoId;
+        mPhotoUri = photoUri;
+        mDisplayName = displayName;
+        mAltDisplayName = altDisplayName;
+        mPhoneticName = phoneticName;
+        mStarred = starred;
+        mPresence = presence;
+        mInvitableAccountTypes = null;
+        mSendToVoicemail = sendToVoicemail;
+        mCustomRingtone = customRingtone;
+        mIsUserProfile = isUserProfile;
+    }
+
+    public Contact(Uri requestedUri, Contact from) {
+        mRequestedUri = requestedUri;
+
+        mStatus = from.mStatus;
+        mException = from.mException;
+        mLookupUri = from.mLookupUri;
+        mUri = from.mUri;
+        mDirectoryId = from.mDirectoryId;
+        mLookupKey = from.mLookupKey;
+        mId = from.mId;
+        mNameRawContactId = from.mNameRawContactId;
+        mDisplayNameSource = from.mDisplayNameSource;
+        mPhotoId = from.mPhotoId;
+        mPhotoUri = from.mPhotoUri;
+        mDisplayName = from.mDisplayName;
+        mAltDisplayName = from.mAltDisplayName;
+        mPhoneticName = from.mPhoneticName;
+        mStarred = from.mStarred;
+        mPresence = from.mPresence;
+        mRawContacts = from.mRawContacts;
+        mStatuses = from.mStatuses;
+        mInvitableAccountTypes = from.mInvitableAccountTypes;
+
+        mDirectoryDisplayName = from.mDirectoryDisplayName;
+        mDirectoryType = from.mDirectoryType;
+        mDirectoryAccountType = from.mDirectoryAccountType;
+        mDirectoryAccountName = from.mDirectoryAccountName;
+        mDirectoryExportSupport = from.mDirectoryExportSupport;
+
+        mGroups = from.mGroups;
+
+        mPhotoBinaryData = from.mPhotoBinaryData;
+        mSendToVoicemail = from.mSendToVoicemail;
+        mCustomRingtone = from.mCustomRingtone;
+        mIsUserProfile = from.mIsUserProfile;
+    }
+
+    /**
+     * @param exportSupport See {@link Directory#EXPORT_SUPPORT}.
+     */
+    public void setDirectoryMetaData(String displayName, String directoryType,
+            String accountType, String accountName, int exportSupport) {
+        mDirectoryDisplayName = displayName;
+        mDirectoryType = directoryType;
+        mDirectoryAccountType = accountType;
+        mDirectoryAccountName = accountName;
+        mDirectoryExportSupport = exportSupport;
+    }
+
+    /* package */ void setPhotoBinaryData(byte[] photoBinaryData) {
+        mPhotoBinaryData = photoBinaryData;
+    }
+
+    /* package */ void setThumbnailPhotoBinaryData(byte[] photoBinaryData) {
+        mThumbnailPhotoBinaryData = photoBinaryData;
+    }
+
+    /**
+     * Returns the URI for the contact that contains both the lookup key and the ID. This is
+     * the best URI to reference a contact.
+     * For directory contacts, this is the same a the URI as returned by {@link #getUri()}
+     */
+    public Uri getLookupUri() {
+        return mLookupUri;
+    }
+
+    public String getLookupKey() {
+        return mLookupKey;
+    }
+
+    /**
+     * Returns the contact Uri that was passed to the provider to make the query. This is
+     * the same as the requested Uri, unless the requested Uri doesn't specify a Contact:
+     * If it either references a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will
+     * always reference the full aggregate contact.
+     */
+    public Uri getUri() {
+        return mUri;
+    }
+
+    /**
+     * Returns the URI for which this {@link ContactLoader) was initially requested.
+     */
+    public Uri getRequestedUri() {
+        return mRequestedUri;
+    }
+
+    /**
+     * Instantiate a new RawContactDeltaList for this contact.
+     */
+    public RawContactDeltaList createRawContactDeltaList() {
+        return RawContactDeltaList.fromIterator(getRawContacts().iterator());
+    }
+
+    /**
+     * Returns the contact ID.
+     */
+    @VisibleForTesting
+    public long getId() {
+        return mId;
+    }
+
+    /**
+     * @return true when an exception happened during loading, in which case
+     *     {@link #getException} returns the actual exception object.
+     *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
+     *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
+     *     and vice versa.
+     */
+    public boolean isError() {
+        return mStatus == Status.ERROR;
+    }
+
+    public Exception getException() {
+        return mException;
+    }
+
+    /**
+     * @return true when the specified contact is not found.
+     *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
+     *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
+     *     and vice versa.
+     */
+    public boolean isNotFound() {
+        return mStatus == Status.NOT_FOUND;
+    }
+
+    /**
+     * @return true if the specified contact is successfully loaded.
+     *     i.e. neither {@link #isError()} nor {@link #isNotFound()}.
+     */
+    public boolean isLoaded() {
+        return mStatus == Status.LOADED;
+    }
+
+    public long getNameRawContactId() {
+        return mNameRawContactId;
+    }
+
+    public int getDisplayNameSource() {
+        return mDisplayNameSource;
+    }
+
+    /**
+     * Used by various classes to determine whether or not this contact should be displayed as
+     * a business rather than a person.
+     */
+    public boolean isDisplayNameFromOrganization() {
+        return DisplayNameSources.ORGANIZATION == mDisplayNameSource;
+    }
+
+    public long getPhotoId() {
+        return mPhotoId;
+    }
+
+    public String getPhotoUri() {
+        return mPhotoUri;
+    }
+
+    public String getDisplayName() {
+        return mDisplayName;
+    }
+
+    public String getAltDisplayName() {
+        return mAltDisplayName;
+    }
+
+    public String getPhoneticName() {
+        return mPhoneticName;
+    }
+
+    public boolean getStarred() {
+        return mStarred;
+    }
+
+    public Integer getPresence() {
+        return mPresence;
+    }
+
+    /**
+     * This can return non-null invitable account types only if the {@link ContactLoader} was
+     * configured to load invitable account types in its constructor.
+     * @return
+     */
+    public ImmutableList<AccountType> getInvitableAccountTypes() {
+        return mInvitableAccountTypes;
+    }
+
+    public ImmutableList<RawContact> getRawContacts() {
+        return mRawContacts;
+    }
+
+    public ImmutableMap<Long, DataStatus> getStatuses() {
+        return mStatuses;
+    }
+
+    public long getDirectoryId() {
+        return mDirectoryId;
+    }
+
+    public boolean isDirectoryEntry() {
+        return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT
+                && mDirectoryId != Directory.LOCAL_INVISIBLE;
+    }
+
+    /**
+     * @return true if this is a contact (not group, etc.) with at least one
+     *         writable raw-contact, and false otherwise.
+     */
+    public boolean isWritableContact(final Context context) {
+        return getFirstWritableRawContactId(context) != -1;
+    }
+
+    /**
+     * Return the ID of the first raw-contact in the contact data that belongs to a
+     * contact-writable account, or -1 if no such entity exists.
+     */
+    public long getFirstWritableRawContactId(final Context context) {
+        // Directory entries are non-writable
+        if (isDirectoryEntry()) return -1;
+
+        // Iterate through raw-contacts; if we find a writable on, return its ID.
+        for (RawContact rawContact : getRawContacts()) {
+            AccountType accountType = rawContact.getAccountType(context);
+            if (accountType != null && accountType.areContactsWritable()) {
+                return rawContact.getId();
+            }
+        }
+        // No writable raw-contact was found.
+        return -1;
+    }
+
+    public int getDirectoryExportSupport() {
+        return mDirectoryExportSupport;
+    }
+
+    public String getDirectoryDisplayName() {
+        return mDirectoryDisplayName;
+    }
+
+    public String getDirectoryType() {
+        return mDirectoryType;
+    }
+
+    public String getDirectoryAccountType() {
+        return mDirectoryAccountType;
+    }
+
+    public String getDirectoryAccountName() {
+        return mDirectoryAccountName;
+    }
+
+    public byte[] getPhotoBinaryData() {
+        return mPhotoBinaryData;
+    }
+
+    public byte[] getThumbnailPhotoBinaryData() {
+        return mThumbnailPhotoBinaryData;
+    }
+
+    public ArrayList<ContentValues> getContentValues() {
+        if (mRawContacts.size() != 1) {
+            throw new IllegalStateException(
+                    "Cannot extract content values from an aggregated contact");
+        }
+
+        RawContact rawContact = mRawContacts.get(0);
+        ArrayList<ContentValues> result = rawContact.getContentValues();
+
+        // If the photo was loaded using the URI, create an entry for the photo
+        // binary data.
+        if (mPhotoId == 0 && mPhotoBinaryData != null) {
+            ContentValues photo = new ContentValues();
+            photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+            photo.put(Photo.PHOTO, mPhotoBinaryData);
+            result.add(photo);
+        }
+
+        return result;
+    }
+
+    /**
+     * This can return non-null group meta-data only if the {@link ContactLoader} was configured to
+     * load group metadata in its constructor.
+     * @return
+     */
+    public ImmutableList<GroupMetaData> getGroupMetaData() {
+        return mGroups;
+    }
+
+    public boolean isSendToVoicemail() {
+        return mSendToVoicemail;
+    }
+
+    public String getCustomRingtone() {
+        return mCustomRingtone;
+    }
+
+    public boolean isUserProfile() {
+        return mIsUserProfile;
+    }
+
+    @Override
+    public String toString() {
+        return "{requested=" + mRequestedUri + ",lookupkey=" + mLookupKey +
+                ",uri=" + mUri + ",status=" + mStatus + "}";
+    }
+
+    /* package */ void setRawContacts(ImmutableList<RawContact> rawContacts) {
+        mRawContacts = rawContacts;
+    }
+
+    /* package */ void setStatuses(ImmutableMap<Long, DataStatus> statuses) {
+        mStatuses = statuses;
+    }
+
+    /* package */ void setInvitableAccountTypes(ImmutableList<AccountType> accountTypes) {
+        mInvitableAccountTypes = accountTypes;
+    }
+
+    /* package */ void setGroupMetaData(ImmutableList<GroupMetaData> groups) {
+        mGroups = groups;
+    }
+}
diff --git a/src/com/android/contacts/common/model/ContactLoader.java b/src/com/android/contacts/common/model/ContactLoader.java
new file mode 100644
index 0000000..f721379
--- /dev/null
+++ b/src/com/android/contacts/common/model/ContactLoader.java
@@ -0,0 +1,1046 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.ContactLoaderUtils;
+import com.android.contacts.common.util.DataStatus;
+import com.android.contacts.common.util.UriUtils;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.android.contacts.common.model.dataitem.PhoneDataItem;
+import com.android.contacts.common.model.dataitem.PhotoDataItem;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Loads a single Contact and all it constituent RawContacts.
+ */
+public class ContactLoader extends AsyncTaskLoader<Contact> {
+
+    private static final String TAG = ContactLoader.class.getSimpleName();
+
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    /** A short-lived cache that can be set by {@link #cacheResult()} */
+    private static Contact sCachedResult = null;
+
+    private final Uri mRequestedUri;
+    private Uri mLookupUri;
+    private boolean mLoadGroupMetaData;
+    private boolean mLoadInvitableAccountTypes;
+    private boolean mPostViewNotification;
+    private boolean mComputeFormattedPhoneNumber;
+    private Contact mContact;
+    private ForceLoadContentObserver mObserver;
+    private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
+
+    public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) {
+        this(context, lookupUri, false, false, postViewNotification, false);
+    }
+
+    public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
+            boolean loadInvitableAccountTypes,
+            boolean postViewNotification, boolean computeFormattedPhoneNumber) {
+        super(context);
+        mLookupUri = lookupUri;
+        mRequestedUri = lookupUri;
+        mLoadGroupMetaData = loadGroupMetaData;
+        mLoadInvitableAccountTypes = loadInvitableAccountTypes;
+        mPostViewNotification = postViewNotification;
+        mComputeFormattedPhoneNumber = computeFormattedPhoneNumber;
+    }
+
+    /**
+     * Projection used for the query that loads all data for the entire contact (except for
+     * social stream items).
+     */
+    private static class ContactQuery {
+        static final String[] COLUMNS_INTERNAL = new String[] {
+                Contacts.NAME_RAW_CONTACT_ID,
+                Contacts.DISPLAY_NAME_SOURCE,
+                Contacts.LOOKUP_KEY,
+                Contacts.DISPLAY_NAME,
+                Contacts.DISPLAY_NAME_ALTERNATIVE,
+                Contacts.PHONETIC_NAME,
+                Contacts.PHOTO_ID,
+                Contacts.STARRED,
+                Contacts.CONTACT_PRESENCE,
+                Contacts.CONTACT_STATUS,
+                Contacts.CONTACT_STATUS_TIMESTAMP,
+                Contacts.CONTACT_STATUS_RES_PACKAGE,
+                Contacts.CONTACT_STATUS_LABEL,
+                Contacts.Entity.CONTACT_ID,
+                Contacts.Entity.RAW_CONTACT_ID,
+
+                RawContacts.ACCOUNT_NAME,
+                RawContacts.ACCOUNT_TYPE,
+                RawContacts.DATA_SET,
+                RawContacts.DIRTY,
+                RawContacts.VERSION,
+                RawContacts.SOURCE_ID,
+                RawContacts.SYNC1,
+                RawContacts.SYNC2,
+                RawContacts.SYNC3,
+                RawContacts.SYNC4,
+                RawContacts.DELETED,
+
+                Contacts.Entity.DATA_ID,
+                Data.DATA1,
+                Data.DATA2,
+                Data.DATA3,
+                Data.DATA4,
+                Data.DATA5,
+                Data.DATA6,
+                Data.DATA7,
+                Data.DATA8,
+                Data.DATA9,
+                Data.DATA10,
+                Data.DATA11,
+                Data.DATA12,
+                Data.DATA13,
+                Data.DATA14,
+                Data.DATA15,
+                Data.SYNC1,
+                Data.SYNC2,
+                Data.SYNC3,
+                Data.SYNC4,
+                Data.DATA_VERSION,
+                Data.IS_PRIMARY,
+                Data.IS_SUPER_PRIMARY,
+                Data.MIMETYPE,
+
+                GroupMembership.GROUP_SOURCE_ID,
+
+                Data.PRESENCE,
+                Data.CHAT_CAPABILITY,
+                Data.STATUS,
+                Data.STATUS_RES_PACKAGE,
+                Data.STATUS_ICON,
+                Data.STATUS_LABEL,
+                Data.STATUS_TIMESTAMP,
+
+                Contacts.PHOTO_URI,
+                Contacts.SEND_TO_VOICEMAIL,
+                Contacts.CUSTOM_RINGTONE,
+                Contacts.IS_USER_PROFILE,
+
+                Data.TIMES_USED,
+                Data.LAST_TIME_USED
+        };
+
+        static final String[] COLUMNS;
+
+        static {
+            List<String> projectionList = Lists.newArrayList(COLUMNS_INTERNAL);
+            if (CompatUtils.isMarshmallowCompatible()) {
+                projectionList.add(Data.CARRIER_PRESENCE);
+            }
+            COLUMNS = projectionList.toArray(new String[projectionList.size()]);
+        }
+
+        public static final int NAME_RAW_CONTACT_ID = 0;
+        public static final int DISPLAY_NAME_SOURCE = 1;
+        public static final int LOOKUP_KEY = 2;
+        public static final int DISPLAY_NAME = 3;
+        public static final int ALT_DISPLAY_NAME = 4;
+        public static final int PHONETIC_NAME = 5;
+        public static final int PHOTO_ID = 6;
+        public static final int STARRED = 7;
+        public static final int CONTACT_PRESENCE = 8;
+        public static final int CONTACT_STATUS = 9;
+        public static final int CONTACT_STATUS_TIMESTAMP = 10;
+        public static final int CONTACT_STATUS_RES_PACKAGE = 11;
+        public static final int CONTACT_STATUS_LABEL = 12;
+        public static final int CONTACT_ID = 13;
+        public static final int RAW_CONTACT_ID = 14;
+
+        public static final int ACCOUNT_NAME = 15;
+        public static final int ACCOUNT_TYPE = 16;
+        public static final int DATA_SET = 17;
+        public static final int DIRTY = 18;
+        public static final int VERSION = 19;
+        public static final int SOURCE_ID = 20;
+        public static final int SYNC1 = 21;
+        public static final int SYNC2 = 22;
+        public static final int SYNC3 = 23;
+        public static final int SYNC4 = 24;
+        public static final int DELETED = 25;
+
+        public static final int DATA_ID = 26;
+        public static final int DATA1 = 27;
+        public static final int DATA2 = 28;
+        public static final int DATA3 = 29;
+        public static final int DATA4 = 30;
+        public static final int DATA5 = 31;
+        public static final int DATA6 = 32;
+        public static final int DATA7 = 33;
+        public static final int DATA8 = 34;
+        public static final int DATA9 = 35;
+        public static final int DATA10 = 36;
+        public static final int DATA11 = 37;
+        public static final int DATA12 = 38;
+        public static final int DATA13 = 39;
+        public static final int DATA14 = 40;
+        public static final int DATA15 = 41;
+        public static final int DATA_SYNC1 = 42;
+        public static final int DATA_SYNC2 = 43;
+        public static final int DATA_SYNC3 = 44;
+        public static final int DATA_SYNC4 = 45;
+        public static final int DATA_VERSION = 46;
+        public static final int IS_PRIMARY = 47;
+        public static final int IS_SUPERPRIMARY = 48;
+        public static final int MIMETYPE = 49;
+
+        public static final int GROUP_SOURCE_ID = 50;
+
+        public static final int PRESENCE = 51;
+        public static final int CHAT_CAPABILITY = 52;
+        public static final int STATUS = 53;
+        public static final int STATUS_RES_PACKAGE = 54;
+        public static final int STATUS_ICON = 55;
+        public static final int STATUS_LABEL = 56;
+        public static final int STATUS_TIMESTAMP = 57;
+
+        public static final int PHOTO_URI = 58;
+        public static final int SEND_TO_VOICEMAIL = 59;
+        public static final int CUSTOM_RINGTONE = 60;
+        public static final int IS_USER_PROFILE = 61;
+
+        public static final int TIMES_USED = 62;
+        public static final int LAST_TIME_USED = 63;
+        public static final int CARRIER_PRESENCE = 64;
+    }
+
+    /**
+     * Projection used for the query that loads all data for the entire contact.
+     */
+    private static class DirectoryQuery {
+        static final String[] COLUMNS = new String[] {
+            Directory.DISPLAY_NAME,
+            Directory.PACKAGE_NAME,
+            Directory.TYPE_RESOURCE_ID,
+            Directory.ACCOUNT_TYPE,
+            Directory.ACCOUNT_NAME,
+            Directory.EXPORT_SUPPORT,
+        };
+
+        public static final int DISPLAY_NAME = 0;
+        public static final int PACKAGE_NAME = 1;
+        public static final int TYPE_RESOURCE_ID = 2;
+        public static final int ACCOUNT_TYPE = 3;
+        public static final int ACCOUNT_NAME = 4;
+        public static final int EXPORT_SUPPORT = 5;
+    }
+
+    private static class GroupQuery {
+        static final String[] COLUMNS = new String[] {
+            Groups.ACCOUNT_NAME,
+            Groups.ACCOUNT_TYPE,
+            Groups.DATA_SET,
+            Groups._ID,
+            Groups.TITLE,
+            Groups.AUTO_ADD,
+            Groups.FAVORITES,
+        };
+
+        public static final int ACCOUNT_NAME = 0;
+        public static final int ACCOUNT_TYPE = 1;
+        public static final int DATA_SET = 2;
+        public static final int ID = 3;
+        public static final int TITLE = 4;
+        public static final int AUTO_ADD = 5;
+        public static final int FAVORITES = 6;
+    }
+
+    public void setLookupUri(Uri lookupUri) {
+        mLookupUri = lookupUri;
+    }
+
+    @Override
+    public Contact loadInBackground() {
+        Log.e(TAG, "loadInBackground=" + mLookupUri);
+        try {
+            final ContentResolver resolver = getContext().getContentResolver();
+            final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(
+                    resolver, mLookupUri);
+            final Contact cachedResult = sCachedResult;
+            sCachedResult = null;
+            // Is this the same Uri as what we had before already? In that case, reuse that result
+            final Contact result;
+            final boolean resultIsCached;
+            if (cachedResult != null &&
+                    UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) {
+                // We are using a cached result from earlier. Below, we should make sure
+                // we are not doing any more network or disc accesses
+                result = new Contact(mRequestedUri, cachedResult);
+                resultIsCached = true;
+            } else {
+                if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) {
+                    result = loadEncodedContactEntity(uriCurrentFormat, mLookupUri);
+                } else {
+                    result = loadContactEntity(resolver, uriCurrentFormat);
+                }
+                resultIsCached = false;
+            }
+            if (result.isLoaded()) {
+                if (result.isDirectoryEntry()) {
+                    if (!resultIsCached) {
+                        loadDirectoryMetaData(result);
+                    }
+                } else if (mLoadGroupMetaData) {
+                    if (result.getGroupMetaData() == null) {
+                        loadGroupMetaData(result);
+                    }
+                }
+                if (mComputeFormattedPhoneNumber) {
+                    computeFormattedPhoneNumbers(result);
+                }
+                if (!resultIsCached) loadPhotoBinaryData(result);
+
+                // Note ME profile should never have "Add connection"
+                if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) {
+                    loadInvitableAccountTypes(result);
+                }
+            }
+            return result;
+        } catch (Exception e) {
+            Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
+            return Contact.forError(mRequestedUri, e);
+        }
+    }
+
+    /**
+     * Parses a {@link Contact} stored as a JSON string in a lookup URI.
+     *
+     * @param lookupUri The contact information to parse .
+     * @return The parsed {@code Contact} information.
+     * @throws JSONException
+     */
+    public static Contact parseEncodedContactEntity(Uri lookupUri)  {
+        try {
+            return loadEncodedContactEntity(lookupUri, lookupUri);
+        } catch (JSONException je) {
+            return null;
+        }
+    }
+
+    private static Contact loadEncodedContactEntity(Uri uri, Uri lookupUri) throws JSONException {
+        final String jsonString = uri.getEncodedFragment();
+        final JSONObject json = new JSONObject(jsonString);
+
+        final long directoryId =
+                Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY));
+
+        final String displayName = json.optString(Contacts.DISPLAY_NAME);
+        final String altDisplayName = json.optString(
+                Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
+        final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE);
+        final String photoUri = json.optString(Contacts.PHOTO_URI, null);
+        final Contact contact = new Contact(
+                uri, uri,
+                lookupUri,
+                directoryId,
+                null /* lookupKey */,
+                -1 /* id */,
+                -1 /* nameRawContactId */,
+                displayNameSource,
+                0 /* photoId */,
+                photoUri,
+                displayName,
+                altDisplayName,
+                null /* phoneticName */,
+                false /* starred */,
+                null /* presence */,
+                false /* sendToVoicemail */,
+                null /* customRingtone */,
+                false /* isUserProfile */);
+
+        contact.setStatuses(new ImmutableMap.Builder<Long, DataStatus>().build());
+
+        final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null);
+        final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME);
+        if (accountName != null) {
+            final String accountType = json.getString(RawContacts.ACCOUNT_TYPE);
+            contact.setDirectoryMetaData(directoryName, null, accountName, accountType,
+                    json.optInt(Directory.EXPORT_SUPPORT,
+                            Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY));
+        } else {
+            contact.setDirectoryMetaData(directoryName, null, null, null,
+                    json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT));
+        }
+
+        final ContentValues values = new ContentValues();
+        values.put(Data._ID, -1);
+        values.put(Data.CONTACT_ID, -1);
+        final RawContact rawContact = new RawContact(values);
+
+        final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
+        final Iterator keys = items.keys();
+        while (keys.hasNext()) {
+            final String mimetype = (String) keys.next();
+
+            // Could be single object or array.
+            final JSONObject obj = items.optJSONObject(mimetype);
+            if (obj == null) {
+                final JSONArray array = items.getJSONArray(mimetype);
+                for (int i = 0; i < array.length(); i++) {
+                    final JSONObject item = array.getJSONObject(i);
+                    processOneRecord(rawContact, item, mimetype);
+                }
+            } else {
+                processOneRecord(rawContact, obj, mimetype);
+            }
+        }
+
+        contact.setRawContacts(new ImmutableList.Builder<RawContact>()
+                .add(rawContact)
+                .build());
+        return contact;
+    }
+
+    private static void processOneRecord(RawContact rawContact, JSONObject item, String mimetype)
+            throws JSONException {
+        final ContentValues itemValues = new ContentValues();
+        itemValues.put(Data.MIMETYPE, mimetype);
+        itemValues.put(Data._ID, -1);
+
+        final Iterator iterator = item.keys();
+        while (iterator.hasNext()) {
+            String name = (String) iterator.next();
+            final Object o = item.get(name);
+            if (o instanceof String) {
+                itemValues.put(name, (String) o);
+            } else if (o instanceof Integer) {
+                itemValues.put(name, (Integer) o);
+            }
+        }
+        rawContact.addDataItemValues(itemValues);
+    }
+
+    private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) {
+        Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
+        Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null,
+                Contacts.Entity.RAW_CONTACT_ID);
+        if (cursor == null) {
+            Log.e(TAG, "No cursor returned in loadContactEntity");
+            return Contact.forNotFound(mRequestedUri);
+        }
+
+        try {
+            if (!cursor.moveToFirst()) {
+                cursor.close();
+                return Contact.forNotFound(mRequestedUri);
+            }
+
+            // Create the loaded contact starting with the header data.
+            Contact contact = loadContactHeaderData(cursor, contactUri);
+
+            // Fill in the raw contacts, which is wrapped in an Entity and any
+            // status data.  Initially, result has empty entities and statuses.
+            long currentRawContactId = -1;
+            RawContact rawContact = null;
+            ImmutableList.Builder<RawContact> rawContactsBuilder =
+                    new ImmutableList.Builder<RawContact>();
+            ImmutableMap.Builder<Long, DataStatus> statusesBuilder =
+                    new ImmutableMap.Builder<Long, DataStatus>();
+            do {
+                long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
+                if (rawContactId != currentRawContactId) {
+                    // First time to see this raw contact id, so create a new entity, and
+                    // add it to the result's entities.
+                    currentRawContactId = rawContactId;
+                    rawContact = new RawContact(loadRawContactValues(cursor));
+                    rawContactsBuilder.add(rawContact);
+                }
+                if (!cursor.isNull(ContactQuery.DATA_ID)) {
+                    ContentValues data = loadDataValues(cursor);
+                    rawContact.addDataItemValues(data);
+
+                    if (!cursor.isNull(ContactQuery.PRESENCE)
+                            || !cursor.isNull(ContactQuery.STATUS)) {
+                        final DataStatus status = new DataStatus(cursor);
+                        final long dataId = cursor.getLong(ContactQuery.DATA_ID);
+                        statusesBuilder.put(dataId, status);
+                    }
+                }
+            } while (cursor.moveToNext());
+
+            contact.setRawContacts(rawContactsBuilder.build());
+            contact.setStatuses(statusesBuilder.build());
+
+            return contact;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Looks for the photo data item in entities. If found, a thumbnail will be stored. A larger
+     * photo will also be stored if available.
+     */
+    private void loadPhotoBinaryData(Contact contactData) {
+        loadThumbnailBinaryData(contactData);
+
+        // Try to load the large photo from a file using the photo URI.
+        String photoUri = contactData.getPhotoUri();
+        if (photoUri != null) {
+            try {
+                final InputStream inputStream;
+                final AssetFileDescriptor fd;
+                final Uri uri = Uri.parse(photoUri);
+                final String scheme = uri.getScheme();
+                if ("http".equals(scheme) || "https".equals(scheme)) {
+                    // Support HTTP urls that might come from extended directories
+                    inputStream = new URL(photoUri).openStream();
+                    fd = null;
+                } else {
+                    fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r");
+                    inputStream = fd.createInputStream();
+                }
+                byte[] buffer = new byte[16 * 1024];
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                try {
+                    int size;
+                    while ((size = inputStream.read(buffer)) != -1) {
+                        baos.write(buffer, 0, size);
+                    }
+                    contactData.setPhotoBinaryData(baos.toByteArray());
+                } finally {
+                    inputStream.close();
+                    if (fd != null) {
+                        fd.close();
+                    }
+                }
+                return;
+            } catch (IOException ioe) {
+                // Just fall back to the case below.
+            }
+        }
+
+        // If we couldn't load from a file, fall back to the data blob.
+        contactData.setPhotoBinaryData(contactData.getThumbnailPhotoBinaryData());
+    }
+
+    private void loadThumbnailBinaryData(Contact contactData) {
+        final long photoId = contactData.getPhotoId();
+        if (photoId <= 0) {
+            // No photo ID
+            return;
+        }
+
+        for (RawContact rawContact : contactData.getRawContacts()) {
+            for (DataItem dataItem : rawContact.getDataItems()) {
+                if (dataItem.getId() == photoId) {
+                    if (!(dataItem instanceof PhotoDataItem)) {
+                        break;
+                    }
+
+                    final PhotoDataItem photo = (PhotoDataItem) dataItem;
+                    contactData.setThumbnailPhotoBinaryData(photo.getPhoto());
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}.
+     */
+    private void loadInvitableAccountTypes(Contact contactData) {
+        final ImmutableList.Builder<AccountType> resultListBuilder =
+                new ImmutableList.Builder<AccountType>();
+        if (!contactData.isUserProfile()) {
+            Map<AccountTypeWithDataSet, AccountType> invitables =
+                    AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
+            if (!invitables.isEmpty()) {
+                final Map<AccountTypeWithDataSet, AccountType> resultMap =
+                        Maps.newHashMap(invitables);
+
+                // Remove the ones that already have a raw contact in the current contact
+                for (RawContact rawContact : contactData.getRawContacts()) {
+                    final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
+                            rawContact.getAccountTypeString(),
+                            rawContact.getDataSet());
+                    resultMap.remove(type);
+                }
+
+                resultListBuilder.addAll(resultMap.values());
+            }
+        }
+
+        // Set to mInvitableAccountTypes
+        contactData.setInvitableAccountTypes(resultListBuilder.build());
+    }
+
+    /**
+     * Extracts Contact level columns from the cursor.
+     */
+    private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) {
+        final String directoryParameter =
+                contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+        final long directoryId = directoryParameter == null
+                ? Directory.DEFAULT
+                : Long.parseLong(directoryParameter);
+        final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+        final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
+        final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
+        final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
+        final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
+        final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
+        final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
+        final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+        final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
+        final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
+        final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
+                ? null
+                : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
+        final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
+        final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
+        final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
+
+        Uri lookupUri;
+        if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
+            lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
+        } else {
+            lookupUri = contactUri;
+        }
+
+        return new Contact(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey,
+                contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName,
+                altDisplayName, phoneticName, starred, presence, sendToVoicemail,
+                customRingtone, isUserProfile);
+    }
+
+    /**
+     * Extracts RawContact level columns from the cursor.
+     */
+    private ContentValues loadRawContactValues(Cursor cursor) {
+        ContentValues cv = new ContentValues();
+
+        cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
+
+        cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
+
+        return cv;
+    }
+
+    /**
+     * Extracts Data level columns from the cursor.
+     */
+    private ContentValues loadDataValues(Cursor cursor) {
+        ContentValues cv = new ContentValues();
+
+        cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
+
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.TIMES_USED);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.LAST_TIME_USED);
+        if (CompatUtils.isMarshmallowCompatible()) {
+            cursorColumnToContentValues(cursor, cv, ContactQuery.CARRIER_PRESENCE);
+        }
+
+        return cv;
+    }
+
+    private void cursorColumnToContentValues(
+            Cursor cursor, ContentValues values, int index) {
+        switch (cursor.getType(index)) {
+            case Cursor.FIELD_TYPE_NULL:
+                // don't put anything in the content values
+                break;
+            case Cursor.FIELD_TYPE_INTEGER:
+                values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
+                break;
+            case Cursor.FIELD_TYPE_STRING:
+                values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
+                break;
+            case Cursor.FIELD_TYPE_BLOB:
+                values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
+                break;
+            default:
+                throw new IllegalStateException("Invalid or unhandled data type");
+        }
+    }
+
+    private void loadDirectoryMetaData(Contact result) {
+        long directoryId = result.getDirectoryId();
+
+        Cursor cursor = getContext().getContentResolver().query(
+                ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
+                DirectoryQuery.COLUMNS, null, null, null);
+        if (cursor == null) {
+            return;
+        }
+        try {
+            if (cursor.moveToFirst()) {
+                final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+                final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+                final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+                final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
+                final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
+                final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
+                String directoryType = null;
+                if (!TextUtils.isEmpty(packageName)) {
+                    PackageManager pm = getContext().getPackageManager();
+                    try {
+                        Resources resources = pm.getResourcesForApplication(packageName);
+                        directoryType = resources.getString(typeResourceId);
+                    } catch (NameNotFoundException e) {
+                        Log.w(TAG, "Contact directory resource not found: "
+                                + packageName + "." + typeResourceId);
+                    }
+                }
+
+                result.setDirectoryMetaData(
+                        displayName, directoryType, accountType, accountName, exportSupport);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    static private class AccountKey {
+        private final String mAccountName;
+        private final String mAccountType;
+        private final String mDataSet;
+
+        public AccountKey(String accountName, String accountType, String dataSet) {
+            mAccountName = accountName;
+            mAccountType = accountType;
+            mDataSet = dataSet;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mAccountName, mAccountType, mDataSet);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof AccountKey)) {
+                return false;
+            }
+            final AccountKey other = (AccountKey) obj;
+            return Objects.equals(mAccountName, other.mAccountName)
+                && Objects.equals(mAccountType, other.mAccountType)
+                && Objects.equals(mDataSet, other.mDataSet);
+        }
+    }
+
+    /**
+     * Loads groups meta-data for all groups associated with all constituent raw contacts'
+     * accounts.
+     */
+    private void loadGroupMetaData(Contact result) {
+        StringBuilder selection = new StringBuilder();
+        ArrayList<String> selectionArgs = new ArrayList<String>();
+        final HashSet<AccountKey> accountsSeen = new HashSet<>();
+        for (RawContact rawContact : result.getRawContacts()) {
+            final String accountName = rawContact.getAccountName();
+            final String accountType = rawContact.getAccountTypeString();
+            final String dataSet = rawContact.getDataSet();
+            final AccountKey accountKey = new AccountKey(accountName, accountType, dataSet);
+            if (accountName != null && accountType != null &&
+                    !accountsSeen.contains(accountKey)) {
+                accountsSeen.add(accountKey);
+                if (selection.length() != 0) {
+                    selection.append(" OR ");
+                }
+                selection.append(
+                        "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
+                selectionArgs.add(accountName);
+                selectionArgs.add(accountType);
+
+                selection.append(" AND " + Groups.DELETED + "=0");
+
+                if (dataSet != null) {
+                    selection.append(" AND " + Groups.DATA_SET + "=?");
+                    selectionArgs.add(dataSet);
+                } else {
+                    selection.append(" AND " + Groups.DATA_SET + " IS NULL");
+                }
+                selection.append(")");
+            }
+        }
+        final ImmutableList.Builder<GroupMetaData> groupListBuilder =
+                new ImmutableList.Builder<GroupMetaData>();
+        final Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI,
+                GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]),
+                null);
+        if (cursor != null) {
+            try {
+                while (cursor.moveToNext()) {
+                    final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
+                    final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
+                    final String dataSet = cursor.getString(GroupQuery.DATA_SET);
+                    final long groupId = cursor.getLong(GroupQuery.ID);
+                    final String title = cursor.getString(GroupQuery.TITLE);
+                    final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD)
+                            ? false
+                            : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
+                    final boolean favorites = cursor.isNull(GroupQuery.FAVORITES)
+                            ? false
+                            : cursor.getInt(GroupQuery.FAVORITES) != 0;
+
+                    groupListBuilder.add(new GroupMetaData(
+                                    accountName, accountType, dataSet, groupId, title, defaultGroup,
+                                    favorites));
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+        result.setGroupMetaData(groupListBuilder.build());
+    }
+
+    /**
+     * Iterates over all data items that represent phone numbers are tries to calculate a formatted
+     * number. This function can safely be called several times as no unformatted data is
+     * overwritten
+     */
+    private void computeFormattedPhoneNumbers(Contact contactData) {
+        final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
+        final ImmutableList<RawContact> rawContacts = contactData.getRawContacts();
+        final int rawContactCount = rawContacts.size();
+        for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) {
+            final RawContact rawContact = rawContacts.get(rawContactIndex);
+            final List<DataItem> dataItems = rawContact.getDataItems();
+            final int dataCount = dataItems.size();
+            for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) {
+                final DataItem dataItem = dataItems.get(dataIndex);
+                if (dataItem instanceof PhoneDataItem) {
+                    final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem;
+                    phoneDataItem.computeFormattedPhoneNumber(countryIso);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void deliverResult(Contact result) {
+        unregisterObserver();
+
+        // The creator isn't interested in any further updates
+        if (isReset() || result == null) {
+            return;
+        }
+
+        mContact = result;
+
+        if (result.isLoaded()) {
+            mLookupUri = result.getLookupUri();
+
+            if (!result.isDirectoryEntry()) {
+                Log.i(TAG, "Registering content observer for " + mLookupUri);
+                if (mObserver == null) {
+                    mObserver = new ForceLoadContentObserver();
+                }
+                getContext().getContentResolver().registerContentObserver(
+                        mLookupUri, true, mObserver);
+            }
+
+            if (mPostViewNotification) {
+                // inform the source of the data that this contact is being looked at
+                postViewNotificationToSyncAdapter();
+            }
+        }
+
+        super.deliverResult(mContact);
+    }
+
+    /**
+     * Posts a message to the contributing sync adapters that have opted-in, notifying them
+     * that the contact has just been loaded
+     */
+    private void postViewNotificationToSyncAdapter() {
+        Context context = getContext();
+        for (RawContact rawContact : mContact.getRawContacts()) {
+            final long rawContactId = rawContact.getId();
+            if (mNotifiedRawContactIds.contains(rawContactId)) {
+                continue; // Already notified for this raw contact.
+            }
+            mNotifiedRawContactIds.add(rawContactId);
+            final AccountType accountType = rawContact.getAccountType(context);
+            final String serviceName = accountType.getViewContactNotifyServiceClassName();
+            final String servicePackageName = accountType.getViewContactNotifyServicePackageName();
+            if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
+                final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+                final Intent intent = new Intent();
+                intent.setClassName(servicePackageName, serviceName);
+                intent.setAction(Intent.ACTION_VIEW);
+                intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE);
+                try {
+                    context.startService(intent);
+                } catch (Exception e) {
+                    Log.e(TAG, "Error sending message to source-app", e);
+                }
+            }
+        }
+    }
+
+    private void unregisterObserver() {
+        if (mObserver != null) {
+            getContext().getContentResolver().unregisterContentObserver(mObserver);
+            mObserver = null;
+        }
+    }
+
+    /**
+     * Fully upgrades this ContactLoader to one with all lists fully loaded. When done, the
+     * new result will be delivered
+     */
+    public void upgradeToFullContact() {
+        // Everything requested already? Nothing to do, so let's bail out
+        if (mLoadGroupMetaData && mLoadInvitableAccountTypes
+                && mPostViewNotification && mComputeFormattedPhoneNumber) return;
+
+        mLoadGroupMetaData = true;
+        mLoadInvitableAccountTypes = true;
+        mPostViewNotification = true;
+        mComputeFormattedPhoneNumber = true;
+
+        // Cache the current result, so that we only load the "missing" parts of the contact.
+        cacheResult();
+
+        // Our load parameters have changed, so let's pretend the data has changed. Its the same
+        // thing, essentially.
+        onContentChanged();
+    }
+
+    public Uri getLookupUri() {
+        return mLookupUri;
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (mContact != null) {
+            deliverResult(mContact);
+        }
+
+        if (takeContentChanged() || mContact == null) {
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        cancelLoad();
+    }
+
+    @Override
+    protected void onReset() {
+        super.onReset();
+        cancelLoad();
+        unregisterObserver();
+        mContact = null;
+    }
+
+    /**
+     * Caches the result, which is useful when we switch from activity to activity, using the same
+     * contact. If the next load is for a different contact, the cached result will be dropped
+     */
+    public void cacheResult() {
+        if (mContact == null || !mContact.isLoaded()) {
+            sCachedResult = null;
+        } else {
+            sCachedResult = mContact;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/model/RawContact.java b/src/com/android/contacts/common/model/RawContact.java
new file mode 100644
index 0000000..3d8db85
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContact.java
@@ -0,0 +1,368 @@
+/*
+ * 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.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * RawContact represents a single raw contact in the raw contacts database.
+ * It has specialized getters/setters for raw contact
+ * items, and also contains a collection of DataItem objects.  A RawContact contains the information
+ * from a single account.
+ *
+ * This allows RawContact objects to be thought of as a class with raw contact
+ * fields (like account type, name, data set, sync state, etc.) and a list of
+ * DataItem objects that represent contact information elements (like phone
+ * numbers, email, address, etc.).
+ */
+final public class RawContact implements Parcelable {
+
+    private AccountTypeManager mAccountTypeManager;
+    private final ContentValues mValues;
+    private final ArrayList<NamedDataItem> mDataItems;
+
+    final public static class NamedDataItem implements Parcelable {
+        public final Uri mUri;
+
+        // This use to be a DataItem. DataItem creation is now delayed until the point of request
+        // since there is no benefit to storing them here due to the multiple inheritance.
+        // Eventually instanceof still has to be used anyways to determine which sub-class of
+        // DataItem it is. And having parent DataItem's here makes it very difficult to serialize or
+        // parcelable.
+        //
+        // Instead of having a common DataItem super class, we should refactor this to be a generic
+        // Object where the object is a concrete class that no longer relies on ContentValues.
+        // (this will also make the classes easier to use).
+        // Since instanceof is used later anyways, having a list of Objects won't hurt and is no
+        // worse than having a DataItem.
+        public final ContentValues mContentValues;
+
+        public NamedDataItem(Uri uri, ContentValues values) {
+            this.mUri = uri;
+            this.mContentValues = values;
+        }
+
+        public NamedDataItem(Parcel parcel) {
+            this.mUri = parcel.readParcelable(Uri.class.getClassLoader());
+            this.mContentValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel parcel, int i) {
+            parcel.writeParcelable(mUri, i);
+            parcel.writeParcelable(mContentValues, i);
+        }
+
+        public static final Parcelable.Creator<NamedDataItem> CREATOR
+                = new Parcelable.Creator<NamedDataItem>() {
+
+            @Override
+            public NamedDataItem createFromParcel(Parcel parcel) {
+                return new NamedDataItem(parcel);
+            }
+
+            @Override
+            public NamedDataItem[] newArray(int i) {
+                return new NamedDataItem[i];
+            }
+        };
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(mUri, mContentValues);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) return false;
+            if (getClass() != obj.getClass()) return false;
+
+            final NamedDataItem other = (NamedDataItem) obj;
+            return Objects.equal(mUri, other.mUri) &&
+                    Objects.equal(mContentValues, other.mContentValues);
+        }
+    }
+
+    public static RawContact createFrom(Entity entity) {
+        final ContentValues values = entity.getEntityValues();
+        final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();
+
+        RawContact rawContact = new RawContact(values);
+        for (Entity.NamedContentValues subValue : subValues) {
+            rawContact.addNamedDataItemValues(subValue.uri, subValue.values);
+        }
+        return rawContact;
+    }
+
+    /**
+     * A RawContact object can be created with or without a context.
+     */
+    public RawContact() {
+        this(new ContentValues());
+    }
+
+    public RawContact(ContentValues values) {
+        mValues = values;
+        mDataItems = new ArrayList<NamedDataItem>();
+    }
+
+    /**
+     * Constructor for the parcelable.
+     *
+     * @param parcel The parcel to de-serialize from.
+     */
+    private RawContact(Parcel parcel) {
+        mValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+        mDataItems = Lists.newArrayList();
+        parcel.readTypedList(mDataItems, NamedDataItem.CREATOR);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int i) {
+        parcel.writeParcelable(mValues, i);
+        parcel.writeTypedList(mDataItems);
+    }
+
+    /**
+     * Create for building the parcelable.
+     */
+    public static final Parcelable.Creator<RawContact> CREATOR
+            = new Parcelable.Creator<RawContact>() {
+
+        @Override
+        public RawContact createFromParcel(Parcel parcel) {
+            return new RawContact(parcel);
+        }
+
+        @Override
+        public RawContact[] newArray(int i) {
+            return new RawContact[i];
+        }
+    };
+
+    public AccountTypeManager getAccountTypeManager(Context context) {
+        if (mAccountTypeManager == null) {
+            mAccountTypeManager = AccountTypeManager.getInstance(context);
+        }
+        return mAccountTypeManager;
+    }
+
+    public ContentValues getValues() {
+        return mValues;
+    }
+
+    /**
+     * Returns the id of the raw contact.
+     */
+    public Long getId() {
+        return getValues().getAsLong(RawContacts._ID);
+    }
+
+    /**
+     * Returns the account name of the raw contact.
+     */
+    public String getAccountName() {
+        return getValues().getAsString(RawContacts.ACCOUNT_NAME);
+    }
+
+    /**
+     * Returns the account type of the raw contact.
+     */
+    public String getAccountTypeString() {
+        return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+    }
+
+    /**
+     * Returns the data set of the raw contact.
+     */
+    public String getDataSet() {
+        return getValues().getAsString(RawContacts.DATA_SET);
+    }
+
+    public boolean isDirty() {
+        return getValues().getAsBoolean(RawContacts.DIRTY);
+    }
+
+    public String getSourceId() {
+        return getValues().getAsString(RawContacts.SOURCE_ID);
+    }
+
+    public String getSync1() {
+        return getValues().getAsString(RawContacts.SYNC1);
+    }
+
+    public String getSync2() {
+        return getValues().getAsString(RawContacts.SYNC2);
+    }
+
+    public String getSync3() {
+        return getValues().getAsString(RawContacts.SYNC3);
+    }
+
+    public String getSync4() {
+        return getValues().getAsString(RawContacts.SYNC4);
+    }
+
+    public boolean isDeleted() {
+        return getValues().getAsBoolean(RawContacts.DELETED);
+    }
+
+    public long getContactId() {
+        return getValues().getAsLong(Contacts.Entity.CONTACT_ID);
+    }
+
+    public boolean isStarred() {
+        return getValues().getAsBoolean(Contacts.STARRED);
+    }
+
+    public AccountType getAccountType(Context context) {
+        return getAccountTypeManager(context).getAccountType(getAccountTypeString(), getDataSet());
+    }
+
+    /**
+     * Sets the account name, account type, and data set strings.
+     * Valid combinations for account-name, account-type, data-set
+     * 1) null, null, null (local account)
+     * 2) non-null, non-null, null (valid account without data-set)
+     * 3) non-null, non-null, non-null (valid account with data-set)
+     */
+    private void setAccount(String accountName, String accountType, String dataSet) {
+        final ContentValues values = getValues();
+        if (accountName == null) {
+            if (accountType == null && dataSet == null) {
+                // This is a local account
+                values.putNull(RawContacts.ACCOUNT_NAME);
+                values.putNull(RawContacts.ACCOUNT_TYPE);
+                values.putNull(RawContacts.DATA_SET);
+                return;
+            }
+        } else {
+            if (accountType != null) {
+                // This is a valid account, either with or without a dataSet.
+                values.put(RawContacts.ACCOUNT_NAME, accountName);
+                values.put(RawContacts.ACCOUNT_TYPE, accountType);
+                if (dataSet == null) {
+                    values.putNull(RawContacts.DATA_SET);
+                } else {
+                    values.put(RawContacts.DATA_SET, dataSet);
+                }
+                return;
+            }
+        }
+        throw new IllegalArgumentException(
+                "Not a valid combination of account name, type, and data set.");
+    }
+
+    public void setAccount(AccountWithDataSet accountWithDataSet) {
+        if (accountWithDataSet != null) {
+            setAccount(accountWithDataSet.name, accountWithDataSet.type,
+                    accountWithDataSet.dataSet);
+        } else {
+            setAccount(null, null, null);
+        }
+    }
+
+    public void setAccountToLocal() {
+        setAccount(null, null, null);
+    }
+
+    /**
+     * Creates and inserts a DataItem object that wraps the content values, and returns it.
+     */
+    public void addDataItemValues(ContentValues values) {
+        addNamedDataItemValues(Data.CONTENT_URI, values);
+    }
+
+    public NamedDataItem addNamedDataItemValues(Uri uri, ContentValues values) {
+        final NamedDataItem namedItem = new NamedDataItem(uri, values);
+        mDataItems.add(namedItem);
+        return namedItem;
+    }
+
+    public ArrayList<ContentValues> getContentValues() {
+        final ArrayList<ContentValues> list = Lists.newArrayListWithCapacity(mDataItems.size());
+        for (NamedDataItem dataItem : mDataItems) {
+            if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+                list.add(dataItem.mContentValues);
+            }
+        }
+        return list;
+    }
+
+    public List<DataItem> getDataItems() {
+        final ArrayList<DataItem> list = Lists.newArrayListWithCapacity(mDataItems.size());
+        for (NamedDataItem dataItem : mDataItems) {
+            if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+                list.add(DataItem.createFrom(dataItem.mContentValues));
+            }
+        }
+        return list;
+    }
+
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("RawContact: ").append(mValues);
+        for (RawContact.NamedDataItem namedDataItem : mDataItems) {
+            sb.append("\n  ").append(namedDataItem.mUri);
+            sb.append("\n  -> ").append(namedDataItem.mContentValues);
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mValues, mDataItems);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+
+        RawContact other = (RawContact) obj;
+        return Objects.equal(mValues, other.mValues) &&
+                Objects.equal(mDataItems, other.mDataItems);
+    }
+}
diff --git a/src/com/android/contacts/common/model/RawContactDelta.java b/src/com/android/contacts/common/model/RawContactDelta.java
new file mode 100644
index 0000000..b8709c3
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContactDelta.java
@@ -0,0 +1,660 @@
+/*
+ * 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;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Profile;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.BuilderWrapper;
+import com.android.contacts.common.model.CPOWrapper;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.testing.NeededForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Contains a {@link RawContact} and records any modifications separately so the
+ * original {@link RawContact} can be swapped out with a newer version and the
+ * changes still cleanly applied.
+ * <p>
+ * One benefit of this approach is that we can build changes entirely on an
+ * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case.
+ * <p>
+ * When applying modifications over an {@link RawContact}, we try finding the
+ * original {@link Data#_ID} rows where the modifications took place. If those
+ * rows are missing from the new {@link RawContact}, we know the original data must
+ * be deleted, but to preserve the user modifications we treat as an insert.
+ */
+public class RawContactDelta implements Parcelable {
+    // TODO: optimize by using contentvalues pool, since we allocate so many of them
+
+    private static final String TAG = "EntityDelta";
+    private static final boolean LOGV = false;
+
+    /**
+     * Direct values from {@link Entity#getEntityValues()}.
+     */
+    private ValuesDelta mValues;
+
+    /**
+     * URI used for contacts queries, by default it is set to query raw contacts.
+     * It can be set to query the profile's raw contact(s).
+     */
+    private Uri mContactsQueryUri = RawContacts.CONTENT_URI;
+
+    /**
+     * Internal map of children values from {@link Entity#getSubValues()}, which
+     * we store here sorted into {@link Data#MIMETYPE} bins.
+     */
+    private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
+
+    public RawContactDelta() {
+    }
+
+    public RawContactDelta(ValuesDelta values) {
+        mValues = values;
+    }
+
+    /**
+     * Build an {@link RawContactDelta} using the given {@link RawContact} as a
+     * starting point; the "before" snapshot.
+     */
+    public static RawContactDelta fromBefore(RawContact before) {
+        final RawContactDelta rawContactDelta = new RawContactDelta();
+        rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
+        rawContactDelta.mValues.setIdColumn(RawContacts._ID);
+        for (final ContentValues values : before.getContentValues()) {
+            rawContactDelta.addEntry(ValuesDelta.fromBefore(values));
+        }
+        return rawContactDelta;
+    }
+
+    /**
+     * Merge the "after" values from the given {@link RawContactDelta} onto the
+     * "before" state represented by this {@link RawContactDelta}, discarding any
+     * existing "after" states. This is typically used when re-parenting changes
+     * onto an updated {@link Entity}.
+     */
+    public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) {
+        // Bail early if trying to merge delete with missing local
+        final ValuesDelta remoteValues = remote.mValues;
+        if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null;
+
+        // Create local version if none exists yet
+        if (local == null) local = new RawContactDelta();
+
+        if (LOGV) {
+            final Long localVersion = (local.mValues == null) ? null : local.mValues
+                    .getAsLong(RawContacts.VERSION);
+            final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
+            Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
+                    + localVersion);
+        }
+
+        // Create values if needed, and merge "after" changes
+        local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);
+
+        // Find matching local entry for each remote values, or create
+        for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
+            for (ValuesDelta remoteEntry : mimeEntries) {
+                final Long childId = remoteEntry.getId();
+
+                // Find or create local match and merge
+                final ValuesDelta localEntry = local.getEntry(childId);
+                final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);
+
+                if (localEntry == null && merged != null) {
+                    // No local entry before, so insert
+                    local.addEntry(merged);
+                }
+            }
+        }
+
+        return local;
+    }
+
+    public ValuesDelta getValues() {
+        return mValues;
+    }
+
+    public boolean isContactInsert() {
+        return mValues.isInsert();
+    }
+
+    /**
+     * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
+     * which may return null when no entry exists.
+     */
+    public ValuesDelta getPrimaryEntry(String mimeType) {
+        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
+        if (mimeEntries == null) return null;
+
+        for (ValuesDelta entry : mimeEntries) {
+            if (entry.isPrimary()) {
+                return entry;
+            }
+        }
+
+        // When no direct primary, return something
+        return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
+    }
+
+    /**
+     * calls {@link #getSuperPrimaryEntry(String, boolean)} with true
+     * @see #getSuperPrimaryEntry(String, boolean)
+     */
+    public ValuesDelta getSuperPrimaryEntry(String mimeType) {
+        return getSuperPrimaryEntry(mimeType, true);
+    }
+
+    /**
+     * Returns the super-primary entry for the given mime type
+     * @param forceSelection if true, will try to return some value even if a super-primary
+     *     doesn't exist (may be a primary, or just a random item
+     * @return
+     */
+    @NeededForTesting
+    public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
+        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
+        if (mimeEntries == null) return null;
+
+        ValuesDelta primary = null;
+        for (ValuesDelta entry : mimeEntries) {
+            if (entry.isSuperPrimary()) {
+                return entry;
+            } else if (entry.isPrimary()) {
+                primary = entry;
+            }
+        }
+
+        if (!forceSelection) {
+            return null;
+        }
+
+        // When no direct super primary, return something
+        if (primary != null) {
+            return primary;
+        }
+        return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
+    }
+
+    /**
+     * Return the AccountType that this raw-contact belongs to.
+     */
+    public AccountType getRawContactAccountType(Context context) {
+        ContentValues entityValues = getValues().getCompleteValues();
+        String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
+        String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
+        return AccountTypeManager.getInstance(context).getAccountType(type, dataSet);
+    }
+
+    public Long getRawContactId() {
+        return getValues().getAsLong(RawContacts._ID);
+    }
+
+    public String getAccountName() {
+        return getValues().getAsString(RawContacts.ACCOUNT_NAME);
+    }
+
+    public String getAccountType() {
+        return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+    }
+
+    public String getDataSet() {
+        return getValues().getAsString(RawContacts.DATA_SET);
+    }
+
+    public AccountType getAccountType(AccountTypeManager manager) {
+        return manager.getAccountType(getAccountType(), getDataSet());
+    }
+
+    public boolean isVisible() {
+        return getValues().isVisible();
+    }
+
+    /**
+     * Return the list of child {@link ValuesDelta} from our optimized map,
+     * creating the list if requested.
+     */
+    private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
+        ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
+        if (mimeEntries == null && lazyCreate) {
+            mimeEntries = Lists.newArrayList();
+            mEntries.put(mimeType, mimeEntries);
+        }
+        return mimeEntries;
+    }
+
+    public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
+        return getMimeEntries(mimeType, false);
+    }
+
+    public int getMimeEntriesCount(String mimeType, boolean onlyVisible) {
+        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType);
+        if (mimeEntries == null) return 0;
+
+        int count = 0;
+        for (ValuesDelta child : mimeEntries) {
+            // Skip deleted items when requesting only visible
+            if (onlyVisible && !child.isVisible()) continue;
+            count++;
+        }
+        return count;
+    }
+
+    public boolean hasMimeEntries(String mimeType) {
+        return mEntries.containsKey(mimeType);
+    }
+
+    public ValuesDelta addEntry(ValuesDelta entry) {
+        final String mimeType = entry.getMimetype();
+        getMimeEntries(mimeType, true).add(entry);
+        return entry;
+    }
+
+    public ArrayList<ContentValues> getContentValues() {
+        ArrayList<ContentValues> values = Lists.newArrayList();
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta entry : mimeEntries) {
+                if (!entry.isDelete()) {
+                    values.add(entry.getCompleteValues());
+                }
+            }
+        }
+        return values;
+    }
+
+    /**
+     * Find entry with the given {@link BaseColumns#_ID} value.
+     */
+    public ValuesDelta getEntry(Long childId) {
+        if (childId == null) {
+            // Requesting an "insert" entry, which has no "before"
+            return null;
+        }
+
+        // Search all children for requested entry
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta entry : mimeEntries) {
+                if (childId.equals(entry.getId())) {
+                    return entry;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the total number of {@link ValuesDelta} contained.
+     */
+    public int getEntryCount(boolean onlyVisible) {
+        int count = 0;
+        for (String mimeType : mEntries.keySet()) {
+            count += getMimeEntriesCount(mimeType, onlyVisible);
+        }
+        return count;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (object instanceof RawContactDelta) {
+            final RawContactDelta other = (RawContactDelta)object;
+
+            // Equality failed if parent values different
+            if (!other.mValues.equals(mValues)) return false;
+
+            for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+                for (ValuesDelta child : mimeEntries) {
+                    // Equality failed if any children unmatched
+                    if (!other.containsEntry(child)) return false;
+                }
+            }
+
+            // Passed all tests, so equal
+            return true;
+        }
+        return false;
+    }
+
+    private boolean containsEntry(ValuesDelta entry) {
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                // Contained if we find any child that matches
+                if (child.equals(entry)) return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Mark this entire object deleted, including any {@link ValuesDelta}.
+     */
+    public void markDeleted() {
+        this.mValues.markDeleted();
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                child.markDeleted();
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("\n(");
+        builder.append("Uri=");
+        builder.append(mContactsQueryUri);
+        builder.append(", Values=");
+        builder.append(mValues != null ? mValues.toString() : "null");
+        builder.append(", Entries={");
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                builder.append("\n\t");
+                child.toString(builder);
+            }
+        }
+        builder.append("\n})\n");
+        return builder.toString();
+    }
+
+    /**
+     * Consider building the given {@link ContentProviderOperation.Builder} and
+     * appending it to the given list, which only happens if builder is valid.
+     */
+    private void possibleAdd(ArrayList<ContentProviderOperation> diff,
+            ContentProviderOperation.Builder builder) {
+        if (builder != null) {
+            diff.add(builder.build());
+        }
+    }
+
+    /**
+     * For compatibility purpose, this method is copied from {@link #possibleAdd} and takes
+     * BuilderWrapper and an ArrayList of CPOWrapper as parameters.
+     */
+    private void possibleAddWrapper(ArrayList<CPOWrapper> diff, BuilderWrapper bw) {
+        if (bw != null && bw.getBuilder() != null) {
+            diff.add(new CPOWrapper(bw.getBuilder().build(), bw.getType()));
+        }
+    }
+
+    /**
+     * Build a list of {@link ContentProviderOperation} that will assert any
+     * "before" state hasn't changed. This is maintained separately so that all
+     * asserts can take place before any updates occur.
+     */
+    public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
+        final Builder builder = buildAssertHelper();
+        if (builder != null) {
+            buildInto.add(builder.build());
+        }
+    }
+
+    /**
+     * For compatibility purpose, this method is copied from {@link #buildAssert} and takes an
+     * ArrayList of CPOWrapper as parameter.
+     */
+    public void buildAssertWrapper(ArrayList<CPOWrapper> buildInto) {
+        final Builder builder = buildAssertHelper();
+        if (builder != null) {
+            buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_ASSERT));
+        }
+    }
+
+    private Builder buildAssertHelper() {
+        final boolean isContactInsert = mValues.isInsert();
+        ContentProviderOperation.Builder builder = null;
+        if (!isContactInsert) {
+            // Assert version is consistent while persisting changes
+            final Long beforeId = mValues.getId();
+            final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
+            if (beforeId == null || beforeVersion == null) return builder;
+            builder = ContentProviderOperation.newAssertQuery(mContactsQueryUri);
+            builder.withSelection(RawContacts._ID + "=" + beforeId, null);
+            builder.withValue(RawContacts.VERSION, beforeVersion);
+        }
+        return builder;
+    }
+
+    /**
+     * Build a list of {@link ContentProviderOperation} that will transform the
+     * current "before" {@link Entity} state into the modified state which this
+     * {@link RawContactDelta} represents.
+     */
+    public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
+        final int firstIndex = buildInto.size();
+
+        final boolean isContactInsert = mValues.isInsert();
+        final boolean isContactDelete = mValues.isDelete();
+        final boolean isContactUpdate = !isContactInsert && !isContactDelete;
+
+        final Long beforeId = mValues.getId();
+
+        Builder builder;
+
+        if (isContactInsert) {
+            // TODO: for now simply disabling aggregation when a new contact is
+            // created on the phone.  In the future, will show aggregation suggestions
+            // after saving the contact.
+            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+        }
+
+        // Build possible operation at Contact level
+        builder = mValues.buildDiff(mContactsQueryUri);
+        possibleAdd(buildInto, builder);
+
+        // Build operations for all children
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                // Ignore children if parent was deleted
+                if (isContactDelete) continue;
+
+                // Use the profile data URI if the contact is the profile.
+                if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
+                    builder = child.buildDiff(Uri.withAppendedPath(Profile.CONTENT_URI,
+                            RawContacts.Data.CONTENT_DIRECTORY));
+                } else {
+                    builder = child.buildDiff(Data.CONTENT_URI);
+                }
+
+                if (child.isInsert()) {
+                    if (isContactInsert) {
+                        // Parent is brand new insert, so back-reference _id
+                        builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
+                    } else {
+                        // Inserting under existing, so fill with known _id
+                        builder.withValue(Data.RAW_CONTACT_ID, beforeId);
+                    }
+                } else if (isContactInsert && builder != null) {
+                    // Child must be insert when Contact insert
+                    throw new IllegalArgumentException("When parent insert, child must be also");
+                }
+                possibleAdd(buildInto, builder);
+            }
+        }
+
+        final boolean addedOperations = buildInto.size() > firstIndex;
+        if (addedOperations && isContactUpdate) {
+            // Suspend aggregation while persisting updates
+            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
+            buildInto.add(firstIndex, builder.build());
+
+            // Restore aggregation mode as last operation
+            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
+            buildInto.add(builder.build());
+        } else if (isContactInsert) {
+            // Restore aggregation mode as last operation
+            builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
+            builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+            builder.withSelection(RawContacts._ID + "=?", new String[1]);
+            builder.withSelectionBackReference(0, firstIndex);
+            buildInto.add(builder.build());
+        }
+    }
+
+    /**
+     * For compatibility purpose, this method is copied from {@link #buildDiff} and takes an
+     * ArrayList of CPOWrapper as parameter.
+     */
+    public void buildDiffWrapper(ArrayList<CPOWrapper> buildInto) {
+        final int firstIndex = buildInto.size();
+
+        final boolean isContactInsert = mValues.isInsert();
+        final boolean isContactDelete = mValues.isDelete();
+        final boolean isContactUpdate = !isContactInsert && !isContactDelete;
+
+        final Long beforeId = mValues.getId();
+
+        if (isContactInsert) {
+            // TODO: for now simply disabling aggregation when a new contact is
+            // created on the phone.  In the future, will show aggregation suggestions
+            // after saving the contact.
+            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+        }
+
+        // Build possible operation at Contact level
+        BuilderWrapper bw = mValues.buildDiffWrapper(mContactsQueryUri);
+        possibleAddWrapper(buildInto, bw);
+
+        // Build operations for all children
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                // Ignore children if parent was deleted
+                if (isContactDelete) continue;
+
+                // Use the profile data URI if the contact is the profile.
+                if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
+                    bw = child.buildDiffWrapper(Uri.withAppendedPath(Profile.CONTENT_URI,
+                            RawContacts.Data.CONTENT_DIRECTORY));
+                } else {
+                    bw = child.buildDiffWrapper(Data.CONTENT_URI);
+                }
+
+                if (child.isInsert()) {
+                    if (isContactInsert) {
+                        // Parent is brand new insert, so back-reference _id
+                        bw.getBuilder().withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
+                    } else {
+                        // Inserting under existing, so fill with known _id
+                        bw.getBuilder().withValue(Data.RAW_CONTACT_ID, beforeId);
+                    }
+                } else if (isContactInsert && bw != null && bw.getBuilder() != null) {
+                    // Child must be insert when Contact insert
+                    throw new IllegalArgumentException("When parent insert, child must be also");
+                }
+                possibleAddWrapper(buildInto, bw);
+            }
+        }
+
+        final boolean addedOperations = buildInto.size() > firstIndex;
+        if (addedOperations && isContactUpdate) {
+            // Suspend aggregation while persisting updates
+            Builder builder =
+                    buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
+            buildInto.add(firstIndex, new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
+
+            // Restore aggregation mode as last operation
+            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
+            buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
+        } else if (isContactInsert) {
+            // Restore aggregation mode as last operation
+            Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
+            builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+            builder.withSelection(RawContacts._ID + "=?", new String[1]);
+            builder.withSelectionBackReference(0, firstIndex);
+            buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
+        }
+    }
+
+    /**
+     * Build a {@link ContentProviderOperation} that changes
+     * {@link RawContacts#AGGREGATION_MODE} to the given value.
+     */
+    protected Builder buildSetAggregationMode(Long beforeId, int mode) {
+        Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
+        builder.withValue(RawContacts.AGGREGATION_MODE, mode);
+        builder.withSelection(RawContacts._ID + "=" + beforeId, null);
+        return builder;
+    }
+
+    /** {@inheritDoc} */
+    public int describeContents() {
+        // Nothing special about this parcel
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    public void writeToParcel(Parcel dest, int flags) {
+        final int size = this.getEntryCount(false);
+        dest.writeInt(size);
+        dest.writeParcelable(mValues, flags);
+        dest.writeParcelable(mContactsQueryUri, flags);
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                dest.writeParcelable(child, flags);
+            }
+        }
+    }
+
+    public void readFromParcel(Parcel source) {
+        final ClassLoader loader = getClass().getClassLoader();
+        final int size = source.readInt();
+        mValues = source.<ValuesDelta> readParcelable(loader);
+        mContactsQueryUri = source.<Uri> readParcelable(loader);
+        for (int i = 0; i < size; i++) {
+            final ValuesDelta child = source.<ValuesDelta> readParcelable(loader);
+            this.addEntry(child);
+        }
+    }
+
+    /**
+     * Used to set the query URI to the profile URI to store profiles.
+     */
+    public void setProfileQueryUri() {
+        mContactsQueryUri = Profile.CONTENT_RAW_CONTACTS_URI;
+    }
+
+    public static final Parcelable.Creator<RawContactDelta> CREATOR =
+            new Parcelable.Creator<RawContactDelta>() {
+        public RawContactDelta createFromParcel(Parcel in) {
+            final RawContactDelta state = new RawContactDelta();
+            state.readFromParcel(in);
+            return state;
+        }
+
+        public RawContactDelta[] newArray(int size) {
+            return new RawContactDelta[size];
+        }
+    };
+
+}
diff --git a/src/com/android/contacts/common/model/RawContactDeltaList.java b/src/com/android/contacts/common/model/RawContactDeltaList.java
new file mode 100644
index 0000000..6964643
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContactDeltaList.java
@@ -0,0 +1,573 @@
+/*
+ * 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;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.CPOWrapper;
+import com.android.contacts.common.model.ValuesDelta;
+
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+
+/**
+ * Container for multiple {@link RawContactDelta} objects, usually when editing
+ * together as an entire aggregate. Provides convenience methods for parceling
+ * and applying another {@link RawContactDeltaList} over it.
+ */
+public class RawContactDeltaList extends ArrayList<RawContactDelta> implements Parcelable {
+    private static final String TAG = RawContactDeltaList.class.getSimpleName();
+    private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
+
+    private boolean mSplitRawContacts;
+    private long[] mJoinWithRawContactIds;
+
+    public RawContactDeltaList() {
+    }
+
+    /**
+     * Create an {@link RawContactDeltaList} based on {@link Contacts} specified by the
+     * given query parameters. This closes the {@link EntityIterator} when
+     * finished, so it doesn't subscribe to updates.
+     */
+    public static RawContactDeltaList fromQuery(Uri entityUri, ContentResolver resolver,
+            String selection, String[] selectionArgs, String sortOrder) {
+        final EntityIterator iterator = RawContacts.newEntityIterator(
+                resolver.query(entityUri, null, selection, selectionArgs, sortOrder));
+        try {
+            return fromIterator(iterator);
+        } finally {
+            iterator.close();
+        }
+    }
+
+    /**
+     * Create an {@link RawContactDeltaList} that contains the entities of the Iterator as before
+     * values.  This function can be passed an iterator of Entity objects or an iterator of
+     * RawContact objects.
+     */
+    public static RawContactDeltaList fromIterator(Iterator<?> iterator) {
+        final RawContactDeltaList state = new RawContactDeltaList();
+        state.addAll(iterator);
+        return state;
+    }
+
+    public void addAll(Iterator<?> iterator) {
+        // Perform background query to pull contact details
+        while (iterator.hasNext()) {
+            // Read all contacts into local deltas to prepare for edits
+            Object nextObject = iterator.next();
+            final RawContact before = nextObject instanceof Entity
+                    ? RawContact.createFrom((Entity) nextObject)
+                    : (RawContact) nextObject;
+            final RawContactDelta rawContactDelta = RawContactDelta.fromBefore(before);
+            add(rawContactDelta);
+        }
+    }
+
+    /**
+     * Merge the "after" values from the given {@link RawContactDeltaList}, discarding any
+     * previous "after" states. This is typically used when re-parenting user
+     * edits onto an updated {@link RawContactDeltaList}.
+     */
+    public static RawContactDeltaList mergeAfter(RawContactDeltaList local,
+            RawContactDeltaList remote) {
+        if (local == null) local = new RawContactDeltaList();
+
+        // For each entity in the remote set, try matching over existing
+        for (RawContactDelta remoteEntity : remote) {
+            final Long rawContactId = remoteEntity.getValues().getId();
+
+            // Find or create local match and merge
+            final RawContactDelta localEntity = local.getByRawContactId(rawContactId);
+            final RawContactDelta merged = RawContactDelta.mergeAfter(localEntity, remoteEntity);
+
+            if (localEntity == null && merged != null) {
+                // No local entry before, so insert
+                local.add(merged);
+            }
+        }
+
+        return local;
+    }
+
+    /**
+     * Build a list of {@link ContentProviderOperation} that will transform all
+     * the "before" {@link Entity} states into the modified state which all
+     * {@link RawContactDelta} objects represent. This method specifically creates
+     * any {@link AggregationExceptions} rules needed to groups edits together.
+     */
+    public ArrayList<ContentProviderOperation> buildDiff() {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "buildDiff: list=" + toString());
+        }
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+
+        final long rawContactId = this.findRawContactId();
+        int firstInsertRow = -1;
+
+        // First pass enforces versions remain consistent
+        for (RawContactDelta delta : this) {
+            delta.buildAssert(diff);
+        }
+
+        final int assertMark = diff.size();
+        int backRefs[] = new int[size()];
+
+        int rawContactIndex = 0;
+
+        // Second pass builds actual operations
+        for (RawContactDelta delta : this) {
+            final int firstBatch = diff.size();
+            final boolean isInsert = delta.isContactInsert();
+            backRefs[rawContactIndex++] = isInsert ? firstBatch : -1;
+
+            delta.buildDiff(diff);
+
+            // If the user chose to join with some other existing raw contact(s) at save time,
+            // add aggregation exceptions for all those raw contacts.
+            if (mJoinWithRawContactIds != null) {
+                for (Long joinedRawContactId : mJoinWithRawContactIds) {
+                    final Builder builder = beginKeepTogether();
+                    builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, joinedRawContactId);
+                    if (rawContactId != -1) {
+                        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId);
+                    } else {
+                        builder.withValueBackReference(
+                                AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                    }
+                    diff.add(builder.build());
+                }
+            }
+
+            // Only create rules for inserts
+            if (!isInsert) continue;
+
+            // If we are going to split all contacts, there is no point in first combining them
+            if (mSplitRawContacts) continue;
+
+            if (rawContactId != -1) {
+                // Has existing contact, so bind to it strongly
+                final Builder builder = beginKeepTogether();
+                builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                diff.add(builder.build());
+
+            } else if (firstInsertRow == -1) {
+                // First insert case, so record row
+                firstInsertRow = firstBatch;
+
+            } else {
+                // Additional insert case, so point at first insert
+                final Builder builder = beginKeepTogether();
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1,
+                        firstInsertRow);
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                diff.add(builder.build());
+            }
+        }
+
+        if (mSplitRawContacts) {
+            buildSplitContactDiff(diff, backRefs);
+        }
+
+        // No real changes if only left with asserts
+        if (diff.size() == assertMark) {
+            diff.clear();
+        }
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "buildDiff: ops=" + diffToString(diff));
+        }
+        return diff;
+    }
+
+    /**
+     * For compatibility purpose, this method is copied from {@link #buildDiff} and returns an
+     * ArrayList of CPOWrapper.
+     */
+    public ArrayList<CPOWrapper> buildDiffWrapper() {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "buildDiffWrapper: list=" + toString());
+        }
+        final ArrayList<CPOWrapper> diffWrapper = Lists.newArrayList();
+
+        final long rawContactId = this.findRawContactId();
+        int firstInsertRow = -1;
+
+        // First pass enforces versions remain consistent
+        for (RawContactDelta delta : this) {
+            delta.buildAssertWrapper(diffWrapper);
+        }
+
+        final int assertMark = diffWrapper.size();
+        int backRefs[] = new int[size()];
+
+        int rawContactIndex = 0;
+
+        // Second pass builds actual operations
+        for (RawContactDelta delta : this) {
+            final int firstBatch = diffWrapper.size();
+            final boolean isInsert = delta.isContactInsert();
+            backRefs[rawContactIndex++] = isInsert ? firstBatch : -1;
+
+            delta.buildDiffWrapper(diffWrapper);
+
+            // If the user chose to join with some other existing raw contact(s) at save time,
+            // add aggregation exceptions for all those raw contacts.
+            if (mJoinWithRawContactIds != null) {
+                for (Long joinedRawContactId : mJoinWithRawContactIds) {
+                    final Builder builder = beginKeepTogether();
+                    builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, joinedRawContactId);
+                    if (rawContactId != -1) {
+                        builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId);
+                    } else {
+                        builder.withValueBackReference(
+                                AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                    }
+                    diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
+                }
+            }
+
+            // Only create rules for inserts
+            if (!isInsert) continue;
+
+            // If we are going to split all contacts, there is no point in first combining them
+            if (mSplitRawContacts) continue;
+
+            if (rawContactId != -1) {
+                // Has existing contact, so bind to it strongly
+                final Builder builder = beginKeepTogether();
+                builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
+
+            } else if (firstInsertRow == -1) {
+                // First insert case, so record row
+                firstInsertRow = firstBatch;
+
+            } else {
+                // Additional insert case, so point at first insert
+                final Builder builder = beginKeepTogether();
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1,
+                        firstInsertRow);
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
+            }
+        }
+
+        if (mSplitRawContacts) {
+            buildSplitContactDiffWrapper(diffWrapper, backRefs);
+        }
+
+        // No real changes if only left with asserts
+        if (diffWrapper.size() == assertMark) {
+            diffWrapper.clear();
+        }
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "buildDiff: ops=" + diffToStringWrapper(diffWrapper));
+        }
+        return diffWrapper;
+    }
+
+    private static String diffToString(ArrayList<ContentProviderOperation> ops) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("[\n");
+        for (ContentProviderOperation op : ops) {
+            sb.append(op.toString());
+            sb.append(",\n");
+        }
+        sb.append("]\n");
+        return sb.toString();
+    }
+
+    /**
+     * For compatibility purpose.
+     */
+    private static String diffToStringWrapper(ArrayList<CPOWrapper> cpoWrappers) {
+        ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
+        for (CPOWrapper cpoWrapper : cpoWrappers) {
+            ops.add(cpoWrapper.getOperation());
+        }
+        return diffToString(ops);
+    }
+
+    /**
+     * Start building a {@link ContentProviderOperation} that will keep two
+     * {@link RawContacts} together.
+     */
+    protected Builder beginKeepTogether() {
+        final Builder builder = ContentProviderOperation
+                .newUpdate(AggregationExceptions.CONTENT_URI);
+        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
+        return builder;
+    }
+
+    /**
+     * Builds {@link AggregationExceptions} to split all constituent raw contacts into
+     * separate contacts.
+     */
+    private void buildSplitContactDiff(final ArrayList<ContentProviderOperation> diff,
+            int[] backRefs) {
+        final int count = size();
+        for (int i = 0; i < count; i++) {
+            for (int j = 0; j < count; j++) {
+                if (i == j) {
+                    continue;
+                }
+                final Builder builder = buildSplitContactDiffHelper(i, j, backRefs);
+                if (builder != null) {
+                    diff.add(builder.build());
+                }
+            }
+        }
+    }
+
+    /**
+     * For compatibility purpose, this method is copied from {@link #buildSplitContactDiff} and
+     * takes an ArrayList of CPOWrapper as parameter.
+     */
+    private void buildSplitContactDiffWrapper(final ArrayList<CPOWrapper> diff, int[] backRefs) {
+        final int count = size();
+        for (int i = 0; i < count; i++) {
+            for (int j = 0; j < count; j++) {
+                if (i == j) {
+                    continue;
+                }
+                final Builder builder = buildSplitContactDiffHelper(i, j, backRefs);
+                if (builder != null) {
+                    diff.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
+                }
+            }
+        }
+    }
+
+    private Builder buildSplitContactDiffHelper(int index1, int index2, int[] backRefs) {
+        final Builder builder =
+                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
+        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
+
+        Long rawContactId1 = get(index1).getValues().getAsLong(RawContacts._ID);
+        int backRef1 = backRefs[index1];
+        if (rawContactId1 != null && rawContactId1 >= 0) {
+            builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
+        } else if (backRef1 >= 0) {
+            builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, backRef1);
+        } else {
+            return null;
+        }
+
+        Long rawContactId2 = get(index2).getValues().getAsLong(RawContacts._ID);
+        int backRef2 = backRefs[index2];
+        if (rawContactId2 != null && rawContactId2 >= 0) {
+            builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
+        } else if (backRef2 >= 0) {
+            builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, backRef2);
+        } else {
+            return null;
+        }
+        return builder;
+    }
+
+    /**
+     * Search all contained {@link RawContactDelta} for the first one with an
+     * existing {@link RawContacts#_ID} value. Usually used when creating
+     * {@link AggregationExceptions} during an update.
+     */
+    public long findRawContactId() {
+        for (RawContactDelta delta : this) {
+            final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
+            if (rawContactId != null && rawContactId >= 0) {
+                return rawContactId;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Find {@link RawContacts#_ID} of the requested {@link RawContactDelta}.
+     */
+    public Long getRawContactId(int index) {
+        if (index >= 0 && index < this.size()) {
+            final RawContactDelta delta = this.get(index);
+            final ValuesDelta values = delta.getValues();
+            if (values.isVisible()) {
+                return values.getAsLong(RawContacts._ID);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Find the raw-contact (an {@link RawContactDelta}) with the specified ID.
+     */
+    public RawContactDelta getByRawContactId(Long rawContactId) {
+        final int index = this.indexOfRawContactId(rawContactId);
+        return (index == -1) ? null : this.get(index);
+    }
+
+    /**
+     * Find index of given {@link RawContacts#_ID} when present.
+     */
+    public int indexOfRawContactId(Long rawContactId) {
+        if (rawContactId == null) return -1;
+        final int size = this.size();
+        for (int i = 0; i < size; i++) {
+            final Long currentId = getRawContactId(i);
+            if (rawContactId.equals(currentId)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Return the index of the first RawContactDelta corresponding to a writable raw-contact, or -1.
+     * */
+    public int indexOfFirstWritableRawContact(Context context) {
+        // Find the first writable entity.
+        int entityIndex = 0;
+        for (RawContactDelta delta : this) {
+            if (delta.getRawContactAccountType(context).areContactsWritable()) return entityIndex;
+            entityIndex++;
+        }
+        return -1;
+    }
+
+    /**  Return the first RawContactDelta corresponding to a writable raw-contact, or null. */
+    public RawContactDelta getFirstWritableRawContact(Context context) {
+        final int index = indexOfFirstWritableRawContact(context);
+        return (index == -1) ? null : get(index);
+    }
+
+    public ValuesDelta getSuperPrimaryEntry(final String mimeType) {
+        ValuesDelta primary = null;
+        ValuesDelta randomEntry = null;
+        for (RawContactDelta delta : this) {
+            final ArrayList<ValuesDelta> mimeEntries = delta.getMimeEntries(mimeType);
+            if (mimeEntries == null) return null;
+
+            for (ValuesDelta entry : mimeEntries) {
+                if (entry.isSuperPrimary()) {
+                    return entry;
+                } else if (primary == null && entry.isPrimary()) {
+                    primary = entry;
+                } else if (randomEntry == null) {
+                    randomEntry = entry;
+                }
+            }
+        }
+        // When no direct super primary, return something
+        if (primary != null) {
+            return primary;
+        }
+        return randomEntry;
+    }
+
+    /**
+     * Sets a flag that will split ("explode") the raw_contacts into seperate contacts
+     */
+    public void markRawContactsForSplitting() {
+        mSplitRawContacts = true;
+    }
+
+    public boolean isMarkedForSplitting() {
+        return mSplitRawContacts;
+    }
+
+    public void setJoinWithRawContacts(long[] rawContactIds) {
+        mJoinWithRawContactIds = rawContactIds;
+    }
+
+    public boolean isMarkedForJoining() {
+        return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int describeContents() {
+        // Nothing special about this parcel
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        final int size = this.size();
+        dest.writeInt(size);
+        for (RawContactDelta delta : this) {
+            dest.writeParcelable(delta, flags);
+        }
+        dest.writeLongArray(mJoinWithRawContactIds);
+        dest.writeInt(mSplitRawContacts ? 1 : 0);
+    }
+
+    @SuppressWarnings("unchecked")
+    public void readFromParcel(Parcel source) {
+        final ClassLoader loader = getClass().getClassLoader();
+        final int size = source.readInt();
+        for (int i = 0; i < size; i++) {
+            this.add(source.<RawContactDelta> readParcelable(loader));
+        }
+        mJoinWithRawContactIds = source.createLongArray();
+        mSplitRawContacts = source.readInt() != 0;
+    }
+
+    public static final Parcelable.Creator<RawContactDeltaList> CREATOR =
+            new Parcelable.Creator<RawContactDeltaList>() {
+        @Override
+        public RawContactDeltaList createFromParcel(Parcel in) {
+            final RawContactDeltaList state = new RawContactDeltaList();
+            state.readFromParcel(in);
+            return state;
+        }
+
+        @Override
+        public RawContactDeltaList[] newArray(int size) {
+            return new RawContactDeltaList[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("(");
+        sb.append("Split=");
+        sb.append(mSplitRawContacts);
+        sb.append(", Join=[");
+        sb.append(Arrays.toString(mJoinWithRawContactIds));
+        sb.append("], Values=");
+        sb.append(super.toString());
+        sb.append(")");
+        return sb.toString();
+    }
+}
diff --git a/src/com/android/contacts/common/model/RawContactModifier.java b/src/com/android/contacts/common/model/RawContactModifier.java
new file mode 100644
index 0000000..fd028e3
--- /dev/null
+++ b/src/com/android/contacts/common/model/RawContactModifier.java
@@ -0,0 +1,1424 @@
+/*
+ * 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;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+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.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.Intents.Insert;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.util.CommonDateUtils;
+import com.android.contacts.common.util.DateUtils;
+import com.android.contacts.common.util.NameConverter;
+import com.android.contacts.common.model.account.AccountType;
+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.EventEditType;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.model.dataitem.PhoneDataItem;
+import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
+
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Helper methods for modifying an {@link RawContactDelta}, such as inserting
+ * new rows, or enforcing {@link AccountType}.
+ */
+public class RawContactModifier {
+    private static final String TAG = RawContactModifier.class.getSimpleName();
+
+    /** Set to true in order to view logs on entity operations */
+    private static final boolean DEBUG = false;
+
+    /**
+     * For the given {@link RawContactDelta}, determine if the given
+     * {@link DataKind} could be inserted under specific
+     * {@link AccountType}.
+     */
+    public static boolean canInsert(RawContactDelta state, DataKind kind) {
+        // Insert possible when have valid types and under overall maximum
+        final int visibleCount = state.getMimeEntriesCount(kind.mimeType, true);
+        final boolean validTypes = hasValidTypes(state, kind);
+        final boolean validOverall = (kind.typeOverallMax == -1)
+                || (visibleCount < kind.typeOverallMax);
+        return (validTypes && validOverall);
+    }
+
+    public static boolean hasValidTypes(RawContactDelta state, DataKind kind) {
+        if (RawContactModifier.hasEditTypes(kind)) {
+            return (getValidTypes(state, kind, null, true, null, true).size() > 0);
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Ensure that at least one of the given {@link DataKind} exists in the
+     * given {@link RawContactDelta} state, and try creating one if none exist.
+     * @return The child (either newly created or the first existing one), or null if the
+     *     account doesn't support this {@link DataKind}.
+     */
+    public static ValuesDelta ensureKindExists(
+            RawContactDelta state, AccountType accountType, String mimeType) {
+        final DataKind kind = accountType.getKindForMimetype(mimeType);
+        final boolean hasChild = state.getMimeEntriesCount(mimeType, true) > 0;
+
+        if (kind != null) {
+            if (hasChild) {
+                // Return the first entry.
+                return state.getMimeEntries(mimeType).get(0);
+            } else {
+                // Create child when none exists and valid kind
+                final ValuesDelta child = insertChild(state, kind);
+                if (kind.mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
+                    child.setFromTemplate(true);
+                }
+                return child;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * For the given {@link RawContactDelta} and {@link DataKind}, return the
+     * list possible {@link EditType} options available based on
+     * {@link AccountType}.
+     *
+     * @param forceInclude Always include this {@link EditType} in the returned
+     *            list, even when an otherwise-invalid choice. This is useful
+     *            when showing a dialog that includes the current type.
+     * @param includeSecondary If true, include any valid types marked as
+     *            {@link EditType#secondary}.
+     * @param typeCount When provided, will be used for the frequency count of
+     *            each {@link EditType}, otherwise built using
+     *            {@link #getTypeFrequencies(RawContactDelta, DataKind)}.
+     * @param checkOverall If true, check if the overall number of types is under limit.
+     */
+    public static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind,
+            EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount,
+            boolean checkOverall) {
+        final ArrayList<EditType> validTypes = new ArrayList<EditType>();
+
+        // Bail early if no types provided
+        if (!hasEditTypes(kind)) return validTypes;
+
+        if (typeCount == null) {
+            // Build frequency counts if not provided
+            typeCount = getTypeFrequencies(state, kind);
+        }
+
+        // Build list of valid types
+        boolean validOverall = true;
+        if (checkOverall) {
+            final int overallCount = typeCount.get(FREQUENCY_TOTAL);
+            validOverall = (kind.typeOverallMax == -1 ? true
+                    : overallCount < kind.typeOverallMax);
+        }
+
+        for (EditType type : kind.typeList) {
+            final boolean validSpecific = (type.specificMax == -1 ? true : typeCount
+                    .get(type.rawValue) < type.specificMax);
+            final boolean validSecondary = (includeSecondary ? true : !type.secondary);
+            final boolean forcedInclude = type.equals(forceInclude);
+            if (forcedInclude || (validOverall && validSpecific && validSecondary)) {
+                // Type is valid when no limit, under limit, or forced include
+                validTypes.add(type);
+            }
+        }
+
+        return validTypes;
+    }
+
+    private static final int FREQUENCY_TOTAL = Integer.MIN_VALUE;
+
+    /**
+     * Count up the frequency that each {@link EditType} appears in the given
+     * {@link RawContactDelta}. The returned {@link SparseIntArray} maps from
+     * {@link EditType#rawValue} to counts, with the total overall count stored
+     * as {@link #FREQUENCY_TOTAL}.
+     */
+    private static SparseIntArray getTypeFrequencies(RawContactDelta state, DataKind kind) {
+        final SparseIntArray typeCount = new SparseIntArray();
+
+        // Find all entries for this kind, bailing early if none found
+        final List<ValuesDelta> mimeEntries = state.getMimeEntries(kind.mimeType);
+        if (mimeEntries == null) return typeCount;
+
+        int totalCount = 0;
+        for (ValuesDelta entry : mimeEntries) {
+            // Only count visible entries
+            if (!entry.isVisible()) continue;
+            totalCount++;
+
+            final EditType type = getCurrentType(entry, kind);
+            if (type != null) {
+                final int count = typeCount.get(type.rawValue);
+                typeCount.put(type.rawValue, count + 1);
+            }
+        }
+        typeCount.put(FREQUENCY_TOTAL, totalCount);
+        return typeCount;
+    }
+
+    /**
+     * Check if the given {@link DataKind} has multiple types that should be
+     * displayed for users to pick.
+     */
+    public static boolean hasEditTypes(DataKind kind) {
+        return kind.typeList != null && kind.typeList.size() > 0;
+    }
+
+    /**
+     * Find the {@link EditType} that describes the given
+     * {@link ValuesDelta} row, assuming the given {@link DataKind} dictates
+     * the possible types.
+     */
+    public static EditType getCurrentType(ValuesDelta entry, DataKind kind) {
+        final Long rawValue = entry.getAsLong(kind.typeColumn);
+        if (rawValue == null) return null;
+        return getType(kind, rawValue.intValue());
+    }
+
+    /**
+     * Find the {@link EditType} that describes the given {@link ContentValues} row,
+     * assuming the given {@link DataKind} dictates the possible types.
+     */
+    public static EditType getCurrentType(ContentValues entry, DataKind kind) {
+        if (kind.typeColumn == null) return null;
+        final Integer rawValue = entry.getAsInteger(kind.typeColumn);
+        if (rawValue == null) return null;
+        return getType(kind, rawValue);
+    }
+
+    /**
+     * Find the {@link EditType} that describes the given {@link Cursor} row,
+     * assuming the given {@link DataKind} dictates the possible types.
+     */
+    public static EditType getCurrentType(Cursor cursor, DataKind kind) {
+        if (kind.typeColumn == null) return null;
+        final int index = cursor.getColumnIndex(kind.typeColumn);
+        if (index == -1) return null;
+        final int rawValue = cursor.getInt(index);
+        return getType(kind, rawValue);
+    }
+
+    /**
+     * Find the {@link EditType} with the given {@link EditType#rawValue}.
+     */
+    public static EditType getType(DataKind kind, int rawValue) {
+        for (EditType type : kind.typeList) {
+            if (type.rawValue == rawValue) {
+                return type;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the precedence for the the given {@link EditType#rawValue}, where
+     * lower numbers are higher precedence.
+     */
+    public static int getTypePrecedence(DataKind kind, int rawValue) {
+        for (int i = 0; i < kind.typeList.size(); i++) {
+            final EditType type = kind.typeList.get(i);
+            if (type.rawValue == rawValue) {
+                return i;
+            }
+        }
+        return Integer.MAX_VALUE;
+    }
+
+    /**
+     * Find the best {@link EditType} for a potential insert. The "best" is the
+     * first primary type that doesn't already exist. When all valid types
+     * exist, we pick the last valid option.
+     */
+    public static EditType getBestValidType(RawContactDelta state, DataKind kind,
+            boolean includeSecondary, int exactValue) {
+        // Shortcut when no types
+        if (kind == null || kind.typeColumn == null) return null;
+
+        // Find type counts and valid primary types, bail if none
+        final SparseIntArray typeCount = getTypeFrequencies(state, kind);
+        final ArrayList<EditType> validTypes = getValidTypes(state, kind, null, includeSecondary,
+                typeCount, /*checkOverall=*/ true);
+        if (validTypes.size() == 0) return null;
+
+        // Keep track of the last valid type
+        final EditType lastType = validTypes.get(validTypes.size() - 1);
+
+        // Remove any types that already exist
+        Iterator<EditType> iterator = validTypes.iterator();
+        while (iterator.hasNext()) {
+            final EditType type = iterator.next();
+            final int count = typeCount.get(type.rawValue);
+
+            if (exactValue == type.rawValue) {
+                // Found exact value match
+                return type;
+            }
+
+            if (count > 0) {
+                // Type already appears, so don't consider
+                iterator.remove();
+            }
+        }
+
+        // Use the best remaining, otherwise the last valid
+        if (validTypes.size() > 0) {
+            return validTypes.get(0);
+        } else {
+            return lastType;
+        }
+    }
+
+    /**
+     * Insert a new child of kind {@link DataKind} into the given
+     * {@link RawContactDelta}. Tries using the best {@link EditType} found using
+     * {@link #getBestValidType(RawContactDelta, DataKind, boolean, int)}.
+     */
+    public static ValuesDelta insertChild(RawContactDelta state, DataKind kind) {
+        // Bail early if invalid kind
+        if (kind == null) return null;
+        // First try finding a valid primary
+        EditType bestType = getBestValidType(state, kind, false, Integer.MIN_VALUE);
+        if (bestType == null) {
+            // No valid primary found, so expand search to secondary
+            bestType = getBestValidType(state, kind, true, Integer.MIN_VALUE);
+        }
+        return insertChild(state, kind, bestType);
+    }
+
+    /**
+     * Insert a new child of kind {@link DataKind} into the given
+     * {@link RawContactDelta}, marked with the given {@link EditType}.
+     */
+    public static ValuesDelta insertChild(RawContactDelta state, DataKind kind, EditType type) {
+        // Bail early if invalid kind
+        if (kind == null) return null;
+        final ContentValues after = new ContentValues();
+
+        // Our parent CONTACT_ID is provided later
+        after.put(Data.MIMETYPE, kind.mimeType);
+
+        // Fill-in with any requested default values
+        if (kind.defaultValues != null) {
+            after.putAll(kind.defaultValues);
+        }
+
+        if (kind.typeColumn != null && type != null) {
+            // Set type, if provided
+            after.put(kind.typeColumn, type.rawValue);
+        }
+
+        final ValuesDelta child = ValuesDelta.fromAfter(after);
+        state.addEntry(child);
+        return child;
+    }
+
+    /**
+     * Processing to trim any empty {@link ValuesDelta} and {@link RawContactDelta}
+     * from the given {@link RawContactDeltaList}, assuming the given {@link AccountTypeManager}
+     * dictates the structure for various fields. This method ignores rows not
+     * described by the {@link AccountType}.
+     */
+    public static void trimEmpty(RawContactDeltaList set, AccountTypeManager accountTypes) {
+        for (RawContactDelta state : set) {
+            ValuesDelta values = state.getValues();
+            final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+            final String dataSet = values.getAsString(RawContacts.DATA_SET);
+            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+            trimEmpty(state, type);
+        }
+    }
+
+    public static boolean hasChanges(RawContactDeltaList set, AccountTypeManager accountTypes) {
+        return hasChanges(set, accountTypes, /* excludedMimeTypes =*/ null);
+    }
+
+    public static boolean hasChanges(RawContactDeltaList set, AccountTypeManager accountTypes,
+            Set<String> excludedMimeTypes) {
+        if (set.isMarkedForSplitting() || set.isMarkedForJoining()) {
+            return true;
+        }
+
+        for (RawContactDelta state : set) {
+            ValuesDelta values = state.getValues();
+            final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+            final String dataSet = values.getAsString(RawContacts.DATA_SET);
+            final AccountType type = accountTypes.getAccountType(accountType, dataSet);
+            if (hasChanges(state, type, excludedMimeTypes)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Processing to trim any empty {@link ValuesDelta} rows from the given
+     * {@link RawContactDelta}, assuming the given {@link AccountType} dictates
+     * the structure for various fields. This method ignores rows not described
+     * by the {@link AccountType}.
+     */
+    public static void trimEmpty(RawContactDelta state, AccountType accountType) {
+        boolean hasValues = false;
+
+        // Walk through entries for each well-known kind
+        for (DataKind kind : accountType.getSortedDataKinds()) {
+            final String mimeType = kind.mimeType;
+            final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+            if (entries == null) continue;
+
+            for (ValuesDelta entry : entries) {
+                // Skip any values that haven't been touched
+                final boolean touched = entry.isInsert() || entry.isUpdate();
+                if (!touched) {
+                    hasValues = true;
+                    continue;
+                }
+
+                // Test and remove this row if empty and it isn't a photo from google
+                final boolean isGoogleAccount = TextUtils.equals(GoogleAccountType.ACCOUNT_TYPE,
+                        state.getValues().getAsString(RawContacts.ACCOUNT_TYPE));
+                final boolean isPhoto = TextUtils.equals(Photo.CONTENT_ITEM_TYPE, kind.mimeType);
+                final boolean isGooglePhoto = isPhoto && isGoogleAccount;
+
+                if (RawContactModifier.isEmpty(entry, kind) && !isGooglePhoto) {
+                    if (DEBUG) {
+                        Log.v(TAG, "Trimming: " + entry.toString());
+                    }
+                    entry.markDeleted();
+                } else if (!entry.isFromTemplate()) {
+                    hasValues = true;
+                }
+            }
+        }
+        if (!hasValues) {
+            // Trim overall entity if no children exist
+            state.markDeleted();
+        }
+    }
+
+    private static boolean hasChanges(RawContactDelta state, AccountType accountType,
+            Set<String> excludedMimeTypes) {
+        for (DataKind kind : accountType.getSortedDataKinds()) {
+            final String mimeType = kind.mimeType;
+            if (excludedMimeTypes != null && excludedMimeTypes.contains(mimeType)) continue;
+            final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+            if (entries == null) continue;
+
+            for (ValuesDelta entry : entries) {
+                // An empty Insert must be ignored, because it won't save anything (an example
+                // is an empty name that stays empty)
+                final boolean isRealInsert = entry.isInsert() && !isEmpty(entry, kind);
+                if (isRealInsert || entry.isUpdate() || entry.isDelete()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Test if the given {@link ValuesDelta} would be considered "empty" in
+     * terms of {@link DataKind#fieldList}.
+     */
+    public static boolean isEmpty(ValuesDelta values, DataKind kind) {
+        if (Photo.CONTENT_ITEM_TYPE.equals(kind.mimeType)) {
+            return values.isInsert() && values.getAsByteArray(Photo.PHOTO) == null;
+        }
+
+        // No defined fields mean this row is always empty
+        if (kind.fieldList == null) return true;
+
+        for (EditField field : kind.fieldList) {
+            // If any field has values, we're not empty
+            final String value = values.getAsString(field.column);
+            if (ContactsUtils.isGraphic(value)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Compares corresponding fields in values1 and values2. Only the fields
+     * declared by the DataKind are taken into consideration.
+     */
+    protected static boolean areEqual(ValuesDelta values1, ContentValues values2, DataKind kind) {
+        if (kind.fieldList == null) return false;
+
+        for (EditField field : kind.fieldList) {
+            final String value1 = values1.getAsString(field.column);
+            final String value2 = values2.getAsString(field.column);
+            if (!TextUtils.equals(value1, value2)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Parse the given {@link Bundle} into the given {@link RawContactDelta} state,
+     * assuming the extras defined through {@link Intents}.
+     */
+    public static void parseExtras(Context context, AccountType accountType, RawContactDelta state,
+            Bundle extras) {
+        if (extras == null || extras.size() == 0) {
+            // Bail early if no useful data
+            return;
+        }
+
+        parseStructuredNameExtra(context, accountType, state, extras);
+        parseStructuredPostalExtra(accountType, state, extras);
+
+        {
+            // Phone
+            final DataKind kind = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+            parseExtras(state, kind, extras, Insert.PHONE_TYPE, Insert.PHONE, Phone.NUMBER);
+            parseExtras(state, kind, extras, Insert.SECONDARY_PHONE_TYPE, Insert.SECONDARY_PHONE,
+                    Phone.NUMBER);
+            parseExtras(state, kind, extras, Insert.TERTIARY_PHONE_TYPE, Insert.TERTIARY_PHONE,
+                    Phone.NUMBER);
+        }
+
+        {
+            // Email
+            final DataKind kind = accountType.getKindForMimetype(Email.CONTENT_ITEM_TYPE);
+            parseExtras(state, kind, extras, Insert.EMAIL_TYPE, Insert.EMAIL, Email.DATA);
+            parseExtras(state, kind, extras, Insert.SECONDARY_EMAIL_TYPE, Insert.SECONDARY_EMAIL,
+                    Email.DATA);
+            parseExtras(state, kind, extras, Insert.TERTIARY_EMAIL_TYPE, Insert.TERTIARY_EMAIL,
+                    Email.DATA);
+        }
+
+        {
+            // Im
+            final DataKind kind = accountType.getKindForMimetype(Im.CONTENT_ITEM_TYPE);
+            fixupLegacyImType(extras);
+            parseExtras(state, kind, extras, Insert.IM_PROTOCOL, Insert.IM_HANDLE, Im.DATA);
+        }
+
+        // Organization
+        final boolean hasOrg = extras.containsKey(Insert.COMPANY)
+                || extras.containsKey(Insert.JOB_TITLE);
+        final DataKind kindOrg = accountType.getKindForMimetype(Organization.CONTENT_ITEM_TYPE);
+        if (hasOrg && RawContactModifier.canInsert(state, kindOrg)) {
+            final ValuesDelta child = RawContactModifier.insertChild(state, kindOrg);
+
+            final String company = extras.getString(Insert.COMPANY);
+            if (ContactsUtils.isGraphic(company)) {
+                child.put(Organization.COMPANY, company);
+            }
+
+            final String title = extras.getString(Insert.JOB_TITLE);
+            if (ContactsUtils.isGraphic(title)) {
+                child.put(Organization.TITLE, title);
+            }
+        }
+
+        // Notes
+        final boolean hasNotes = extras.containsKey(Insert.NOTES);
+        final DataKind kindNotes = accountType.getKindForMimetype(Note.CONTENT_ITEM_TYPE);
+        if (hasNotes && RawContactModifier.canInsert(state, kindNotes)) {
+            final ValuesDelta child = RawContactModifier.insertChild(state, kindNotes);
+
+            final String notes = extras.getString(Insert.NOTES);
+            if (ContactsUtils.isGraphic(notes)) {
+                child.put(Note.NOTE, notes);
+            }
+        }
+
+        // Arbitrary additional data
+        ArrayList<ContentValues> values = extras.getParcelableArrayList(Insert.DATA);
+        if (values != null) {
+            parseValues(state, accountType, values);
+        }
+    }
+
+    private static void parseStructuredNameExtra(
+            Context context, AccountType accountType, RawContactDelta state, Bundle extras) {
+        // StructuredName
+        RawContactModifier.ensureKindExists(state, accountType, StructuredName.CONTENT_ITEM_TYPE);
+        final ValuesDelta child = state.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
+
+        final String name = extras.getString(Insert.NAME);
+        if (ContactsUtils.isGraphic(name)) {
+            final DataKind kind = accountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+            boolean supportsDisplayName = false;
+            if (kind.fieldList != null) {
+                for (EditField field : kind.fieldList) {
+                    if (StructuredName.DISPLAY_NAME.equals(field.column)) {
+                        supportsDisplayName = true;
+                        break;
+                    }
+                }
+            }
+
+            if (supportsDisplayName) {
+                child.put(StructuredName.DISPLAY_NAME, name);
+            } else {
+                Uri uri = ContactsContract.AUTHORITY_URI.buildUpon()
+                        .appendPath("complete_name")
+                        .appendQueryParameter(StructuredName.DISPLAY_NAME, name)
+                        .build();
+                Cursor cursor = context.getContentResolver().query(uri,
+                        new String[]{
+                                StructuredName.PREFIX,
+                                StructuredName.GIVEN_NAME,
+                                StructuredName.MIDDLE_NAME,
+                                StructuredName.FAMILY_NAME,
+                                StructuredName.SUFFIX,
+                        }, null, null, null);
+
+                if (cursor != null) {
+                    try {
+                        if (cursor.moveToFirst()) {
+                            child.put(StructuredName.PREFIX, cursor.getString(0));
+                            child.put(StructuredName.GIVEN_NAME, cursor.getString(1));
+                            child.put(StructuredName.MIDDLE_NAME, cursor.getString(2));
+                            child.put(StructuredName.FAMILY_NAME, cursor.getString(3));
+                            child.put(StructuredName.SUFFIX, cursor.getString(4));
+                        }
+                    } finally {
+                        cursor.close();
+                    }
+                }
+            }
+        }
+
+        final String phoneticName = extras.getString(Insert.PHONETIC_NAME);
+        if (ContactsUtils.isGraphic(phoneticName)) {
+            StructuredNameDataItem dataItem = NameConverter.parsePhoneticName(phoneticName, null);
+            child.put(StructuredName.PHONETIC_FAMILY_NAME, dataItem.getPhoneticFamilyName());
+            child.put(StructuredName.PHONETIC_MIDDLE_NAME, dataItem.getPhoneticMiddleName());
+            child.put(StructuredName.PHONETIC_GIVEN_NAME, dataItem.getPhoneticGivenName());
+        }
+    }
+
+    private static void parseStructuredPostalExtra(
+            AccountType accountType, RawContactDelta state, Bundle extras) {
+        // StructuredPostal
+        final DataKind kind = accountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
+        final ValuesDelta child = parseExtras(state, kind, extras, Insert.POSTAL_TYPE,
+                Insert.POSTAL, StructuredPostal.FORMATTED_ADDRESS);
+        String address = child == null ? null
+                : child.getAsString(StructuredPostal.FORMATTED_ADDRESS);
+        if (!TextUtils.isEmpty(address)) {
+            boolean supportsFormatted = false;
+            if (kind.fieldList != null) {
+                for (EditField field : kind.fieldList) {
+                    if (StructuredPostal.FORMATTED_ADDRESS.equals(field.column)) {
+                        supportsFormatted = true;
+                        break;
+                    }
+                }
+            }
+
+            if (!supportsFormatted) {
+                child.put(StructuredPostal.STREET, address);
+                child.putNull(StructuredPostal.FORMATTED_ADDRESS);
+            }
+        }
+    }
+
+    private static void parseValues(
+            RawContactDelta state, AccountType accountType,
+            ArrayList<ContentValues> dataValueList) {
+        for (ContentValues values : dataValueList) {
+            String mimeType = values.getAsString(Data.MIMETYPE);
+            if (TextUtils.isEmpty(mimeType)) {
+                Log.e(TAG, "Mimetype is required. Ignoring: " + values);
+                continue;
+            }
+
+            // Won't override the contact name
+            if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                continue;
+            } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                values.remove(PhoneDataItem.KEY_FORMATTED_PHONE_NUMBER);
+                final Integer type = values.getAsInteger(Phone.TYPE);
+                // If the provided phone number provides a custom phone type but not a label,
+                // replace it with mobile (by default) to avoid the "Enter custom label" from
+                // popping up immediately upon entering the ContactEditorFragment
+                if (type != null && type == Phone.TYPE_CUSTOM &&
+                        TextUtils.isEmpty(values.getAsString(Phone.LABEL))) {
+                    values.put(Phone.TYPE, Phone.TYPE_MOBILE);
+                }
+            }
+
+            DataKind kind = accountType.getKindForMimetype(mimeType);
+            if (kind == null) {
+                Log.e(TAG, "Mimetype not supported for account type "
+                        + accountType.getAccountTypeAndDataSet() + ". Ignoring: " + values);
+                continue;
+            }
+
+            ValuesDelta entry = ValuesDelta.fromAfter(values);
+            if (isEmpty(entry, kind)) {
+                continue;
+            }
+
+            ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+
+            if ((kind.typeOverallMax != 1) || GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                // Check for duplicates
+                boolean addEntry = true;
+                int count = 0;
+                if (entries != null && entries.size() > 0) {
+                    for (ValuesDelta delta : entries) {
+                        if (!delta.isDelete()) {
+                            if (areEqual(delta, values, kind)) {
+                                addEntry = false;
+                                break;
+                            }
+                            count++;
+                        }
+                    }
+                }
+
+                if (kind.typeOverallMax != -1 && count >= kind.typeOverallMax) {
+                    Log.e(TAG, "Mimetype allows at most " + kind.typeOverallMax
+                            + " entries. Ignoring: " + values);
+                    addEntry = false;
+                }
+
+                if (addEntry) {
+                    addEntry = adjustType(entry, entries, kind);
+                }
+
+                if (addEntry) {
+                    state.addEntry(entry);
+                }
+            } else {
+                // Non-list entries should not be overridden
+                boolean addEntry = true;
+                if (entries != null && entries.size() > 0) {
+                    for (ValuesDelta delta : entries) {
+                        if (!delta.isDelete() && !isEmpty(delta, kind)) {
+                            addEntry = false;
+                            break;
+                        }
+                    }
+                    if (addEntry) {
+                        for (ValuesDelta delta : entries) {
+                            delta.markDeleted();
+                        }
+                    }
+                }
+
+                if (addEntry) {
+                    addEntry = adjustType(entry, entries, kind);
+                }
+
+                if (addEntry) {
+                    state.addEntry(entry);
+                } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)){
+                    // Note is most likely to contain large amounts of text
+                    // that we don't want to drop on the ground.
+                    for (ValuesDelta delta : entries) {
+                        if (!isEmpty(delta, kind)) {
+                            delta.put(Note.NOTE, delta.getAsString(Note.NOTE) + "\n"
+                                    + values.getAsString(Note.NOTE));
+                            break;
+                        }
+                    }
+                } else {
+                    Log.e(TAG, "Will not override mimetype " + mimeType + ". Ignoring: "
+                            + values);
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks if the data kind allows addition of another entry (e.g. Exchange only
+     * supports two "work" phone numbers).  If not, tries to switch to one of the
+     * unused types.  If successful, returns true.
+     */
+    private static boolean adjustType(
+            ValuesDelta entry, ArrayList<ValuesDelta> entries, DataKind kind) {
+        if (kind.typeColumn == null || kind.typeList == null || kind.typeList.size() == 0) {
+            return true;
+        }
+
+        Integer typeInteger = entry.getAsInteger(kind.typeColumn);
+        int type = typeInteger != null ? typeInteger : kind.typeList.get(0).rawValue;
+
+        if (isTypeAllowed(type, entries, kind)) {
+            entry.put(kind.typeColumn, type);
+            return true;
+        }
+
+        // Specified type is not allowed - choose the first available type that is allowed
+        int size = kind.typeList.size();
+        for (int i = 0; i < size; i++) {
+            EditType editType = kind.typeList.get(i);
+            if (isTypeAllowed(editType.rawValue, entries, kind)) {
+                entry.put(kind.typeColumn, editType.rawValue);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks if a new entry of the specified type can be added to the raw
+     * contact. For example, Exchange only supports two "work" phone numbers, so
+     * addition of a third would not be allowed.
+     */
+    private static boolean isTypeAllowed(int type, ArrayList<ValuesDelta> entries, DataKind kind) {
+        int max = 0;
+        int size = kind.typeList.size();
+        for (int i = 0; i < size; i++) {
+            EditType editType = kind.typeList.get(i);
+            if (editType.rawValue == type) {
+                max = editType.specificMax;
+                break;
+            }
+        }
+
+        if (max == 0) {
+            // This type is not allowed at all
+            return false;
+        }
+
+        if (max == -1) {
+            // Unlimited instances of this type are allowed
+            return true;
+        }
+
+        return getEntryCountByType(entries, kind.typeColumn, type) < max;
+    }
+
+    /**
+     * Counts occurrences of the specified type in the supplied entry list.
+     *
+     * @return The count of occurrences of the type in the entry list. 0 if entries is
+     * {@literal null}
+     */
+    private static int getEntryCountByType(ArrayList<ValuesDelta> entries, String typeColumn,
+            int type) {
+        int count = 0;
+        if (entries != null) {
+            for (ValuesDelta entry : entries) {
+                Integer typeInteger = entry.getAsInteger(typeColumn);
+                if (typeInteger != null && typeInteger == type) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+    /**
+     * Attempt to parse legacy {@link Insert#IM_PROTOCOL} values, replacing them
+     * with updated values.
+     */
+    @SuppressWarnings("deprecation")
+    private static void fixupLegacyImType(Bundle bundle) {
+        final String encodedString = bundle.getString(Insert.IM_PROTOCOL);
+        if (encodedString == null) return;
+
+        try {
+            final Object protocol = android.provider.Contacts.ContactMethods
+                    .decodeImProtocol(encodedString);
+            if (protocol instanceof Integer) {
+                bundle.putInt(Insert.IM_PROTOCOL, (Integer)protocol);
+            } else {
+                bundle.putString(Insert.IM_PROTOCOL, (String)protocol);
+            }
+        } catch (IllegalArgumentException e) {
+            // Ignore exception when legacy parser fails
+        }
+    }
+
+    /**
+     * Parse a specific entry from the given {@link Bundle} and insert into the
+     * given {@link RawContactDelta}. Silently skips the insert when missing value
+     * or no valid {@link EditType} found.
+     *
+     * @param typeExtra {@link Bundle} key that holds the incoming
+     *            {@link EditType#rawValue} value.
+     * @param valueExtra {@link Bundle} key that holds the incoming value.
+     * @param valueColumn Column to write value into {@link ValuesDelta}.
+     */
+    public static ValuesDelta parseExtras(RawContactDelta state, DataKind kind, Bundle extras,
+            String typeExtra, String valueExtra, String valueColumn) {
+        final CharSequence value = extras.getCharSequence(valueExtra);
+
+        // Bail early if account type doesn't handle this MIME type
+        if (kind == null) return null;
+
+        // Bail when can't insert type, or value missing
+        final boolean canInsert = RawContactModifier.canInsert(state, kind);
+        final boolean validValue = (value != null && TextUtils.isGraphic(value));
+        if (!validValue || !canInsert) return null;
+
+        // Find exact type when requested, otherwise best available type
+        final boolean hasType = extras.containsKey(typeExtra);
+        final int typeValue = extras.getInt(typeExtra, hasType ? BaseTypes.TYPE_CUSTOM
+                : Integer.MIN_VALUE);
+        final EditType editType = RawContactModifier.getBestValidType(state, kind, true, typeValue);
+
+        // Create data row and fill with value
+        final ValuesDelta child = RawContactModifier.insertChild(state, kind, editType);
+        child.put(valueColumn, value.toString());
+
+        if (editType != null && editType.customColumn != null) {
+            // Write down label when custom type picked
+            final String customType = extras.getString(typeExtra);
+            child.put(editType.customColumn, customType);
+        }
+
+        return child;
+    }
+
+    /**
+     * Generic mime types with type support (e.g. TYPE_HOME).
+     * Here, "type support" means if the data kind has CommonColumns#TYPE or not. Data kinds which
+     * have their own migrate methods aren't listed here.
+     */
+    private static final Set<String> sGenericMimeTypesWithTypeSupport = new HashSet<String>(
+            Arrays.asList(Phone.CONTENT_ITEM_TYPE,
+                    Email.CONTENT_ITEM_TYPE,
+                    Im.CONTENT_ITEM_TYPE,
+                    Nickname.CONTENT_ITEM_TYPE,
+                    Website.CONTENT_ITEM_TYPE,
+                    Relation.CONTENT_ITEM_TYPE,
+                    SipAddress.CONTENT_ITEM_TYPE));
+    private static final Set<String> sGenericMimeTypesWithoutTypeSupport = new HashSet<String>(
+            Arrays.asList(Organization.CONTENT_ITEM_TYPE,
+                    Note.CONTENT_ITEM_TYPE,
+                    Photo.CONTENT_ITEM_TYPE,
+                    GroupMembership.CONTENT_ITEM_TYPE));
+    // CommonColumns.TYPE cannot be accessed as it is protected interface, so use
+    // Phone.TYPE instead.
+    private static final String COLUMN_FOR_TYPE  = Phone.TYPE;
+    private static final String COLUMN_FOR_LABEL  = Phone.LABEL;
+    private static final int TYPE_CUSTOM = Phone.TYPE_CUSTOM;
+
+    /**
+     * Migrates old RawContactDelta to newly created one with a new restriction supplied from
+     * newAccountType.
+     *
+     * This is only for account switch during account creation (which must be insert operation).
+     */
+    public static void migrateStateForNewContact(Context context,
+            RawContactDelta oldState, RawContactDelta newState,
+            AccountType oldAccountType, AccountType newAccountType) {
+        if (newAccountType == oldAccountType) {
+            // Just copying all data in oldState isn't enough, but we can still rely on a lot of
+            // shortcuts.
+            for (DataKind kind : newAccountType.getSortedDataKinds()) {
+                final String mimeType = kind.mimeType;
+                // The fields with short/long form capability must be treated properly.
+                if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    migrateStructuredName(context, oldState, newState, kind);
+                } else {
+                    List<ValuesDelta> entryList = oldState.getMimeEntries(mimeType);
+                    if (entryList != null && !entryList.isEmpty()) {
+                        for (ValuesDelta entry : entryList) {
+                            ContentValues values = entry.getAfter();
+                            if (values != null) {
+                                newState.addEntry(ValuesDelta.fromAfter(values));
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            // Migrate data supported by the new account type.
+            // All the other data inside oldState are silently dropped.
+            for (DataKind kind : newAccountType.getSortedDataKinds()) {
+                if (!kind.editable) continue;
+                final String mimeType = kind.mimeType;
+                if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType)
+                        || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) {
+                    // Ignore pseudo data.
+                    continue;
+                } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    migrateStructuredName(context, oldState, newState, kind);
+                } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    migratePostal(oldState, newState, kind);
+                } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    migrateEvent(oldState, newState, kind, null /* default Year */);
+                } else if (sGenericMimeTypesWithoutTypeSupport.contains(mimeType)) {
+                    migrateGenericWithoutTypeColumn(oldState, newState, kind);
+                } else if (sGenericMimeTypesWithTypeSupport.contains(mimeType)) {
+                    migrateGenericWithTypeColumn(oldState, newState, kind);
+                } else {
+                    throw new IllegalStateException("Unexpected editable mime-type: " + mimeType);
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks {@link DataKind#isList} and {@link DataKind#typeOverallMax}, and restricts
+     * the number of entries (ValuesDelta) inside newState.
+     */
+    private static ArrayList<ValuesDelta> ensureEntryMaxSize(RawContactDelta newState,
+            DataKind kind, ArrayList<ValuesDelta> mimeEntries) {
+        if (mimeEntries == null) {
+            return null;
+        }
+
+        final int typeOverallMax = kind.typeOverallMax;
+        if (typeOverallMax >= 0 && (mimeEntries.size() > typeOverallMax)) {
+            ArrayList<ValuesDelta> newMimeEntries = new ArrayList<ValuesDelta>(typeOverallMax);
+            for (int i = 0; i < typeOverallMax; i++) {
+                newMimeEntries.add(mimeEntries.get(i));
+            }
+            mimeEntries = newMimeEntries;
+        }
+        return mimeEntries;
+    }
+
+    /** @hide Public only for testing. */
+    public static void migrateStructuredName(
+            Context context, RawContactDelta oldState, RawContactDelta newState,
+            DataKind newDataKind) {
+        final ContentValues values =
+                oldState.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE).getAfter();
+        if (values == null) {
+            return;
+        }
+
+        boolean supportDisplayName = false;
+        boolean supportPhoneticFullName = false;
+        boolean supportPhoneticFamilyName = false;
+        boolean supportPhoneticMiddleName = false;
+        boolean supportPhoneticGivenName = false;
+        for (EditField editField : newDataKind.fieldList) {
+            if (StructuredName.DISPLAY_NAME.equals(editField.column)) {
+                supportDisplayName = true;
+            }
+            if (DataKind.PSEUDO_COLUMN_PHONETIC_NAME.equals(editField.column)) {
+                supportPhoneticFullName = true;
+            }
+            if (StructuredName.PHONETIC_FAMILY_NAME.equals(editField.column)) {
+                supportPhoneticFamilyName = true;
+            }
+            if (StructuredName.PHONETIC_MIDDLE_NAME.equals(editField.column)) {
+                supportPhoneticMiddleName = true;
+            }
+            if (StructuredName.PHONETIC_GIVEN_NAME.equals(editField.column)) {
+                supportPhoneticGivenName = true;
+            }
+        }
+
+        // DISPLAY_NAME <-> PREFIX, GIVEN_NAME, MIDDLE_NAME, FAMILY_NAME, SUFFIX
+        final String displayName = values.getAsString(StructuredName.DISPLAY_NAME);
+        if (!TextUtils.isEmpty(displayName)) {
+            if (!supportDisplayName) {
+                // Old data has a display name, while the new account doesn't allow it.
+                NameConverter.displayNameToStructuredName(context, displayName, values);
+
+                // We don't want to migrate unseen data which may confuse users after the creation.
+                values.remove(StructuredName.DISPLAY_NAME);
+            }
+        } else {
+            if (supportDisplayName) {
+                // Old data does not have display name, while the new account requires it.
+                values.put(StructuredName.DISPLAY_NAME,
+                        NameConverter.structuredNameToDisplayName(context, values));
+                for (String field : NameConverter.STRUCTURED_NAME_FIELDS) {
+                    values.remove(field);
+                }
+            }
+        }
+
+        // Phonetic (full) name <-> PHONETIC_FAMILY_NAME, PHONETIC_MIDDLE_NAME, PHONETIC_GIVEN_NAME
+        final String phoneticFullName = values.getAsString(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
+        if (!TextUtils.isEmpty(phoneticFullName)) {
+            if (!supportPhoneticFullName) {
+                // Old data has a phonetic (full) name, while the new account doesn't allow it.
+                final StructuredNameDataItem tmpItem =
+                        NameConverter.parsePhoneticName(phoneticFullName, null);
+                values.remove(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
+                if (supportPhoneticFamilyName) {
+                    values.put(StructuredName.PHONETIC_FAMILY_NAME,
+                            tmpItem.getPhoneticFamilyName());
+                } else {
+                    values.remove(StructuredName.PHONETIC_FAMILY_NAME);
+                }
+                if (supportPhoneticMiddleName) {
+                    values.put(StructuredName.PHONETIC_MIDDLE_NAME,
+                            tmpItem.getPhoneticMiddleName());
+                } else {
+                    values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
+                }
+                if (supportPhoneticGivenName) {
+                    values.put(StructuredName.PHONETIC_GIVEN_NAME,
+                            tmpItem.getPhoneticGivenName());
+                } else {
+                    values.remove(StructuredName.PHONETIC_GIVEN_NAME);
+                }
+            }
+        } else {
+            if (supportPhoneticFullName) {
+                // Old data does not have a phonetic (full) name, while the new account requires it.
+                values.put(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
+                        NameConverter.buildPhoneticName(
+                                values.getAsString(StructuredName.PHONETIC_FAMILY_NAME),
+                                values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME),
+                                values.getAsString(StructuredName.PHONETIC_GIVEN_NAME)));
+            }
+            if (!supportPhoneticFamilyName) {
+                values.remove(StructuredName.PHONETIC_FAMILY_NAME);
+            }
+            if (!supportPhoneticMiddleName) {
+                values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
+            }
+            if (!supportPhoneticGivenName) {
+                values.remove(StructuredName.PHONETIC_GIVEN_NAME);
+            }
+        }
+
+        newState.addEntry(ValuesDelta.fromAfter(values));
+    }
+
+    /** @hide Public only for testing. */
+    public static void migratePostal(RawContactDelta oldState, RawContactDelta newState,
+            DataKind newDataKind) {
+        final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
+                oldState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE));
+        if (mimeEntries == null || mimeEntries.isEmpty()) {
+            return;
+        }
+
+        boolean supportFormattedAddress = false;
+        boolean supportStreet = false;
+        final String firstColumn = newDataKind.fieldList.get(0).column;
+        for (EditField editField : newDataKind.fieldList) {
+            if (StructuredPostal.FORMATTED_ADDRESS.equals(editField.column)) {
+                supportFormattedAddress = true;
+            }
+            if (StructuredPostal.STREET.equals(editField.column)) {
+                supportStreet = true;
+            }
+        }
+
+        final Set<Integer> supportedTypes = new HashSet<Integer>();
+        if (newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) {
+            for (EditType editType : newDataKind.typeList) {
+                supportedTypes.add(editType.rawValue);
+            }
+        }
+
+        for (ValuesDelta entry : mimeEntries) {
+            final ContentValues values = entry.getAfter();
+            if (values == null) {
+                continue;
+            }
+            final Integer oldType = values.getAsInteger(StructuredPostal.TYPE);
+            if (!supportedTypes.contains(oldType)) {
+                int defaultType;
+                if (newDataKind.defaultValues != null) {
+                    defaultType = newDataKind.defaultValues.getAsInteger(StructuredPostal.TYPE);
+                } else {
+                    defaultType = newDataKind.typeList.get(0).rawValue;
+                }
+                values.put(StructuredPostal.TYPE, defaultType);
+                if (oldType != null && oldType == StructuredPostal.TYPE_CUSTOM) {
+                    values.remove(StructuredPostal.LABEL);
+                }
+            }
+
+            final String formattedAddress = values.getAsString(StructuredPostal.FORMATTED_ADDRESS);
+            if (!TextUtils.isEmpty(formattedAddress)) {
+                if (!supportFormattedAddress) {
+                    // Old data has a formatted address, while the new account doesn't allow it.
+                    values.remove(StructuredPostal.FORMATTED_ADDRESS);
+
+                    // Unlike StructuredName we don't have logic to split it, so first
+                    // try to use street field and. If the new account doesn't have one,
+                    // then select first one anyway.
+                    if (supportStreet) {
+                        values.put(StructuredPostal.STREET, formattedAddress);
+                    } else {
+                        values.put(firstColumn, formattedAddress);
+                    }
+                }
+            } else {
+                if (supportFormattedAddress) {
+                    // Old data does not have formatted address, while the new account requires it.
+                    // Unlike StructuredName we don't have logic to join multiple address values.
+                    // Use poor join heuristics for now.
+                    String[] structuredData;
+                    final boolean useJapaneseOrder =
+                            Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
+                    if (useJapaneseOrder) {
+                        structuredData = new String[] {
+                                values.getAsString(StructuredPostal.COUNTRY),
+                                values.getAsString(StructuredPostal.POSTCODE),
+                                values.getAsString(StructuredPostal.REGION),
+                                values.getAsString(StructuredPostal.CITY),
+                                values.getAsString(StructuredPostal.NEIGHBORHOOD),
+                                values.getAsString(StructuredPostal.STREET),
+                                values.getAsString(StructuredPostal.POBOX) };
+                    } else {
+                        structuredData = new String[] {
+                                values.getAsString(StructuredPostal.POBOX),
+                                values.getAsString(StructuredPostal.STREET),
+                                values.getAsString(StructuredPostal.NEIGHBORHOOD),
+                                values.getAsString(StructuredPostal.CITY),
+                                values.getAsString(StructuredPostal.REGION),
+                                values.getAsString(StructuredPostal.POSTCODE),
+                                values.getAsString(StructuredPostal.COUNTRY) };
+                    }
+                    final StringBuilder builder = new StringBuilder();
+                    for (String elem : structuredData) {
+                        if (!TextUtils.isEmpty(elem)) {
+                            builder.append(elem + "\n");
+                        }
+                    }
+                    values.put(StructuredPostal.FORMATTED_ADDRESS, builder.toString());
+
+                    values.remove(StructuredPostal.POBOX);
+                    values.remove(StructuredPostal.STREET);
+                    values.remove(StructuredPostal.NEIGHBORHOOD);
+                    values.remove(StructuredPostal.CITY);
+                    values.remove(StructuredPostal.REGION);
+                    values.remove(StructuredPostal.POSTCODE);
+                    values.remove(StructuredPostal.COUNTRY);
+                }
+            }
+
+            newState.addEntry(ValuesDelta.fromAfter(values));
+        }
+    }
+
+    /** @hide Public only for testing. */
+    public static void migrateEvent(RawContactDelta oldState, RawContactDelta newState,
+            DataKind newDataKind, Integer defaultYear) {
+        final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
+                oldState.getMimeEntries(Event.CONTENT_ITEM_TYPE));
+        if (mimeEntries == null || mimeEntries.isEmpty()) {
+            return;
+        }
+
+        final SparseArray<EventEditType> allowedTypes = new SparseArray<EventEditType>();
+        for (EditType editType : newDataKind.typeList) {
+            allowedTypes.put(editType.rawValue, (EventEditType) editType);
+        }
+        for (ValuesDelta entry : mimeEntries) {
+            final ContentValues values = entry.getAfter();
+            if (values == null) {
+                continue;
+            }
+            final String dateString = values.getAsString(Event.START_DATE);
+            final Integer type = values.getAsInteger(Event.TYPE);
+            if (type != null && (allowedTypes.indexOfKey(type) >= 0)
+                    && !TextUtils.isEmpty(dateString)) {
+                EventEditType suitableType = allowedTypes.get(type);
+
+                final ParsePosition position = new ParsePosition(0);
+                boolean yearOptional = false;
+                Date date = CommonDateUtils.DATE_AND_TIME_FORMAT.parse(dateString, position);
+                if (date == null) {
+                    yearOptional = true;
+                    date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(dateString, position);
+                }
+                if (date != null) {
+                    if (yearOptional && !suitableType.isYearOptional()) {
+                        // The new EditType doesn't allow optional year. Supply default.
+                        final Calendar calendar = Calendar.getInstance(DateUtils.UTC_TIMEZONE,
+                                Locale.US);
+                        if (defaultYear == null) {
+                            defaultYear = calendar.get(Calendar.YEAR);
+                        }
+                        calendar.setTime(date);
+                        final int month = calendar.get(Calendar.MONTH);
+                        final int day = calendar.get(Calendar.DAY_OF_MONTH);
+                        // Exchange requires 8:00 for birthdays
+                        calendar.set(defaultYear, month, day,
+                                CommonDateUtils.DEFAULT_HOUR, 0, 0);
+                        values.put(Event.START_DATE,
+                                CommonDateUtils.FULL_DATE_FORMAT.format(calendar.getTime()));
+                    }
+                }
+                newState.addEntry(ValuesDelta.fromAfter(values));
+            } else {
+                // Just drop it.
+            }
+        }
+    }
+
+    /** @hide Public only for testing. */
+    public static void migrateGenericWithoutTypeColumn(
+            RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
+        final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
+                oldState.getMimeEntries(newDataKind.mimeType));
+        if (mimeEntries == null || mimeEntries.isEmpty()) {
+            return;
+        }
+
+        for (ValuesDelta entry : mimeEntries) {
+            ContentValues values = entry.getAfter();
+            if (values != null) {
+                newState.addEntry(ValuesDelta.fromAfter(values));
+            }
+        }
+    }
+
+    /** @hide Public only for testing. */
+    public static void migrateGenericWithTypeColumn(
+            RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
+        final ArrayList<ValuesDelta> mimeEntries = oldState.getMimeEntries(newDataKind.mimeType);
+        if (mimeEntries == null || mimeEntries.isEmpty()) {
+            return;
+        }
+
+        // Note that type specified with the old account may be invalid with the new account, while
+        // we want to preserve its data as much as possible. e.g. if a user typed a phone number
+        // with a type which is valid with an old account but not with a new account, the user
+        // probably wants to have the number with default type, rather than seeing complete data
+        // loss.
+        //
+        // Specifically, this method works as follows:
+        // 1. detect defaultType
+        // 2. prepare constants & variables for iteration
+        // 3. iterate over mimeEntries:
+        // 3.1 stop iteration if total number of mimeEntries reached typeOverallMax specified in
+        //     DataKind
+        // 3.2 replace unallowed types with defaultType
+        // 3.3 check if the number of entries is below specificMax specified in AccountType
+
+        // Here, defaultType can be supplied in two ways
+        // - via kind.defaultValues
+        // - via kind.typeList.get(0).rawValue
+        Integer defaultType = null;
+        if (newDataKind.defaultValues != null) {
+            defaultType = newDataKind.defaultValues.getAsInteger(COLUMN_FOR_TYPE);
+        }
+        final Set<Integer> allowedTypes = new HashSet<Integer>();
+        // key: type, value: the number of entries allowed for the type (specificMax)
+        final SparseIntArray typeSpecificMaxMap = new SparseIntArray();
+        if (defaultType != null) {
+            allowedTypes.add(defaultType);
+            typeSpecificMaxMap.put(defaultType, -1);
+        }
+        // Note: typeList may be used in different purposes when defaultValues are specified.
+        // Especially in IM, typeList contains available protocols (e.g. PROTOCOL_GOOGLE_TALK)
+        // instead of "types" which we want to treate here (e.g. TYPE_HOME). So we don't add
+        // anything other than defaultType into allowedTypes and typeSpecificMapMax.
+        if (!Im.CONTENT_ITEM_TYPE.equals(newDataKind.mimeType) &&
+                newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) {
+            for (EditType editType : newDataKind.typeList) {
+                allowedTypes.add(editType.rawValue);
+                typeSpecificMaxMap.put(editType.rawValue, editType.specificMax);
+            }
+            if (defaultType == null) {
+                defaultType = newDataKind.typeList.get(0).rawValue;
+            }
+        }
+
+        if (defaultType == null) {
+            Log.w(TAG, "Default type isn't available for mimetype " + newDataKind.mimeType);
+        }
+
+        final int typeOverallMax = newDataKind.typeOverallMax;
+
+        // key: type, value: the number of current entries.
+        final SparseIntArray currentEntryCount = new SparseIntArray();
+        int totalCount = 0;
+
+        for (ValuesDelta entry : mimeEntries) {
+            if (typeOverallMax != -1 && totalCount >= typeOverallMax) {
+                break;
+            }
+
+            final ContentValues values = entry.getAfter();
+            if (values == null) {
+                continue;
+            }
+
+            final Integer oldType = entry.getAsInteger(COLUMN_FOR_TYPE);
+            final Integer typeForNewAccount;
+            if (!allowedTypes.contains(oldType)) {
+                // The new account doesn't support the type.
+                if (defaultType != null) {
+                    typeForNewAccount = defaultType.intValue();
+                    values.put(COLUMN_FOR_TYPE, defaultType.intValue());
+                    if (oldType != null && oldType == TYPE_CUSTOM) {
+                        values.remove(COLUMN_FOR_LABEL);
+                    }
+                } else {
+                    typeForNewAccount = null;
+                    values.remove(COLUMN_FOR_TYPE);
+                }
+            } else {
+                typeForNewAccount = oldType;
+            }
+            if (typeForNewAccount != null) {
+                final int specificMax = typeSpecificMaxMap.get(typeForNewAccount, 0);
+                if (specificMax >= 0) {
+                    final int currentCount = currentEntryCount.get(typeForNewAccount, 0);
+                    if (currentCount >= specificMax) {
+                        continue;
+                    }
+                    currentEntryCount.put(typeForNewAccount, currentCount + 1);
+                }
+            }
+            newState.addEntry(ValuesDelta.fromAfter(values));
+            totalCount++;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/model/ValuesDelta.java b/src/com/android/contacts/common/model/ValuesDelta.java
new file mode 100644
index 0000000..9023709
--- /dev/null
+++ b/src/com/android/contacts/common/model/ValuesDelta.java
@@ -0,0 +1,597 @@
+/*
+ * 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.model;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.BuilderWrapper;
+import com.android.contacts.common.testing.NeededForTesting;
+import com.google.common.collect.Sets;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Type of {@link android.content.ContentValues} that maintains both an original state and a
+ * modified version of that state. This allows us to build insert, update,
+ * or delete operations based on a "before" {@link Entity} snapshot.
+ */
+public class ValuesDelta implements Parcelable {
+    protected ContentValues mBefore;
+    protected ContentValues mAfter;
+    protected String mIdColumn = BaseColumns._ID;
+    private boolean mFromTemplate;
+
+    /**
+     * Next value to assign to {@link #mIdColumn} when building an insert
+     * operation through {@link #fromAfter(android.content.ContentValues)}. This is used so
+     * we can concretely reference this {@link ValuesDelta} before it has
+     * been persisted.
+     */
+    protected static int sNextInsertId = -1;
+
+    protected ValuesDelta() {
+    }
+
+    /**
+     * Create {@link ValuesDelta}, using the given object as the
+     * "before" state, usually from an {@link Entity}.
+     */
+    public static ValuesDelta fromBefore(ContentValues before) {
+        final ValuesDelta entry = new ValuesDelta();
+        entry.mBefore = before;
+        entry.mAfter = new ContentValues();
+        return entry;
+    }
+
+    /**
+     * Create {@link ValuesDelta}, using the given object as the "after"
+     * state, usually when we are inserting a row instead of updating.
+     */
+    public static ValuesDelta fromAfter(ContentValues after) {
+        final ValuesDelta entry = new ValuesDelta();
+        entry.mBefore = null;
+        entry.mAfter = after;
+
+        // Assign temporary id which is dropped before insert.
+        entry.mAfter.put(entry.mIdColumn, sNextInsertId--);
+        return entry;
+    }
+
+    @NeededForTesting
+    public ContentValues getAfter() {
+        return mAfter;
+    }
+
+    public boolean containsKey(String key) {
+        return ((mAfter != null && mAfter.containsKey(key)) ||
+                (mBefore != null && mBefore.containsKey(key)));
+    }
+
+    public String getAsString(String key) {
+        if (mAfter != null && mAfter.containsKey(key)) {
+            return mAfter.getAsString(key);
+        } else if (mBefore != null && mBefore.containsKey(key)) {
+            return mBefore.getAsString(key);
+        } else {
+            return null;
+        }
+    }
+
+    public byte[] getAsByteArray(String key) {
+        if (mAfter != null && mAfter.containsKey(key)) {
+            return mAfter.getAsByteArray(key);
+        } else if (mBefore != null && mBefore.containsKey(key)) {
+            return mBefore.getAsByteArray(key);
+        } else {
+            return null;
+        }
+    }
+
+    public Long getAsLong(String key) {
+        if (mAfter != null && mAfter.containsKey(key)) {
+            return mAfter.getAsLong(key);
+        } else if (mBefore != null && mBefore.containsKey(key)) {
+            return mBefore.getAsLong(key);
+        } else {
+            return null;
+        }
+    }
+
+    public Integer getAsInteger(String key) {
+        return getAsInteger(key, null);
+    }
+
+    public Integer getAsInteger(String key, Integer defaultValue) {
+        if (mAfter != null && mAfter.containsKey(key)) {
+            return mAfter.getAsInteger(key);
+        } else if (mBefore != null && mBefore.containsKey(key)) {
+            return mBefore.getAsInteger(key);
+        } else {
+            return defaultValue;
+        }
+    }
+
+    public boolean isChanged(String key) {
+        if (mAfter == null || !mAfter.containsKey(key)) {
+            return false;
+        }
+
+        Object newValue = mAfter.get(key);
+        Object oldValue = mBefore.get(key);
+
+        if (oldValue == null) {
+            return newValue != null;
+        }
+
+        return !oldValue.equals(newValue);
+    }
+
+    public String getMimetype() {
+        return getAsString(ContactsContract.Data.MIMETYPE);
+    }
+
+    public Long getId() {
+        return getAsLong(mIdColumn);
+    }
+
+    public void setIdColumn(String idColumn) {
+        mIdColumn = idColumn;
+    }
+
+    public boolean isPrimary() {
+        final Long isPrimary = getAsLong(ContactsContract.Data.IS_PRIMARY);
+        return isPrimary == null ? false : isPrimary != 0;
+    }
+
+    public void setFromTemplate(boolean isFromTemplate) {
+        mFromTemplate = isFromTemplate;
+    }
+
+    public boolean isFromTemplate() {
+        return mFromTemplate;
+    }
+
+    public boolean isSuperPrimary() {
+        final Long isSuperPrimary = getAsLong(ContactsContract.Data.IS_SUPER_PRIMARY);
+        return isSuperPrimary == null ? false : isSuperPrimary != 0;
+    }
+
+    public boolean beforeExists() {
+        return (mBefore != null && mBefore.containsKey(mIdColumn));
+    }
+
+    /**
+     * When "after" is present, then visible
+     */
+    public boolean isVisible() {
+        return (mAfter != null);
+    }
+
+    /**
+     * When "after" is wiped, action is "delete"
+     */
+    public boolean isDelete() {
+        return beforeExists() && (mAfter == null);
+    }
+
+    /**
+     * When no "before" or "after", is transient
+     */
+    public boolean isTransient() {
+        return (mBefore == null) && (mAfter == null);
+    }
+
+    /**
+     * When "after" has some changes, action is "update"
+     */
+    public boolean isUpdate() {
+        if (!beforeExists() || mAfter == null || mAfter.size() == 0) {
+            return false;
+        }
+        for (String key : mAfter.keySet()) {
+            Object newValue = mAfter.get(key);
+            Object oldValue = mBefore.get(key);
+            if (oldValue == null) {
+                if (newValue != null) {
+                    return true;
+                }
+            } else if (!oldValue.equals(newValue)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * When "after" has no changes, action is no-op
+     */
+    public boolean isNoop() {
+        return beforeExists() && (mAfter != null && mAfter.size() == 0);
+    }
+
+    /**
+     * When no "before" id, and has "after", action is "insert"
+     */
+    public boolean isInsert() {
+        return !beforeExists() && (mAfter != null);
+    }
+
+    public void markDeleted() {
+        mAfter = null;
+    }
+
+    /**
+     * Ensure that our internal structure is ready for storing updates.
+     */
+    private void ensureUpdate() {
+        if (mAfter == null) {
+            mAfter = new ContentValues();
+        }
+    }
+
+    public void put(String key, String value) {
+        ensureUpdate();
+        mAfter.put(key, value);
+    }
+
+    public void put(String key, byte[] value) {
+        ensureUpdate();
+        mAfter.put(key, value);
+    }
+
+    public void put(String key, int value) {
+        ensureUpdate();
+        mAfter.put(key, value);
+    }
+
+    public void put(String key, long value) {
+        ensureUpdate();
+        mAfter.put(key, value);
+    }
+
+    public void putNull(String key) {
+        ensureUpdate();
+        mAfter.putNull(key);
+    }
+
+    public void copyStringFrom(ValuesDelta from, String key) {
+        ensureUpdate();
+        if (containsKey(key) || from.containsKey(key)) {
+            put(key, from.getAsString(key));
+        }
+    }
+
+    /**
+     * Return set of all keys defined through this object.
+     */
+    public Set<String> keySet() {
+        final HashSet<String> keys = Sets.newHashSet();
+
+        if (mBefore != null) {
+            for (Map.Entry<String, Object> entry : mBefore.valueSet()) {
+                keys.add(entry.getKey());
+            }
+        }
+
+        if (mAfter != null) {
+            for (Map.Entry<String, Object> entry : mAfter.valueSet()) {
+                keys.add(entry.getKey());
+            }
+        }
+
+        return keys;
+    }
+
+    /**
+     * Return complete set of "before" and "after" values mixed together,
+     * giving full state regardless of edits.
+     */
+    public ContentValues getCompleteValues() {
+        final ContentValues values = new ContentValues();
+        if (mBefore != null) {
+            values.putAll(mBefore);
+        }
+        if (mAfter != null) {
+            values.putAll(mAfter);
+        }
+        if (values.containsKey(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID)) {
+            // Clear to avoid double-definitions, and prefer rows
+            values.remove(ContactsContract.CommonDataKinds.GroupMembership.GROUP_SOURCE_ID);
+        }
+
+        return values;
+    }
+
+    /**
+     * Merge the "after" values from the given {@link ValuesDelta},
+     * discarding any existing "after" state. This is typically used when
+     * re-parenting changes onto an updated {@link Entity}.
+     */
+    public static ValuesDelta mergeAfter(ValuesDelta local, ValuesDelta remote) {
+        // Bail early if trying to merge delete with missing local
+        if (local == null && (remote.isDelete() || remote.isTransient())) return null;
+
+        // Create local version if none exists yet
+        if (local == null) local = new ValuesDelta();
+
+        if (!local.beforeExists()) {
+            // Any "before" record is missing, so take all values as "insert"
+            local.mAfter = remote.getCompleteValues();
+        } else {
+            // Existing "update" with only "after" values
+            local.mAfter = remote.mAfter;
+        }
+
+        return local;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (object instanceof ValuesDelta) {
+            // Only exactly equal with both are identical subsets
+            final ValuesDelta other = (ValuesDelta)object;
+            return this.subsetEquals(other) && other.subsetEquals(this);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        toString(builder);
+        return builder.toString();
+    }
+
+    /**
+     * Helper for building string representation, leveraging the given
+     * {@link StringBuilder} to minimize allocations.
+     */
+    public void toString(StringBuilder builder) {
+        builder.append("{ ");
+        builder.append("IdColumn=");
+        builder.append(mIdColumn);
+        builder.append(", FromTemplate=");
+        builder.append(mFromTemplate);
+        builder.append(", ");
+        for (String key : this.keySet()) {
+            builder.append(key);
+            builder.append("=");
+            builder.append(this.getAsString(key));
+            builder.append(", ");
+        }
+        builder.append("}");
+    }
+
+    /**
+     * Check if the given {@link ValuesDelta} is both a subset of this
+     * object, and any defined keys have equal values.
+     */
+    public boolean subsetEquals(ValuesDelta other) {
+        for (String key : this.keySet()) {
+            final String ourValue = this.getAsString(key);
+            final String theirValue = other.getAsString(key);
+            if (ourValue == null) {
+                // If they have value when we're null, no match
+                if (theirValue != null) return false;
+            } else {
+                // If both values defined and aren't equal, no match
+                if (!ourValue.equals(theirValue)) return false;
+            }
+        }
+        // All values compared and matched
+        return true;
+    }
+
+    /**
+     * Build a {@link android.content.ContentProviderOperation} that will transform our
+     * "before" state into our "after" state, using insert, update, or
+     * delete as needed.
+     */
+    public ContentProviderOperation.Builder buildDiff(Uri targetUri) {
+        return buildDiffHelper(targetUri);
+    }
+
+    /**
+     * For compatibility purpose.
+     */
+    public BuilderWrapper buildDiffWrapper(Uri targetUri) {
+        final ContentProviderOperation.Builder builder = buildDiffHelper(targetUri);
+        BuilderWrapper bw = null;
+        if (isInsert()) {
+            bw = new BuilderWrapper(builder, CompatUtils.TYPE_INSERT);
+        } else if (isDelete()) {
+            bw = new BuilderWrapper(builder, CompatUtils.TYPE_DELETE);
+        } else if (isUpdate()) {
+            bw = new BuilderWrapper(builder, CompatUtils.TYPE_UPDATE);
+        }
+        return bw;
+    }
+
+    private ContentProviderOperation.Builder buildDiffHelper(Uri targetUri) {
+        ContentProviderOperation.Builder builder = null;
+        if (isInsert()) {
+            // Changed values are "insert" back-referenced to Contact
+            mAfter.remove(mIdColumn);
+            builder = ContentProviderOperation.newInsert(targetUri);
+            builder.withValues(mAfter);
+        } else if (isDelete()) {
+            // When marked for deletion and "before" exists, then "delete"
+            builder = ContentProviderOperation.newDelete(targetUri);
+            builder.withSelection(mIdColumn + "=" + getId(), null);
+        } else if (isUpdate()) {
+            // When has changes and "before" exists, then "update"
+            builder = ContentProviderOperation.newUpdate(targetUri);
+            builder.withSelection(mIdColumn + "=" + getId(), null);
+            builder.withValues(mAfter);
+        }
+        return builder;
+    }
+
+    /** {@inheritDoc} */
+    public int describeContents() {
+        // Nothing special about this parcel
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mBefore, flags);
+        dest.writeParcelable(mAfter, flags);
+        dest.writeString(mIdColumn);
+    }
+
+    public void readFromParcel(Parcel source) {
+        final ClassLoader loader = getClass().getClassLoader();
+        mBefore = source.<ContentValues> readParcelable(loader);
+        mAfter = source.<ContentValues> readParcelable(loader);
+        mIdColumn = source.readString();
+    }
+
+    public static final Creator<ValuesDelta> CREATOR = new Creator<ValuesDelta>() {
+        public ValuesDelta createFromParcel(Parcel in) {
+            final ValuesDelta values = new ValuesDelta();
+            values.readFromParcel(in);
+            return values;
+        }
+
+        public ValuesDelta[] newArray(int size) {
+            return new ValuesDelta[size];
+        }
+    };
+
+    public void setGroupRowId(long groupId) {
+        put(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId);
+    }
+
+    public Long getGroupRowId() {
+        return getAsLong(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID);
+    }
+
+    public void setPhoto(byte[] value) {
+        put(ContactsContract.CommonDataKinds.Photo.PHOTO, value);
+    }
+
+    public byte[] getPhoto() {
+        return getAsByteArray(ContactsContract.CommonDataKinds.Photo.PHOTO);
+    }
+
+    public void setSuperPrimary(boolean val) {
+        if (val) {
+            put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
+        } else {
+            put(ContactsContract.Data.IS_SUPER_PRIMARY, 0);
+        }
+    }
+
+    public void setPhoneticFamilyName(String value) {
+        put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, value);
+    }
+
+    public void setPhoneticMiddleName(String value) {
+        put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME, value);
+    }
+
+    public void setPhoneticGivenName(String value) {
+        put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, value);
+    }
+
+    public String getPhoneticFamilyName() {
+        return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME);
+    }
+
+    public String getPhoneticMiddleName() {
+        return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME);
+    }
+
+    public String getPhoneticGivenName() {
+        return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME);
+    }
+
+    public String getDisplayName() {
+        return getAsString(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
+    }
+
+    public void setDisplayName(String name) {
+        if (name == null) {
+            putNull(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
+        } else {
+            put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
+        }
+    }
+
+    public void copyStructuredNameFieldsFrom(ValuesDelta name) {
+        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
+
+        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME);
+        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME);
+        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PREFIX);
+        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME);
+        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.SUFFIX);
+
+        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME);
+        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME);
+        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME);
+
+        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.FULL_NAME_STYLE);
+        copyStringFrom(name, ContactsContract.Data.DATA11);
+    }
+
+    public String getPhoneNumber() {
+        return getAsString(ContactsContract.CommonDataKinds.Phone.NUMBER);
+    }
+
+    public String getPhoneNormalizedNumber() {
+        return getAsString(ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER);
+    }
+
+    public boolean hasPhoneType() {
+        return getPhoneType() != null;
+    }
+
+    public Integer getPhoneType() {
+        return getAsInteger(ContactsContract.CommonDataKinds.Phone.TYPE);
+    }
+
+    public String getPhoneLabel() {
+        return getAsString(ContactsContract.CommonDataKinds.Phone.LABEL);
+    }
+
+    public String getEmailData() {
+        return getAsString(ContactsContract.CommonDataKinds.Email.DATA);
+    }
+
+    public boolean hasEmailType() {
+        return getEmailType() != null;
+    }
+
+    public Integer getEmailType() {
+        return getAsInteger(ContactsContract.CommonDataKinds.Email.TYPE);
+    }
+
+    public String getEmailLabel() {
+        return getAsString(ContactsContract.CommonDataKinds.Email.LABEL);
+    }
+}
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..8b50d79
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/AccountType.java
@@ -0,0 +1,526 @@
+/*
+ * 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;
+    }
+
+    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) {
+        return getDisplayIcon(context, titleRes, iconRes, syncAdapterPackageName);
+    }
+
+    public static Drawable getDisplayIcon(Context context, int titleRes, int iconRes,
+            String syncAdapterPackageName) {
+        if (titleRes != -1 && syncAdapterPackageName != null) {
+            final PackageManager pm = context.getPackageManager();
+            return pm.getDrawable(syncAdapterPackageName, iconRes, null);
+        } else if (titleRes != -1) {
+            return context.getResources().getDrawable(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..5947647
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/AccountWithDataSet.java
@@ -0,0 +1,239 @@
+/*
+ * 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.Parcelable;
+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 implements Parcelable {
+    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 name;
+    public final String type;
+    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 static final String LOCAL_ACCOUNT_SELECTION = RawContacts.ACCOUNT_TYPE + " IS NULL AND "
+            + RawContacts.ACCOUNT_NAME + " IS NULL AND "
+            + RawContacts.DATA_SET + " IS NULL";
+
+    public AccountWithDataSet(String name, String type, String dataSet) {
+        this.name = emptyToNull(name);
+        this.type = emptyToNull(type);
+        this.dataSet = emptyToNull(dataSet);
+        mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
+    }
+
+    private static final String emptyToNull(String text) {
+        return TextUtils.isEmpty(text) ? null : text;
+    }
+
+    public AccountWithDataSet(Parcel in) {
+        this.name = in.readString();
+        this.type = in.readString();
+        this.dataSet = in.readString();
+        mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
+    }
+
+    public boolean isLocalAccount() {
+        return name == null && type == null && dataSet == null;
+    }
+
+    public static AccountWithDataSet getLocalAccount() {
+        return new AccountWithDataSet(null, null, null);
+    }
+
+    public Account getAccountOrNull() {
+        if (name != null && type != null) {
+            return new Account(name, type);
+        }
+        return null;
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(name);
+        dest.writeString(type);
+        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) {
+        String selection;
+        String[] args = null;
+        if (isLocalAccount()) {
+            selection = LOCAL_ACCOUNT_SELECTION;
+        } else {
+            final String BASE_SELECTION =
+                    RawContacts.ACCOUNT_TYPE + " = ?" + " AND " + RawContacts.ACCOUNT_NAME + " = ?";
+            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();
+        }
+    }
+
+    public boolean equals(Object obj) {
+        if (obj instanceof AccountWithDataSet) {
+            AccountWithDataSet other = (AccountWithDataSet) obj;
+            return Objects.equal(name, other.name)
+                    && Objects.equal(type, other.type)
+                    && Objects.equal(dataSet, other.dataSet);
+        }
+        return false;
+    }
+
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + (name != null ? name.hashCode() : 0);
+        result = 31 * result + (type != null ? type.hashCode() : 0);
+        result = 31 * result + (dataSet != null ? dataSet.hashCode() : 0);
+        return result;
+    }
+
+    public String toString() {
+        return "AccountWithDataSet {name=" + name + ", type=" + type + ", dataSet=" + dataSet + "}";
+    }
+
+    private static StringBuilder addStringified(StringBuilder sb, AccountWithDataSet account) {
+        if (!TextUtils.isEmpty(account.name)) sb.append(account.name);
+        sb.append(STRINGIFY_SEPARATOR);
+        if (!TextUtils.isEmpty(account.type)) 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..6481c06
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/BaseAccountType.java
@@ -0,0 +1,1488 @@
+/*
+ * 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.testing.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";
+    }
+
+    protected interface Weight {
+        static final int NONE = -1;
+        static final int PHONE = 10;
+        static final int EMAIL = 15;
+        static final int STRUCTURED_POSTAL = 25;
+        static final int NICKNAME = 111;
+        static final int EVENT = 120;
+        static final int ORGANIZATION = 125;
+        static final int NOTE = 130;
+        static final int IM = 140;
+        static final int SIP_ADDRESS = 145;
+        static final int GROUP_MEMBERSHIP = 150;
+        static final int WEBSITE = 160;
+        static final int RELATIONSHIP = 999;
+    }
+
+    public BaseAccountType() {
+        this.accountType = null;
+        this.dataSet = null;
+        this.titleRes = R.string.account_phone;
+        this.iconRes = R.mipmap.ic_contacts_launcher;
+    }
+
+    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, Weight.NONE, true));
+        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, Weight.NONE, true));
+        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, Weight.NONE, true));
+        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, Weight.NICKNAME, true));
+        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,
+                Weight.PHONE, true));
+        kind.iconAltRes = R.drawable.ic_message_24dp_mirrored;
+        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,
+                Weight.EMAIL, true));
+        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, Weight.STRUCTURED_POSTAL, true));
+        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,
+                Weight.IM, true));
+        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, Weight.ORGANIZATION, true));
+        kind.actionHeader = new SimpleInflater(R.string.organizationLabelsGroup);
+        kind.actionBody = ORGANIZATION_BODY_INFLATER;
+        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, Weight.NONE, true));
+        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,
+                Weight.NOTE, true));
+        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, Weight.WEBSITE, true));
+        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, Weight.SIP_ADDRESS, true));
+
+        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));
+        kind.typeOverallMax = 1;
+
+        return kind;
+    }
+
+    protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(GroupMembership.CONTENT_ITEM_TYPE,
+                R.string.groupsLabel, Weight.GROUP_MEMBERSHIP, true));
+
+        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;
+            }
+        }
+    }
+
+    public static final StringInflater ORGANIZATION_BODY_INFLATER = new StringInflater() {
+        @Override
+        public CharSequence inflateUsing(Context context, ContentValues values) {
+            final CharSequence companyValue = values.containsKey(Organization.COMPANY) ?
+                    values.getAsString(Organization.COMPANY) : null;
+            final CharSequence titleValue = values.containsKey(Organization.TITLE) ?
+                    values.getAsString(Organization.TITLE) : null;
+
+            if (companyValue != null && titleValue != null) {
+                return companyValue +  ": " + titleValue;
+            } else if (companyValue == null) {
+                return titleValue;
+            } else {
+                return companyValue;
+            }
+        }
+    };
+
+    @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, 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);
+            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,
+                    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,
+                    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,
+                    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,
+                    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,
+                    new PhoneActionInflater(), new SimpleInflater(Phone.NUMBER));
+
+            kind.iconAltRes = R.drawable.ic_message_24dp_mirrored;
+            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,
+                    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,
+                    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,
+                    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,
+                    new SimpleInflater(R.string.organizationLabelsGroup),
+                    ORGANIZATION_BODY_INFLATER);
+
+            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 */, 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,
+                    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,
+                    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,
+                    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, 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,
+                    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,
+                    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..7020836
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/ExchangeAccountType.java
@@ -0,0 +1,349 @@
+/*
+ * 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";
+
+    private static final String ACCOUNT_TYPE_AOSP = "com.android.exchange";
+    private static final String ACCOUNT_TYPE_GOOGLE_1 = "com.google.android.exchange";
+    private static final String ACCOUNT_TYPE_GOOGLE_2 = "com.google.android.gm.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_1.equals(type)
+                || ACCOUNT_TYPE_GOOGLE_2.equals(type);
+    }
+
+    @Override
+    protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
+        DataKind kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+                R.string.nameLabelsGroup, Weight.NONE, true));
+        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, Weight.NONE, true));
+
+        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, Weight.NONE, true));
+        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,
+                Weight.EVENT, true));
+        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..1298fb3
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/ExternalAccountType.java
@@ -0,0 +1,441 @@
+/*
+ * 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.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+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.R;
+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 SYNC_META_DATA = "android.content.SyncAdapter";
+
+    /**
+     * The metadata name for so-called "contacts.xml".
+     *
+     * On LMP and later, we also accept the "alternate" name.
+     * This is to allow sync adapters to have a contacts.xml without making it visible on older
+     * platforms. If you modify this also update the corresponding list in
+     * ContactsProvider/PhotoPriorityResolver
+     */
+    private static final String[] METADATA_CONTACTS_NAMES = new String[] {
+            "android.provider.ALTERNATE_CONTACTS_STRUCTURE",
+            "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_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 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 XmlResourceParser parser;
+        if (injectedMetadata == null) {
+            parser = loadContactsXml(context, packageName);
+        } 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.
+     *
+     * This method looks through all services in the package that handle sync adapter
+     * intents for the first one that contains CONTACTS_STRUCTURE metadata. We have to look
+     * through all sync adapters in the package in case there are contacts and other sync
+     * adapters (eg, calendar) in the same package.
+     *
+     * Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata.  In this case
+     * the account type *will* be initialized with minimal configuration.
+     */
+    public static XmlResourceParser loadContactsXml(Context context, String resPackageName) {
+        final PackageManager pm = context.getPackageManager();
+        final Intent intent = new Intent(SYNC_META_DATA).setPackage(resPackageName);
+        final List<ResolveInfo> intentServices = pm.queryIntentServices(intent,
+                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+
+        if (intentServices != null) {
+            for (final ResolveInfo resolveInfo : intentServices) {
+                final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+                if (serviceInfo == null) {
+                    continue;
+                }
+                for (String metadataName : METADATA_CONTACTS_NAMES) {
+                    final XmlResourceParser parser = serviceInfo.loadXmlMetaData(
+                            pm, metadataName);
+                    if (parser != null) {
+                        if (Log.isLoggable(TAG, Log.DEBUG)) {
+                            Log.d(TAG, String.format("Metadata loaded from: %s, %s, %s",
+                                    serviceInfo.packageName, serviceInfo.name,
+                                    metadataName));
+                        }
+                        return parser;
+                    }
+                }
+            }
+        }
+
+        // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata.
+        return null;
+    }
+
+    /**
+     * Returns {@code TRUE} if the package contains CONTACTS_STRUCTURE metadata.
+     */
+    public static boolean hasContactsXml(Context context, String resPackageName) {
+        return loadContactsXml(context, resPackageName) != 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 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_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,
+                            R.styleable.ContactsDataKind);
+                    final DataKind kind = new DataKind();
+
+                    kind.mimeType = a
+                            .getString(R.styleable.ContactsDataKind_android_mimeType);
+                    final String summaryColumn = a.getString(
+                            R.styleable.ContactsDataKind_android_summaryColumn);
+                    if (summaryColumn != null) {
+                        // Inflate a specific column as summary when requested
+                        kind.actionHeader = new SimpleInflater(summaryColumn);
+                    }
+                    final String detailColumn = a.getString(
+                            R.styleable.ContactsDataKind_android_detailColumn);
+                    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..42e6b6a
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/FallbackAccountType.java
@@ -0,0 +1,79 @@
+/*
+ * 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.testing.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.drawable.ic_device;
+
+        // 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);
+            addDataKindGroupMembership(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..8f7f172
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/GoogleAccountType.java
@@ -0,0 +1,204 @@
+/*
+ * 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";
+
+    /**
+     * The package name that we should load contacts.xml from and rely on to handle
+     * G+ account actions. Even though this points to gms, in some cases gms will still hand
+     * off responsibility to the G+ app.
+     */
+    public static final String PLUS_EXTENSION_PACKAGE_NAME = "com.google.android.gms";
+
+    public static final String ACCOUNT_TYPE = "com.google";
+
+    private static final List<String> mExtensionPackages =
+            Lists.newArrayList(PLUS_EXTENSION_PACKAGE_NAME);
+
+    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, Weight.RELATIONSHIP, true));
+        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, Weight.EVENT, true));
+        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/account/SamsungAccountType.java b/src/com/android/contacts/common/model/account/SamsungAccountType.java
new file mode 100644
index 0000000..85a9ab8
--- /dev/null
+++ b/src/com/android/contacts/common/model/account/SamsungAccountType.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2015 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 com.google.common.collect.Lists;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+
+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.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.util.Log;
+
+import java.util.Locale;
+
+/**
+ * A writable account type that can be used to support samsung contacts. This may not perfectly
+ * match Samsung's latest intended account schema.
+ *
+ * This is only used to partially support Samsung accounts. The DataKind labels & fields are
+ * setup to support the values used by Samsung. But, not everything in the Samsung account type is
+ * supported. The Samsung account type includes a "Message Type" mimetype that we have no intention
+ * of showing inside the Contact editor. Similarly, we don't handle the "Ringtone" mimetype here
+ * since managing ringtones is handled in a different flow.
+ */
+public class SamsungAccountType extends BaseAccountType {
+    private static final String TAG = "KnownExternalAccountType";
+    private static final String ACCOUNT_TYPE_SAMSUNG = "com.osp.app.signin";
+
+    public SamsungAccountType(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);
+            addDataKindWebsite(context);
+            addDataKindGroupMembership(context);
+            addDataKindRelation(context);
+            addDataKindEvent(context);
+
+            mIsInitialized = true;
+        } catch (DefinitionException e) {
+            Log.e(TAG, "Problem building account type", e);
+        }
+    }
+
+    /**
+     * Returns {@code TRUE} if this is samsung's account type and Samsung hasn't bothered to
+     * define a contacts.xml to provide a more accurate definition than ours.
+     */
+    public static boolean isSamsungAccountType(Context context, String type,
+            String packageName) {
+        return ACCOUNT_TYPE_SAMSUNG.equals(type)
+                && !ExternalAccountType.hasContactsXml(context, packageName);
+    }
+
+    @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 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_HOME));
+        kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+        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_RADIO).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));
+        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));
+        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;
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/DataItem.java b/src/com/android/contacts/common/model/dataitem/DataItem.java
new file mode 100644
index 0000000..4e66e32
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/DataItem.java
@@ -0,0 +1,246 @@
+/*
+ * 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.model.dataitem;
+
+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.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+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.provider.ContactsContract.Contacts.Data;
+import android.provider.ContactsContract.Contacts.Entity;
+
+import com.android.contacts.common.Collapser;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.model.RawContactModifier;
+
+/**
+ * This is the base class for data items, which represents a row from the Data table.
+ */
+public class DataItem implements Collapser.Collapsible<DataItem> {
+
+    private final ContentValues mContentValues;
+    protected DataKind mKind;
+
+    protected DataItem(ContentValues values) {
+        mContentValues = values;
+    }
+
+    /**
+     * Factory for creating subclasses of DataItem objects based on the mimetype in the
+     * content values.  Raw contact is the raw contact that this data item is associated with.
+     */
+    public static DataItem createFrom(ContentValues values) {
+        final String mimeType = values.getAsString(Data.MIMETYPE);
+        if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new GroupMembershipDataItem(values);
+        } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new StructuredNameDataItem(values);
+        } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new PhoneDataItem(values);
+        } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new EmailDataItem(values);
+        } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new StructuredPostalDataItem(values);
+        } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new ImDataItem(values);
+        } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new OrganizationDataItem(values);
+        } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new NicknameDataItem(values);
+        } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new NoteDataItem(values);
+        } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new WebsiteDataItem(values);
+        } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new SipAddressDataItem(values);
+        } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new EventDataItem(values);
+        } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new RelationDataItem(values);
+        } else if (Identity.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new IdentityDataItem(values);
+        } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+            return new PhotoDataItem(values);
+        }
+
+        // generic
+        return new DataItem(values);
+    }
+
+    public ContentValues getContentValues() {
+        return mContentValues;
+    }
+
+    public void setRawContactId(long rawContactId) {
+        mContentValues.put(Data.RAW_CONTACT_ID, rawContactId);
+    }
+
+    public Long getRawContactId() {
+        return mContentValues.getAsLong(Data.RAW_CONTACT_ID);
+    }
+
+    /**
+     * Returns the data id.
+     */
+    public long getId() {
+        return mContentValues.getAsLong(Data._ID);
+    }
+
+    /**
+     * Returns the mimetype of the data.
+     */
+    public String getMimeType() {
+        return mContentValues.getAsString(Data.MIMETYPE);
+    }
+
+    public void setMimeType(String mimeType) {
+        mContentValues.put(Data.MIMETYPE, mimeType);
+    }
+
+    public boolean isPrimary() {
+        Integer primary = mContentValues.getAsInteger(Data.IS_PRIMARY);
+        return primary != null && primary != 0;
+    }
+
+    public boolean isSuperPrimary() {
+        Integer superPrimary = mContentValues.getAsInteger(Data.IS_SUPER_PRIMARY);
+        return superPrimary != null && superPrimary != 0;
+    }
+
+    public boolean hasKindTypeColumn(DataKind kind) {
+        final String key = kind.typeColumn;
+        return key != null && mContentValues.containsKey(key) &&
+            mContentValues.getAsInteger(key) != null;
+    }
+
+    public int getKindTypeColumn(DataKind kind) {
+        final String key = kind.typeColumn;
+        return mContentValues.getAsInteger(key);
+    }
+
+    /**
+     * Indicates the carrier presence value for the current {@link DataItem}.
+     *
+     * @return {@link Data#CARRIER_PRESENCE_VT_CAPABLE} if the {@link DataItem} supports carrier
+     *      video calling, {@code 0} otherwise.
+     */
+    public int getCarrierPresence() {
+        final Integer value = mContentValues.getAsInteger(Data.CARRIER_PRESENCE);
+        return value != null ? value.intValue() : 0;
+    }
+
+    /**
+     * This builds the data string depending on the type of data item by using the generic
+     * DataKind object underneath.
+     */
+    public String buildDataString(Context context, DataKind kind) {
+        if (kind.actionBody == null) {
+            return null;
+        }
+        CharSequence actionBody = kind.actionBody.inflateUsing(context, mContentValues);
+        return actionBody == null ? null : actionBody.toString();
+    }
+
+    /**
+     * This builds the data string(intended for display) depending on the type of data item. It
+     * returns the same value as {@link #buildDataString} by default, but certain data items can
+     * override it to provide their version of formatted data strings.
+     *
+     * @return Data string representing the data item, possibly formatted for display
+     */
+    public String buildDataStringForDisplay(Context context, DataKind kind) {
+        return buildDataString(context, kind);
+    }
+
+    public void setDataKind(DataKind kind) {
+        mKind = kind;
+    }
+
+    public DataKind getDataKind() {
+        return mKind;
+    }
+
+    public Integer getTimesUsed() {
+        return mContentValues.getAsInteger(Entity.TIMES_USED);
+    }
+
+    public Long getLastTimeUsed() {
+        return mContentValues.getAsLong(Entity.LAST_TIME_USED);
+    }
+
+    @Override
+    public void collapseWith(DataItem that) {
+        DataKind thisKind = getDataKind();
+        DataKind thatKind = that.getDataKind();
+        // If this does not have a type and that does, or if that's type is higher precedence,
+        // use that's type
+        if ((!hasKindTypeColumn(thisKind) && that.hasKindTypeColumn(thatKind)) ||
+                that.hasKindTypeColumn(thatKind) &&
+                RawContactModifier.getTypePrecedence(thisKind, getKindTypeColumn(thisKind))
+                >
+                RawContactModifier.getTypePrecedence(thatKind, that.getKindTypeColumn(thatKind))) {
+            mContentValues.put(thatKind.typeColumn, that.getKindTypeColumn(thatKind));
+            mKind = thatKind;
+        }
+
+        // Choose the max of the maxLines and maxLabelLines values.
+        mKind.maxLinesForDisplay = Math.max(thisKind.maxLinesForDisplay,
+                thatKind.maxLinesForDisplay);
+
+        // If any of the collapsed entries are super primary make the whole thing super primary.
+        if (isSuperPrimary() || that.isSuperPrimary()) {
+            mContentValues.put(Data.IS_SUPER_PRIMARY, 1);
+            mContentValues.put(Data.IS_PRIMARY, 1);
+        }
+
+        // If any of the collapsed entries are primary make the whole thing primary.
+        if (isPrimary() || that.isPrimary()) {
+            mContentValues.put(Data.IS_PRIMARY, 1);
+        }
+
+        // Add up the times used
+        mContentValues.put(Entity.TIMES_USED, (getTimesUsed() == null ? 0 : getTimesUsed()) +
+                (that.getTimesUsed() == null ? 0 : that.getTimesUsed()));
+
+        // Use the most recent time
+        mContentValues.put(Entity.LAST_TIME_USED,
+                Math.max(getLastTimeUsed() == null ? 0 : getLastTimeUsed(),
+                        that.getLastTimeUsed() == null ? 0 : that.getLastTimeUsed()));
+    }
+
+    @Override
+    public boolean shouldCollapseWith(DataItem t, Context context) {
+        if (mKind == null || t.getDataKind() == null) {
+            return false;
+        }
+        return MoreContactUtils.shouldCollapse(getMimeType(), buildDataString(context, mKind),
+                t.getMimeType(), t.buildDataString(context, t.getDataKind()));
+    }
+}
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..e4b6aea
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/DataKind.java
@@ -0,0 +1,140 @@
+/*
+ * 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.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 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;
+
+    /**
+     * 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() {
+        maxLinesForDisplay = 1;
+    }
+
+    public DataKind(String mimeType, int titleRes, int weight, boolean editable) {
+        this.mimeType = mimeType;
+        this.titleRes = titleRes;
+        this.weight = weight;
+        this.editable = editable;
+        this.typeOverallMax = -1;
+        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(" 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(" 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/model/dataitem/EmailDataItem.java b/src/com/android/contacts/common/model/dataitem/EmailDataItem.java
new file mode 100644
index 0000000..23efb01
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/EmailDataItem.java
@@ -0,0 +1,48 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+
+/**
+ * Represents an email data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Email}.
+ */
+public class EmailDataItem extends DataItem {
+
+    /* package */ EmailDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getAddress() {
+        return getContentValues().getAsString(Email.ADDRESS);
+    }
+
+    public String getDisplayName() {
+        return getContentValues().getAsString(Email.DISPLAY_NAME);
+    }
+
+    public String getData() {
+        return getContentValues().getAsString(Email.DATA);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Email.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/EventDataItem.java b/src/com/android/contacts/common/model/dataitem/EventDataItem.java
new file mode 100644
index 0000000..5096fea
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/EventDataItem.java
@@ -0,0 +1,63 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.text.TextUtils;
+
+/**
+ * Represents an event data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Event}.
+ */
+public class EventDataItem extends DataItem {
+
+    /* package */ EventDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getStartDate() {
+        return getContentValues().getAsString(Event.START_DATE);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Event.LABEL);
+    }
+
+    @Override
+    public boolean shouldCollapseWith(DataItem t, Context context) {
+        if (!(t instanceof EventDataItem) || mKind == null || t.getDataKind() == null) {
+            return false;
+        }
+        final EventDataItem that = (EventDataItem) t;
+        // Events can be different (anniversary, birthday) but have the same start date
+        if (!TextUtils.equals(getStartDate(), that.getStartDate())) {
+            return false;
+        } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) {
+            return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind());
+        } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) {
+            return false;
+        } else if (getKindTypeColumn(mKind) == Event.TYPE_CUSTOM &&
+                !TextUtils.equals(getLabel(), that.getLabel())) {
+            // Check if custom types are not the same
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java b/src/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
new file mode 100644
index 0000000..41f19e6
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+
+/**
+ * Represents a group memebership data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.GroupMembership}.
+ */
+public class GroupMembershipDataItem extends DataItem {
+
+    /* package */ GroupMembershipDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public Long getGroupRowId() {
+        return getContentValues().getAsLong(GroupMembership.GROUP_ROW_ID);
+    }
+
+    public String getGroupSourceId() {
+        return getContentValues().getAsString(GroupMembership.GROUP_SOURCE_ID);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/IdentityDataItem.java b/src/com/android/contacts/common/model/dataitem/IdentityDataItem.java
new file mode 100644
index 0000000..29e9a40
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/IdentityDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+
+/**
+ * Represents an identity data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Identity}.
+ */
+public class IdentityDataItem extends DataItem {
+
+    /* package */ IdentityDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getIdentity() {
+        return getContentValues().getAsString(Identity.IDENTITY);
+    }
+
+    public String getNamespace() {
+        return getContentValues().getAsString(Identity.NAMESPACE);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/ImDataItem.java b/src/com/android/contacts/common/model/dataitem/ImDataItem.java
new file mode 100644
index 0000000..f89e5c6
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/ImDataItem.java
@@ -0,0 +1,113 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.text.TextUtils;
+
+/**
+ * Represents an IM data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Im}.
+ */
+public class ImDataItem extends DataItem {
+
+    private final boolean mCreatedFromEmail;
+
+    /* package */ ImDataItem(ContentValues values) {
+        super(values);
+        mCreatedFromEmail = false;
+    }
+
+    private ImDataItem(ContentValues values, boolean createdFromEmail) {
+        super(values);
+        mCreatedFromEmail = createdFromEmail;
+    }
+
+    public static ImDataItem createFromEmail(EmailDataItem item) {
+        final ImDataItem im = new ImDataItem(new ContentValues(item.getContentValues()), true);
+        im.setMimeType(Im.CONTENT_ITEM_TYPE);
+        return im;
+    }
+
+    public String getData() {
+        if (mCreatedFromEmail) {
+            return getContentValues().getAsString(Email.DATA);
+        } else {
+            return getContentValues().getAsString(Im.DATA);
+        }
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Im.LABEL);
+    }
+
+    /**
+     * Values are one of Im.PROTOCOL_
+     */
+    public Integer getProtocol() {
+        return getContentValues().getAsInteger(Im.PROTOCOL);
+    }
+
+    public boolean isProtocolValid() {
+        return getProtocol() != null;
+    }
+
+    public String getCustomProtocol() {
+        return getContentValues().getAsString(Im.CUSTOM_PROTOCOL);
+    }
+
+    public int getChatCapability() {
+        Integer result = getContentValues().getAsInteger(Im.CHAT_CAPABILITY);
+        return result == null ? 0 : result;
+    }
+
+    public boolean isCreatedFromEmail() {
+        return mCreatedFromEmail;
+    }
+
+    @Override
+    public boolean shouldCollapseWith(DataItem t, Context context) {
+        if (!(t instanceof ImDataItem) || mKind == null || t.getDataKind() == null) {
+            return false;
+        }
+        final ImDataItem that = (ImDataItem) t;
+        // IM can have the same data put different protocol. These should not collapse.
+        if (!getData().equals(that.getData())) {
+            return false;
+        } else if (!isProtocolValid() || !that.isProtocolValid()) {
+            // Deal with invalid protocol as if it was custom. If either has a non valid
+            // protocol, check to see if the other has a valid that is not custom
+            if (isProtocolValid()) {
+                return getProtocol() == Im.PROTOCOL_CUSTOM;
+            } else if (that.isProtocolValid()) {
+                return that.getProtocol() == Im.PROTOCOL_CUSTOM;
+            }
+            return true;
+        } else if (getProtocol() != that.getProtocol()) {
+            return false;
+        } else if (getProtocol() == Im.PROTOCOL_CUSTOM &&
+                !TextUtils.equals(getCustomProtocol(), that.getCustomProtocol())) {
+            // Check if custom protocols are not the same
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/NicknameDataItem.java b/src/com/android/contacts/common/model/dataitem/NicknameDataItem.java
new file mode 100644
index 0000000..e7f9d4a
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/NicknameDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+
+/**
+ * Represents a nickname data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Nickname}.
+ */
+public class NicknameDataItem extends DataItem {
+
+    public NicknameDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getName() {
+        return getContentValues().getAsString(Nickname.NAME);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Nickname.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/NoteDataItem.java b/src/com/android/contacts/common/model/dataitem/NoteDataItem.java
new file mode 100644
index 0000000..3d71167
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/NoteDataItem.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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+
+/**
+ * Represents a note data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Note}.
+ */
+public class NoteDataItem extends DataItem {
+
+    /* package */ NoteDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getNote() {
+        return getContentValues().getAsString(Note.NOTE);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/OrganizationDataItem.java b/src/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
new file mode 100644
index 0000000..37cd852
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
@@ -0,0 +1,64 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+
+/**
+ * Represents an organization data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Organization}.
+ */
+public class OrganizationDataItem extends DataItem {
+
+    /* package */ OrganizationDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getCompany() {
+        return getContentValues().getAsString(Organization.COMPANY);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Organization.LABEL);
+    }
+
+    public String getTitle() {
+        return getContentValues().getAsString(Organization.TITLE);
+    }
+
+    public String getDepartment() {
+        return getContentValues().getAsString(Organization.DEPARTMENT);
+    }
+
+    public String getJobDescription() {
+        return getContentValues().getAsString(Organization.JOB_DESCRIPTION);
+    }
+
+    public String getSymbol() {
+        return getContentValues().getAsString(Organization.SYMBOL);
+    }
+
+    public String getPhoneticName() {
+        return getContentValues().getAsString(Organization.PHONETIC_NAME);
+    }
+
+    public String getOfficeLocation() {
+        return getContentValues().getAsString(Organization.OFFICE_LOCATION);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/PhoneDataItem.java b/src/com/android/contacts/common/model/dataitem/PhoneDataItem.java
new file mode 100644
index 0000000..d6aa2a9
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/PhoneDataItem.java
@@ -0,0 +1,80 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.model.dataitem.DataKind;
+
+/**
+ * Represents a phone data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Phone}.
+ */
+public class PhoneDataItem extends DataItem {
+
+    public static final String KEY_FORMATTED_PHONE_NUMBER = "formattedPhoneNumber";
+
+    /* package */ PhoneDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getNumber() {
+        return getContentValues().getAsString(Phone.NUMBER);
+    }
+
+    /**
+     * Returns the normalized phone number in E164 format.
+     */
+    public String getNormalizedNumber() {
+        return getContentValues().getAsString(Phone.NORMALIZED_NUMBER);
+    }
+
+    public String getFormattedPhoneNumber() {
+        return getContentValues().getAsString(KEY_FORMATTED_PHONE_NUMBER);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Phone.LABEL);
+    }
+
+    public void computeFormattedPhoneNumber(String defaultCountryIso) {
+        final String phoneNumber = getNumber();
+        if (phoneNumber != null) {
+            final String formattedPhoneNumber = PhoneNumberUtilsCompat.formatNumber(phoneNumber,
+                    getNormalizedNumber(), defaultCountryIso);
+            getContentValues().put(KEY_FORMATTED_PHONE_NUMBER, formattedPhoneNumber);
+        }
+    }
+
+    /**
+     * Returns the formatted phone number (if already computed using {@link
+     * #computeFormattedPhoneNumber}). Otherwise this method returns the unformatted phone number.
+     */
+    @Override
+    public String buildDataStringForDisplay(Context context, DataKind kind) {
+        final String formatted = getFormattedPhoneNumber();
+        if (formatted != null) {
+            return formatted;
+        } else {
+            return getNumber();
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/PhotoDataItem.java b/src/com/android/contacts/common/model/dataitem/PhotoDataItem.java
new file mode 100644
index 0000000..a61218b
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/PhotoDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts.Photo;
+
+/**
+ * Represents a photo data item, wrapping the columns in
+ * {@link ContactsContract.Contacts.Photo}.
+ */
+public class PhotoDataItem extends DataItem {
+
+    /* package */ PhotoDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public Long getPhotoFileId() {
+        return getContentValues().getAsLong(Photo.PHOTO_FILE_ID);
+    }
+
+    public byte[] getPhoto() {
+        return getContentValues().getAsByteArray(Photo.PHOTO);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/RelationDataItem.java b/src/com/android/contacts/common/model/dataitem/RelationDataItem.java
new file mode 100644
index 0000000..9e883fe
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/RelationDataItem.java
@@ -0,0 +1,63 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.text.TextUtils;
+
+/**
+ * Represents a relation data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Relation}.
+ */
+public class RelationDataItem extends DataItem {
+
+    /* package */ RelationDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getName() {
+        return getContentValues().getAsString(Relation.NAME);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Relation.LABEL);
+    }
+
+    @Override
+    public boolean shouldCollapseWith(DataItem t, Context context) {
+        if (!(t instanceof RelationDataItem) || mKind == null || t.getDataKind() == null) {
+            return false;
+        }
+        final RelationDataItem that = (RelationDataItem) t;
+        // Relations can have different types (assistant, father) but have the same name
+        if (!TextUtils.equals(getName(), that.getName())) {
+            return false;
+        } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) {
+            return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind());
+        } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) {
+            return false;
+        } else if (getKindTypeColumn(mKind) == Relation.TYPE_CUSTOM &&
+                !TextUtils.equals(getLabel(), that.getLabel())) {
+            // Check if custom types are not the same
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/SipAddressDataItem.java b/src/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
new file mode 100644
index 0000000..ec704fc
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+
+/**
+ * Represents a sip address data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.SipAddress}.
+ */
+public class SipAddressDataItem extends DataItem {
+
+    /* package */ SipAddressDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getSipAddress() {
+        return getContentValues().getAsString(SipAddress.SIP_ADDRESS);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(SipAddress.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java b/src/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
new file mode 100644
index 0000000..4d463da
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
@@ -0,0 +1,100 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts.Data;
+
+/**
+ * Represents a structured name data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.StructuredName}.
+ */
+public class StructuredNameDataItem extends DataItem {
+
+    public StructuredNameDataItem() {
+        super(new ContentValues());
+        getContentValues().put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+    }
+
+    /* package */ StructuredNameDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getDisplayName() {
+        return getContentValues().getAsString(StructuredName.DISPLAY_NAME);
+    }
+
+    public void setDisplayName(String name) {
+        getContentValues().put(StructuredName.DISPLAY_NAME, name);
+    }
+
+    public String getGivenName() {
+        return getContentValues().getAsString(StructuredName.GIVEN_NAME);
+    }
+
+    public String getFamilyName() {
+        return getContentValues().getAsString(StructuredName.FAMILY_NAME);
+    }
+
+    public String getPrefix() {
+        return getContentValues().getAsString(StructuredName.PREFIX);
+    }
+
+    public String getMiddleName() {
+        return getContentValues().getAsString(StructuredName.MIDDLE_NAME);
+    }
+
+    public String getSuffix() {
+        return getContentValues().getAsString(StructuredName.SUFFIX);
+    }
+
+    public String getPhoneticGivenName() {
+        return getContentValues().getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+    }
+
+    public String getPhoneticMiddleName() {
+        return getContentValues().getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
+    }
+
+    public String getPhoneticFamilyName() {
+        return getContentValues().getAsString(StructuredName.PHONETIC_FAMILY_NAME);
+    }
+
+    public String getFullNameStyle() {
+        return getContentValues().getAsString(StructuredName.FULL_NAME_STYLE);
+    }
+
+    public void setPhoneticFamilyName(String name) {
+        getContentValues().put(StructuredName.PHONETIC_FAMILY_NAME, name);
+    }
+
+    public void setPhoneticMiddleName(String name) {
+        getContentValues().put(StructuredName.PHONETIC_MIDDLE_NAME, name);
+    }
+
+    public void setPhoneticGivenName(String name) {
+        getContentValues().put(StructuredName.PHONETIC_GIVEN_NAME, name);
+    }
+
+    public boolean isSuperPrimary() {
+        final ContentValues contentValues = getContentValues();
+        return contentValues == null || !contentValues.containsKey(StructuredName.IS_SUPER_PRIMARY)
+                ? false : contentValues.getAsBoolean(StructuredName.IS_SUPER_PRIMARY);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java b/src/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
new file mode 100644
index 0000000..6cfc0c1
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
@@ -0,0 +1,68 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+
+/**
+ * Represents a structured postal data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.StructuredPostal}.
+ */
+public class StructuredPostalDataItem extends DataItem {
+
+    /* package */ StructuredPostalDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getFormattedAddress() {
+        return getContentValues().getAsString(StructuredPostal.FORMATTED_ADDRESS);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(StructuredPostal.LABEL);
+    }
+
+    public String getStreet() {
+        return getContentValues().getAsString(StructuredPostal.STREET);
+    }
+
+    public String getPOBox() {
+        return getContentValues().getAsString(StructuredPostal.POBOX);
+    }
+
+    public String getNeighborhood() {
+        return getContentValues().getAsString(StructuredPostal.NEIGHBORHOOD);
+    }
+
+    public String getCity() {
+        return getContentValues().getAsString(StructuredPostal.CITY);
+    }
+
+    public String getRegion() {
+        return getContentValues().getAsString(StructuredPostal.REGION);
+    }
+
+    public String getPostcode() {
+        return getContentValues().getAsString(StructuredPostal.POSTCODE);
+    }
+
+    public String getCountry() {
+        return getContentValues().getAsString(StructuredPostal.COUNTRY);
+    }
+}
diff --git a/src/com/android/contacts/common/model/dataitem/WebsiteDataItem.java b/src/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
new file mode 100644
index 0000000..0939421
--- /dev/null
+++ b/src/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * 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.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+
+/**
+ * Represents a website data item, wrapping the columns in
+ * {@link ContactsContract.CommonDataKinds.Website}.
+ */
+public class WebsiteDataItem extends DataItem {
+
+    /* package */ WebsiteDataItem(ContentValues values) {
+        super(values);
+    }
+
+    public String getUrl() {
+        return getContentValues().getAsString(Website.URL);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(Website.LABEL);
+    }
+}
diff --git a/src/com/android/contacts/common/preference/AboutPreferenceFragment.java b/src/com/android/contacts/common/preference/AboutPreferenceFragment.java
new file mode 100644
index 0000000..3b5a28d
--- /dev/null
+++ b/src/com/android/contacts/common/preference/AboutPreferenceFragment.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 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.preference;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.widget.Toast;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.activity.LicenseActivity;
+
+/**
+ * This fragment shows the preferences for "about".
+ */
+public class AboutPreferenceFragment extends PreferenceFragment {
+
+    private static final String PRIVACY_POLICY_URL = "http://www.google.com/policies/privacy";
+    private static final String TERMS_OF_SERVICE_URL = "http://www.google.com/policies/terms";
+
+    public static AboutPreferenceFragment newInstance() {
+        return new AboutPreferenceFragment();
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Load the preferences from an XML resource
+        addPreferencesFromResource(R.xml.preference_about);
+
+        // Set build version of Contacts App.
+        final PackageManager manager = getActivity().getPackageManager();
+        try {
+            final PackageInfo info = manager.getPackageInfo(getActivity().getPackageName(), 0);
+            final Preference versionPreference = findPreference(
+                    getString(R.string.pref_build_version_key));
+            versionPreference.setSummary(info.versionName);
+        } catch (PackageManager.NameNotFoundException e) {
+            // Nothing
+        }
+
+        final Preference licensePreference = findPreference(
+                getString(R.string.pref_open_source_licenses_key));
+        licensePreference.setIntent(new Intent(getActivity(), LicenseActivity.class));
+
+        final Preference privacyPolicyPreference = findPreference("pref_privacy_policy");
+        final Preference termsOfServicePreference = findPreference("pref_terms_of_service");
+
+        final Preference.OnPreferenceClickListener listener =
+                new Preference.OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference preference) {
+                try {
+                    if (preference == privacyPolicyPreference) {
+                        startActivityForUrl(PRIVACY_POLICY_URL);
+                    } else if (preference == termsOfServicePreference) {
+                        startActivityForUrl(TERMS_OF_SERVICE_URL);
+                    }
+                } catch (ActivityNotFoundException ex) {
+                    Toast.makeText(getContext(), getString(R.string.url_open_error_toast),
+                            Toast.LENGTH_SHORT).show();
+                }
+                return true;
+            }
+        };
+
+        privacyPolicyPreference.setOnPreferenceClickListener(listener);
+        termsOfServicePreference.setOnPreferenceClickListener(listener);
+    }
+
+    @Override
+    public Context getContext() {
+        return getActivity();
+    }
+
+    private void startActivityForUrl(String urlString) {
+        final Intent intent = new Intent();
+        intent.setAction(Intent.ACTION_VIEW);
+        intent.setData(Uri.parse(urlString));
+        startActivity(intent);
+    }
+}
+
diff --git a/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java b/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java
new file mode 100644
index 0000000..dbf3cb9
--- /dev/null
+++ b/src/com/android/contacts/common/preference/ContactsPreferenceActivity.java
@@ -0,0 +1,142 @@
+/*
+ * 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.preference;
+
+import android.app.ActionBar;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.provider.ContactsContract.ProviderStatus;
+import android.provider.ContactsContract.QuickContact;
+import android.text.TextUtils;
+import android.view.MenuItem;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.list.ProviderStatusWatcher;
+import com.android.contacts.common.preference.DisplayOptionsPreferenceFragment.ProfileListener;
+import com.android.contacts.common.preference.DisplayOptionsPreferenceFragment.ProfileQuery;
+
+/**
+ * Contacts settings.
+ */
+public final class ContactsPreferenceActivity extends PreferenceActivity implements
+        ProfileListener {
+
+    private static final String TAG_ABOUT = "about_contacts";
+    private static final String TAG_DISPLAY_OPTIONS = "display_options";
+
+    private String mNewLocalProfileExtra;
+    private String mPreviousScreenExtra;
+    private int mModeFullyExpanded;
+    private boolean mAreContactsAvailable;
+
+    private ProviderStatusWatcher mProviderStatusWatcher;
+
+    public static final String EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
+    public static final String EXTRA_MODE_FULLY_EXPANDED = "modeFullyExpanded";
+    public static final String EXTRA_PREVIOUS_SCREEN_TYPE = "previousScreenType";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP);
+        }
+
+        mProviderStatusWatcher = ProviderStatusWatcher.getInstance(this);
+
+        mNewLocalProfileExtra = getIntent().getStringExtra(EXTRA_NEW_LOCAL_PROFILE);
+        mModeFullyExpanded = getIntent().getIntExtra(EXTRA_MODE_FULLY_EXPANDED,
+                QuickContact.MODE_LARGE);
+        mPreviousScreenExtra = getIntent().getStringExtra(EXTRA_PREVIOUS_SCREEN_TYPE);
+        final int providerStatus = mProviderStatusWatcher.getProviderStatus();
+        mAreContactsAvailable = providerStatus == ProviderStatus.STATUS_NORMAL;
+
+        if (savedInstanceState == null) {
+            final DisplayOptionsPreferenceFragment fragment = DisplayOptionsPreferenceFragment
+                    .newInstance(mNewLocalProfileExtra, mPreviousScreenExtra, mModeFullyExpanded,
+                            mAreContactsAvailable);
+            getFragmentManager().beginTransaction()
+                    .replace(android.R.id.content, fragment, TAG_DISPLAY_OPTIONS)
+                    .commit();
+            setActivityTitle(R.string.activity_title_settings);
+        } else {
+            final AboutPreferenceFragment aboutFragment = (AboutPreferenceFragment)
+                    getFragmentManager().findFragmentByTag(TAG_ABOUT);
+
+            if (aboutFragment != null) {
+                setActivityTitle(R.string.setting_about);
+            } else {
+                setActivityTitle(R.string.activity_title_settings);
+            }
+        }
+    }
+
+    protected void showAboutFragment() {
+        getFragmentManager().beginTransaction()
+                .replace(android.R.id.content, AboutPreferenceFragment.newInstance(), TAG_ABOUT)
+                .addToBackStack(null)
+                .commit();
+        setActivityTitle(R.string.setting_about);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            onBackPressed();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (getFragmentManager().getBackStackEntryCount() > 0) {
+            setActivityTitle(R.string.activity_title_settings);
+            getFragmentManager().popBackStack();
+        } else {
+            super.onBackPressed();
+        }
+    }
+
+    private void setActivityTitle(int res) {
+        final ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            actionBar.setTitle(res);
+        }
+    }
+
+    @Override
+    public void onProfileLoaded(Cursor cursor) {
+        boolean hasProfile = false;
+        String displayName = null;
+        long contactId = -1;
+        if (cursor != null && cursor.moveToFirst()) {
+            hasProfile = cursor.getInt(ProfileQuery.CONTACT_IS_USER_PROFILE) == 1;
+            displayName = cursor.getString(ProfileQuery.CONTACT_DISPLAY_NAME);
+            contactId = cursor.getLong(ProfileQuery.CONTACT_ID);
+        }
+        if (hasProfile && TextUtils.isEmpty(displayName)) {
+            displayName = getString(R.string.missing_name);
+        }
+        final DisplayOptionsPreferenceFragment fragment = (DisplayOptionsPreferenceFragment)
+                getFragmentManager().findFragmentByTag(TAG_DISPLAY_OPTIONS);
+        fragment.updateMyInfoPreference(hasProfile, displayName, contactId);
+    }
+}
diff --git a/src/com/android/contacts/common/preference/ContactsPreferences.java b/src/com/android/contacts/common/preference/ContactsPreferences.java
new file mode 100644
index 0000000..a8a9089
--- /dev/null
+++ b/src/com/android/contacts/common/preference/ContactsPreferences.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.preference;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.text.TextUtils;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.AccountTypeManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages user preferences for contacts.
+ */
+public class ContactsPreferences implements OnSharedPreferenceChangeListener {
+
+    /**
+     * The value for the DISPLAY_ORDER key to show the given name first.
+     */
+    public static final int DISPLAY_ORDER_PRIMARY = 1;
+
+    /**
+     * The value for the DISPLAY_ORDER key to show the family name first.
+     */
+    public static final int DISPLAY_ORDER_ALTERNATIVE = 2;
+
+    public static final String DISPLAY_ORDER_KEY = "android.contacts.DISPLAY_ORDER";
+
+    /**
+     * The value for the SORT_ORDER key corresponding to sort by given name first.
+     */
+    public static final int SORT_ORDER_PRIMARY = 1;
+
+    public static final String SORT_ORDER_KEY = "android.contacts.SORT_ORDER";
+
+    /**
+     * The value for the SORT_ORDER key corresponding to sort by family name first.
+     */
+    public static final int SORT_ORDER_ALTERNATIVE = 2;
+
+    public static final String PREF_DISPLAY_ONLY_PHONES = "only_phones";
+
+    public static final boolean PREF_DISPLAY_ONLY_PHONES_DEFAULT = false;
+
+    public static final String DO_NOT_SYNC_CONTACT_METADATA_MSG = "Do not sync metadata";
+
+    public static final String CONTACT_METADATA_AUTHORITY = "com.android.contacts.metadata";
+
+    public static final String SHOULD_CLEAR_METADATA_BEFORE_SYNCING =
+            "should_clear_metadata_before_syncing";
+
+    public static final String ONLY_CLEAR_DONOT_SYNC = "only_clear_donot_sync";
+    /**
+     * Value to use when a preference is unassigned and needs to be read from the shared preferences
+     */
+    private static final int PREFERENCE_UNASSIGNED = -1;
+
+    private final Context mContext;
+    private int mSortOrder = PREFERENCE_UNASSIGNED;
+    private int mDisplayOrder = PREFERENCE_UNASSIGNED;
+    private String mDefaultAccount = null;
+    private ChangeListener mListener = null;
+    private Handler mHandler;
+    private final SharedPreferences mPreferences;
+    private String mDefaultAccountKey;
+    private String mDefaultAccountSavedKey;
+
+    public ContactsPreferences(Context context) {
+        mContext = context;
+        mHandler = new Handler();
+        mPreferences = mContext.getSharedPreferences(context.getPackageName(),
+                Context.MODE_PRIVATE);
+        mDefaultAccountKey = mContext.getResources().getString(
+                R.string.contact_editor_default_account_key);
+        mDefaultAccountSavedKey = mContext.getResources().getString(
+                R.string.contact_editor_anything_saved_key);
+        maybeMigrateSystemSettings();
+    }
+
+    public boolean isSortOrderUserChangeable() {
+        return mContext.getResources().getBoolean(R.bool.config_sort_order_user_changeable);
+    }
+
+    public int getDefaultSortOrder() {
+        if (mContext.getResources().getBoolean(R.bool.config_default_sort_order_primary)) {
+            return SORT_ORDER_PRIMARY;
+        } else {
+            return SORT_ORDER_ALTERNATIVE;
+        }
+    }
+
+    public int getSortOrder() {
+        if (!isSortOrderUserChangeable()) {
+            return getDefaultSortOrder();
+        }
+        if (mSortOrder == PREFERENCE_UNASSIGNED) {
+            mSortOrder = mPreferences.getInt(SORT_ORDER_KEY, getDefaultSortOrder());
+        }
+        return mSortOrder;
+    }
+
+    public void setSortOrder(int sortOrder) {
+        mSortOrder = sortOrder;
+        final Editor editor = mPreferences.edit();
+        editor.putInt(SORT_ORDER_KEY, sortOrder);
+        editor.commit();
+    }
+
+    public boolean isDisplayOrderUserChangeable() {
+        return mContext.getResources().getBoolean(R.bool.config_display_order_user_changeable);
+    }
+
+    public int getDefaultDisplayOrder() {
+        if (mContext.getResources().getBoolean(R.bool.config_default_display_order_primary)) {
+            return DISPLAY_ORDER_PRIMARY;
+        } else {
+            return DISPLAY_ORDER_ALTERNATIVE;
+        }
+    }
+
+    public int getDisplayOrder() {
+        if (!isDisplayOrderUserChangeable()) {
+            return getDefaultDisplayOrder();
+        }
+        if (mDisplayOrder == PREFERENCE_UNASSIGNED) {
+            mDisplayOrder = mPreferences.getInt(DISPLAY_ORDER_KEY, getDefaultDisplayOrder());
+        }
+        return mDisplayOrder;
+    }
+
+    public void setDisplayOrder(int displayOrder) {
+        mDisplayOrder = displayOrder;
+        final Editor editor = mPreferences.edit();
+        editor.putInt(DISPLAY_ORDER_KEY, displayOrder);
+        editor.commit();
+    }
+
+    public boolean isDefaultAccountUserChangeable() {
+        return mContext.getResources().getBoolean(R.bool.config_default_account_user_changeable);
+    }
+
+    public String getDefaultAccount() {
+        if (!isDefaultAccountUserChangeable()) {
+            return mDefaultAccount;
+        }
+        if (TextUtils.isEmpty(mDefaultAccount)) {
+            final String accountString = mPreferences
+                    .getString(mDefaultAccountKey, mDefaultAccount);
+            if (!TextUtils.isEmpty(accountString)) {
+                final AccountWithDataSet accountWithDataSet = AccountWithDataSet.unstringify(
+                        accountString);
+                mDefaultAccount = accountWithDataSet.name;
+            }
+        }
+        return mDefaultAccount;
+    }
+
+    public void setDefaultAccount(AccountWithDataSet accountWithDataSet) {
+        mDefaultAccount = accountWithDataSet == null ? null : accountWithDataSet.name;
+        final Editor editor = mPreferences.edit();
+        if (TextUtils.isEmpty(mDefaultAccount)) {
+            editor.remove(mDefaultAccountKey);
+        } else {
+            editor.putString(mDefaultAccountKey, accountWithDataSet.stringify());
+        }
+        editor.putBoolean(mDefaultAccountSavedKey, true);
+        editor.commit();
+    }
+
+    public String getContactMetadataSyncAccountName() {
+        final Account syncAccount = getContactMetadataSyncAccount();
+        return syncAccount == null ? DO_NOT_SYNC_CONTACT_METADATA_MSG : syncAccount.name;
+    }
+
+    public void setContactMetadataSyncAccount(AccountWithDataSet accountWithDataSet) {
+        final String mContactMetadataSyncAccount =
+                accountWithDataSet == null ? null : accountWithDataSet.name;
+        requestMetadataSyncForAccount(mContactMetadataSyncAccount);
+    }
+
+    private Account getContactMetadataSyncAccount() {
+        for (Account account : getFocusGoogleAccounts()) {
+            if (ContentResolver.getIsSyncable(account, CONTACT_METADATA_AUTHORITY) == 1
+                    && ContentResolver.getSyncAutomatically(account, CONTACT_METADATA_AUTHORITY)) {
+                return account;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Turn on contact metadata sync for this {@param accountName} and turn off automatic sync
+     * for other accounts. If accountName is null, then turn off automatic sync for all accounts.
+     */
+    private void requestMetadataSyncForAccount(String accountName) {
+        for (Account account : getFocusGoogleAccounts()) {
+            if (!TextUtils.isEmpty(accountName) && accountName.equals(account.name)) {
+                // Request sync.
+                final Bundle b = new Bundle();
+                b.putBoolean(SHOULD_CLEAR_METADATA_BEFORE_SYNCING, true);
+                b.putBoolean(ONLY_CLEAR_DONOT_SYNC, false);
+                b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+                b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+                ContentResolver.requestSync(account, CONTACT_METADATA_AUTHORITY, b);
+
+                ContentResolver.setSyncAutomatically(account, CONTACT_METADATA_AUTHORITY, true);
+            } else if (ContentResolver.getSyncAutomatically(account, CONTACT_METADATA_AUTHORITY)) {
+                // Turn off automatic sync for previous sync account.
+                ContentResolver.setSyncAutomatically(account, CONTACT_METADATA_AUTHORITY, false);
+                if (TextUtils.isEmpty(accountName)) {
+                    // Request sync to clear old data.
+                    final Bundle b = new Bundle();
+                    b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+                    b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+                    b.putBoolean(SHOULD_CLEAR_METADATA_BEFORE_SYNCING, true);
+                    b.putBoolean(ONLY_CLEAR_DONOT_SYNC, true);
+                    ContentResolver.requestSync(account, CONTACT_METADATA_AUTHORITY, b);
+                }
+            }
+        }
+    }
+
+    /**
+     * @return google accounts with "com.google" account type and null data set.
+     */
+    private List<Account> getFocusGoogleAccounts() {
+        List<Account> focusGoogleAccounts = new ArrayList<Account>();
+        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
+        List<AccountWithDataSet> accounts = accountTypeManager.getAccounts(true);
+        for (AccountWithDataSet account : accounts) {
+            if (GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null) {
+                focusGoogleAccounts.add(account.getAccountOrNull());
+            }
+        }
+        return focusGoogleAccounts;
+    }
+
+    public void registerChangeListener(ChangeListener listener) {
+        if (mListener != null) unregisterChangeListener();
+
+        mListener = listener;
+
+        // Reset preferences to "unknown" because they may have changed while the
+        // listener was unregistered.
+        mDisplayOrder = PREFERENCE_UNASSIGNED;
+        mSortOrder = PREFERENCE_UNASSIGNED;
+        mDefaultAccount = null;
+
+        mPreferences.registerOnSharedPreferenceChangeListener(this);
+    }
+
+    public void unregisterChangeListener() {
+        if (mListener != null) {
+            mListener = null;
+        }
+
+        mPreferences.unregisterOnSharedPreferenceChangeListener(this);
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
+        // This notification is not sent on the Ui thread. Use the previously created Handler
+        // to switch to the Ui thread
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                refreshValue(key);
+            }
+        });
+    }
+
+    /**
+     * Forces the value for the given key to be looked up from shared preferences and notifies
+     * the registered {@link ChangeListener}
+     *
+     * @param key the {@link SharedPreferences} key to look up
+     */
+    public void refreshValue(String key) {
+        if (DISPLAY_ORDER_KEY.equals(key)) {
+            mDisplayOrder = PREFERENCE_UNASSIGNED;
+            mDisplayOrder = getDisplayOrder();
+        } else if (SORT_ORDER_KEY.equals(key)) {
+            mSortOrder = PREFERENCE_UNASSIGNED;
+            mSortOrder = getSortOrder();
+        } else if (mDefaultAccountKey.equals(key)) {
+            mDefaultAccount = null;
+            mDefaultAccount = getDefaultAccount();
+        }
+        if (mListener != null) mListener.onChange();
+    }
+
+    public interface ChangeListener {
+        void onChange();
+    }
+
+    /**
+     * If there are currently no preferences (which means this is the first time we are run),
+     * For sort order and display order, check to see if there are any preferences stored in
+     * system settings (pre-L) which can be copied into our own SharedPreferences.
+     * For default account setting, check to see if there are any preferences stored in the previous
+     * SharedPreferences which can be copied into current SharedPreferences.
+     */
+    private void maybeMigrateSystemSettings() {
+        if (!mPreferences.contains(SORT_ORDER_KEY)) {
+            int sortOrder = getDefaultSortOrder();
+            try {
+                 sortOrder = Settings.System.getInt(mContext.getContentResolver(),
+                        SORT_ORDER_KEY);
+            } catch (SettingNotFoundException e) {
+            }
+            setSortOrder(sortOrder);
+        }
+
+        if (!mPreferences.contains(DISPLAY_ORDER_KEY)) {
+            int displayOrder = getDefaultDisplayOrder();
+            try {
+                displayOrder = Settings.System.getInt(mContext.getContentResolver(),
+                        DISPLAY_ORDER_KEY);
+            } catch (SettingNotFoundException e) {
+            }
+            setDisplayOrder(displayOrder);
+        }
+
+        if (!mPreferences.contains(mDefaultAccountKey)) {
+            final SharedPreferences previousPrefs =
+                    PreferenceManager.getDefaultSharedPreferences(mContext);
+            final String defaultAccount = previousPrefs.getString(mDefaultAccountKey, null);
+            if (!TextUtils.isEmpty(defaultAccount)) {
+                final AccountWithDataSet accountWithDataSet = AccountWithDataSet.unstringify(
+                        defaultAccount);
+                setDefaultAccount(accountWithDataSet);
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/preference/DefaultAccountPreference.java b/src/com/android/contacts/common/preference/DefaultAccountPreference.java
new file mode 100644
index 0000000..2fc5c7b
--- /dev/null
+++ b/src/com/android/contacts/common/preference/DefaultAccountPreference.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2015 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.preference;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class DefaultAccountPreference extends ListPreference {
+    private ContactsPreferences mPreferences;
+    private Map<String, AccountWithDataSet> mAccountMap;
+
+    public DefaultAccountPreference(Context context) {
+        super(context);
+        prepare();
+    }
+
+    public DefaultAccountPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        prepare();
+    }
+
+    @Override
+    protected View onCreateDialogView() {
+        prepare();
+        return super.onCreateDialogView();
+    }
+
+    private void prepare() {
+        mPreferences = new ContactsPreferences(getContext());
+        mAccountMap = new HashMap<>();
+        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(getContext());
+        List<AccountWithDataSet> accounts = accountTypeManager.getAccounts(true);
+        for (AccountWithDataSet account : accounts) {
+            mAccountMap.put(account.name, account);
+        }
+        final Set<String> accountNames = mAccountMap.keySet();
+        final String[] accountNamesArray = accountNames.toArray(new String[accountNames.size()]);
+        setEntries(accountNamesArray);
+        setEntryValues(accountNamesArray);
+        final String defaultAccount = String.valueOf(mPreferences.getDefaultAccount());
+        if (accounts.size() == 1) {
+            setValue(accounts.get(0).name);
+        } else if (accountNames.contains(defaultAccount)) {
+            setValue(defaultAccount);
+        } else {
+            setValue(null);
+        }
+    }
+
+    @Override
+    protected boolean shouldPersist() {
+        return false;   // This preference takes care of its own storage
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        return mPreferences.getDefaultAccount();
+    }
+
+    @Override
+    protected boolean persistString(String value) {
+        if (value == null && mPreferences.getDefaultAccount() == null) {
+            return true;
+        }
+        if (value == null || mPreferences.getDefaultAccount() == null
+                || !value.equals(mPreferences.getDefaultAccount())) {
+            mPreferences.setDefaultAccount(mAccountMap.get(value));
+            notifyChanged();
+        }
+        return true;
+    }
+
+    @Override
+    // UX recommendation is not to show cancel button on such lists.
+    protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+        super.onPrepareDialogBuilder(builder);
+        builder.setNegativeButton(null, null);
+    }
+}
diff --git a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
new file mode 100644
index 0000000..57229a4
--- /dev/null
+++ b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.preference;
+
+import android.app.Activity;
+import android.app.LoaderManager;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.provider.BlockedNumberContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Profile;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.TelecomManagerUtil;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.contacts.common.interactions.ImportExportDialogFragment;
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.common.list.ContactListFilterController;
+import com.android.contacts.common.logging.ScreenEvent.ScreenType;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.AccountFilterUtil;
+import com.android.contacts.common.util.ImplicitIntentsUtil;
+import com.android.contacts.commonbind.ObjectFactory;
+
+import java.util.List;
+
+/**
+ * This fragment shows the preferences for "display options"
+ */
+public class DisplayOptionsPreferenceFragment extends PreferenceFragment
+        implements Preference.OnPreferenceClickListener {
+
+    private static final int REQUEST_CODE_CUSTOM_CONTACTS_FILTER = 0;
+
+    private static final String ARG_CONTACTS_AVAILABLE = "are_contacts_available";
+    private static final String ARG_MODE_FULLY_EXPANDED = "mode_fully_expanded";
+    private static final String ARG_NEW_LOCAL_PROFILE = "new_local_profile";
+    private static final String ARG_PREVIOUS_SCREEN = "previous_screen";
+
+    private static final String KEY_ABOUT = "about";
+    private static final String KEY_ACCOUNTS = "accounts";
+    private static final String KEY_DEFAULT_ACCOUNT = "defaultAccount";
+    private static final String KEY_BLOCKED_NUMBERS = "blockedNumbers";
+    private static final String KEY_DISPLAY_ORDER = "displayOrder";
+    private static final String KEY_CUSTOM_CONTACTS_FILTER = "customContactsFilter";
+    private static final String KEY_IMPORT_EXPORT = "importExport";
+    private static final String KEY_MY_INFO = "myInfo";
+    private static final String KEY_SORT_ORDER = "sortOrder";
+
+    private static final int LOADER_PROFILE = 0;
+
+    /**
+     * Callbacks for hosts of the {@link DisplayOptionsPreferenceFragment}.
+     */
+    public interface ProfileListener  {
+        /**
+         * Invoked after profile has been loaded.
+         */
+        void onProfileLoaded(Cursor data);
+    }
+
+    /**
+     * The projections that are used to obtain user profile
+     */
+    public static class ProfileQuery {
+        /**
+         * Not instantiable.
+         */
+        private ProfileQuery() {}
+
+        private static final String[] PROFILE_PROJECTION_PRIMARY = new String[] {
+                Contacts._ID,                           // 0
+                Contacts.DISPLAY_NAME_PRIMARY,          // 1
+                Contacts.IS_USER_PROFILE,               // 2
+        };
+
+        private static final String[] PROFILE_PROJECTION_ALTERNATIVE = new String[] {
+                Contacts._ID,                           // 0
+                Contacts.DISPLAY_NAME_ALTERNATIVE,      // 1
+                Contacts.IS_USER_PROFILE,               // 2
+        };
+
+        public static final int CONTACT_ID               = 0;
+        public static final int CONTACT_DISPLAY_NAME     = 1;
+        public static final int CONTACT_IS_USER_PROFILE  = 2;
+    }
+
+    private String mNewLocalProfileExtra;
+    private String mPreviousScreenExtra;
+    private int mModeFullyExpanded;
+    private boolean mAreContactsAvailable;
+
+    private boolean mHasProfile;
+    private long mProfileContactId;
+
+    private Preference mMyInfoPreference;
+
+    private ProfileListener mListener;
+
+    private final LoaderManager.LoaderCallbacks<Cursor> mProfileLoaderListener =
+            new LoaderManager.LoaderCallbacks<Cursor>() {
+
+        @Override
+        public CursorLoader onCreateLoader(int id, Bundle args) {
+            final CursorLoader loader = createCursorLoader(getContext());
+            loader.setUri(Profile.CONTENT_URI);
+            loader.setProjection(getProjection(getContext()));
+            return loader;
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+            if (mListener != null) {
+                mListener.onProfileLoaded(data);
+            }
+        }
+
+        public void onLoaderReset(Loader<Cursor> loader) {
+        }
+    };
+
+    public static DisplayOptionsPreferenceFragment newInstance(String newLocalProfileExtra,
+            String previousScreenExtra, int modeFullyExpanded, boolean areContactsAvailable) {
+        final DisplayOptionsPreferenceFragment fragment = new DisplayOptionsPreferenceFragment();
+        final Bundle args = new Bundle();
+        args.putString(ARG_NEW_LOCAL_PROFILE, newLocalProfileExtra);
+        args.putString(ARG_PREVIOUS_SCREEN, previousScreenExtra);
+        args.putInt(ARG_MODE_FULLY_EXPANDED, modeFullyExpanded);
+        args.putBoolean(ARG_CONTACTS_AVAILABLE, areContactsAvailable);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        try {
+            mListener = (ProfileListener) activity;
+        } catch (ClassCastException e) {
+            throw new ClassCastException(activity.toString() + " must implement ProfileListener");
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Load the preferences from an XML resource
+        addPreferencesFromResource(R.xml.preference_display_options);
+
+        removeUnsupportedPreferences();
+        addExtraPreferences();
+
+        final Bundle args = getArguments();
+        mNewLocalProfileExtra = args.getString(ARG_NEW_LOCAL_PROFILE);
+        mPreviousScreenExtra = args.getString(ARG_PREVIOUS_SCREEN);
+        mModeFullyExpanded = args.getInt(ARG_MODE_FULLY_EXPANDED);
+        mAreContactsAvailable = args.getBoolean(ARG_CONTACTS_AVAILABLE);
+
+        mMyInfoPreference = findPreference(KEY_MY_INFO);
+
+        final Preference accountsPreference = findPreference(KEY_ACCOUNTS);
+        accountsPreference.setOnPreferenceClickListener(this);
+
+        final Preference importExportPreference = findPreference(KEY_IMPORT_EXPORT);
+        importExportPreference.setOnPreferenceClickListener(this);
+
+        final Preference blockedNumbersPreference = findPreference(KEY_BLOCKED_NUMBERS);
+        if (blockedNumbersPreference != null) {
+            blockedNumbersPreference.setOnPreferenceClickListener(this);
+        }
+
+        final Preference aboutPreference = findPreference(KEY_ABOUT);
+        aboutPreference.setOnPreferenceClickListener(this);
+
+        final Preference customFilterPreference = findPreference(KEY_CUSTOM_CONTACTS_FILTER);
+        if (customFilterPreference != null) {
+            customFilterPreference.setOnPreferenceClickListener(this);
+            setCustomContactsFilterSummary();
+        }
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        getLoaderManager().restartLoader(LOADER_PROFILE, null, mProfileLoaderListener);
+    }
+
+    public void updateMyInfoPreference(boolean hasProfile, String displayName, long contactId) {
+        final CharSequence summary = hasProfile ? displayName : getString(R.string.set_up_profile);
+        mMyInfoPreference.setSummary(summary);
+        mHasProfile = hasProfile;
+        mProfileContactId = contactId;
+        mMyInfoPreference.setOnPreferenceClickListener(this);
+    }
+
+    private void removeUnsupportedPreferences() {
+        // Disable sort order for CJK locales where it is not supported
+        final Resources resources = getResources();
+        if (!resources.getBoolean(R.bool.config_sort_order_user_changeable)) {
+            getPreferenceScreen().removePreference(findPreference(KEY_SORT_ORDER));
+        }
+
+        // Disable display order for CJK locales as well
+        if (!resources.getBoolean(R.bool.config_display_order_user_changeable)) {
+            getPreferenceScreen().removePreference(findPreference(KEY_DISPLAY_ORDER));
+        }
+
+        // Remove the "Default account" setting if there aren't any writable accounts
+        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(getContext());
+        final List<AccountWithDataSet> accounts = accountTypeManager.getAccounts(
+                /* contactWritableOnly */ true);
+        if (accounts.isEmpty()) {
+            getPreferenceScreen().removePreference(findPreference(KEY_DEFAULT_ACCOUNT));
+        }
+
+        final boolean isPhone = TelephonyManagerCompat.isVoiceCapable(
+                (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE));
+        final boolean showBlockedNumbers = isPhone && ContactsUtils.FLAG_N_FEATURE
+                && BlockedNumberContract.canCurrentUserBlockNumbers(getContext());
+        if (!showBlockedNumbers) {
+            getPreferenceScreen().removePreference(findPreference(KEY_BLOCKED_NUMBERS));
+        }
+    }
+
+    private void addExtraPreferences() {
+        final PreferenceManager preferenceManager = ObjectFactory.getPreferenceManager(
+                getContext());
+        if (preferenceManager != null) {
+            for (Preference preference : preferenceManager.getPreferences()) {
+                getPreferenceScreen().addPreference(preference);
+            }
+        }
+    }
+
+    @Override
+    public Context getContext() {
+        return getActivity();
+    }
+
+    private CursorLoader createCursorLoader(Context context) {
+        return new CursorLoader(context) {
+            @Override
+            protected Cursor onLoadInBackground() {
+                try {
+                    return super.onLoadInBackground();
+                } catch (RuntimeException e) {
+                    return null;
+                }
+            }
+        };
+    }
+
+    private String[] getProjection(Context context) {
+        final ContactsPreferences contactsPrefs = new ContactsPreferences(context);
+        final int displayOrder = contactsPrefs.getDisplayOrder();
+        if (displayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+            return ProfileQuery.PROFILE_PROJECTION_PRIMARY;
+        }
+        return ProfileQuery.PROFILE_PROJECTION_ALTERNATIVE;
+    }
+
+    @Override
+    public boolean onPreferenceClick(Preference p) {
+        final String prefKey = p.getKey();
+
+        if (KEY_ABOUT.equals(prefKey)) {
+            ((ContactsPreferenceActivity) getActivity()).showAboutFragment();
+            return true;
+        } else if (KEY_IMPORT_EXPORT.equals(prefKey)) {
+            ImportExportDialogFragment.show(getFragmentManager(), mAreContactsAvailable,
+                    ContactsPreferenceActivity.class,
+                    ImportExportDialogFragment.EXPORT_MODE_ALL_CONTACTS);
+            return true;
+        } else if (KEY_MY_INFO.equals(prefKey)) {
+            final Intent intent;
+            if (mHasProfile) {
+                final Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, mProfileContactId);
+                intent = ImplicitIntentsUtil.composeQuickContactIntent(uri, mModeFullyExpanded);
+                intent.putExtra(mPreviousScreenExtra, ScreenType.ME_CONTACT);
+            } else {
+                intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
+                intent.putExtra(mNewLocalProfileExtra, true);
+            }
+            ImplicitIntentsUtil.startActivityInApp(getActivity(), intent);
+            return true;
+        } else if (KEY_ACCOUNTS.equals(prefKey)) {
+            ImplicitIntentsUtil.startActivityOutsideApp(getContext(),
+                    ImplicitIntentsUtil.getIntentForAddingAccount());
+            return true;
+        } else if (KEY_BLOCKED_NUMBERS.equals(prefKey)) {
+            final Intent intent = TelecomManagerUtil.createManageBlockedNumbersIntent(
+                    (TelecomManager) getContext().getSystemService(Context.TELECOM_SERVICE));
+            startActivity(intent);
+            return true;
+        } else if (KEY_CUSTOM_CONTACTS_FILTER.equals(prefKey)) {
+            final ContactListFilter filter =
+                    ContactListFilterController.getInstance(getContext()).getFilter();
+            AccountFilterUtil.startAccountFilterActivityForResult(
+                    this, REQUEST_CODE_CUSTOM_CONTACTS_FILTER, filter);
+        }
+        return false;
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CODE_CUSTOM_CONTACTS_FILTER
+                && resultCode == Activity.RESULT_OK) {
+            AccountFilterUtil.handleAccountFilterResult(
+                    ContactListFilterController.getInstance(getContext()), resultCode, data);
+            setCustomContactsFilterSummary();
+        } else {
+            super.onActivityResult(requestCode, resultCode, data);
+        }
+    }
+
+    private void setCustomContactsFilterSummary() {
+        final Preference customFilterPreference = findPreference(KEY_CUSTOM_CONTACTS_FILTER);
+        if (customFilterPreference != null) {
+            final ContactListFilter filter =
+                    ContactListFilterController.getInstance(getContext()).getPersistedFilter();
+            if (filter != null) {
+                if (filter.filterType == ContactListFilter.FILTER_TYPE_DEFAULT ||
+                        filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) {
+                    customFilterPreference.setSummary(R.string.list_filter_all_accounts);
+                } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+                    customFilterPreference.setSummary(R.string.listCustomView);
+                } else {
+                    customFilterPreference.setSummary(null);
+                }
+            }
+        }
+    }
+}
+
diff --git a/src/com/android/contacts/common/preference/DisplayOrderPreference.java b/src/com/android/contacts/common/preference/DisplayOrderPreference.java
new file mode 100644
index 0000000..6a182c5
--- /dev/null
+++ b/src/com/android/contacts/common/preference/DisplayOrderPreference.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.preference;
+
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.preference.ListPreference;
+import android.provider.ContactsContract;
+import android.util.AttributeSet;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/**
+ * Custom preference: view-name-as (first name first or last name first).
+ */
+public final class DisplayOrderPreference extends ListPreference {
+
+    private ContactsPreferences mPreferences;
+    private Context mContext;
+
+    public DisplayOrderPreference(Context context) {
+        super(context);
+        prepare();
+    }
+
+    public DisplayOrderPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        prepare();
+    }
+
+    private void prepare() {
+        mContext = getContext();
+        mPreferences = new ContactsPreferences(mContext);
+        setEntries(new String[]{
+                mContext.getString(R.string.display_options_view_given_name_first),
+                mContext.getString(R.string.display_options_view_family_name_first),
+        });
+        setEntryValues(new String[]{
+                String.valueOf(ContactsPreferences.DISPLAY_ORDER_PRIMARY),
+                String.valueOf(ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE),
+        });
+        setValue(String.valueOf(mPreferences.getDisplayOrder()));
+    }
+
+    @Override
+    protected boolean shouldPersist() {
+        return false;   // This preference takes care of its own storage
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        switch (mPreferences.getDisplayOrder()) {
+            case ContactsPreferences.DISPLAY_ORDER_PRIMARY:
+                return mContext.getString(R.string.display_options_view_given_name_first);
+            case ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE:
+                return mContext.getString(R.string.display_options_view_family_name_first);
+        }
+        return null;
+    }
+
+    @Override
+    protected boolean persistString(String value) {
+        int newValue = Integer.parseInt(value);
+        if (newValue != mPreferences.getDisplayOrder()) {
+            mPreferences.setDisplayOrder(newValue);
+            notifyChanged();
+        }
+        return true;
+    }
+
+    @Override
+    // UX recommendation is not to show cancel button on such lists.
+    protected void onPrepareDialogBuilder(Builder builder) {
+        super.onPrepareDialogBuilder(builder);
+        builder.setNegativeButton(null, null);
+    }
+}
diff --git a/src/com/android/contacts/common/preference/PreferenceManager.java b/src/com/android/contacts/common/preference/PreferenceManager.java
new file mode 100644
index 0000000..816f94e
--- /dev/null
+++ b/src/com/android/contacts/common/preference/PreferenceManager.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 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.preference;
+
+import android.preference.Preference;
+
+import java.util.List;
+
+public interface PreferenceManager {
+    List<Preference> getPreferences();
+}
diff --git a/src/com/android/contacts/common/preference/SortOrderPreference.java b/src/com/android/contacts/common/preference/SortOrderPreference.java
new file mode 100644
index 0000000..dfd9550
--- /dev/null
+++ b/src/com/android/contacts/common/preference/SortOrderPreference.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.preference;
+
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/**
+ * Custom preference: sort-by.
+ */
+public final class SortOrderPreference extends ListPreference {
+
+    private ContactsPreferences mPreferences;
+    private Context mContext;
+
+    public SortOrderPreference(Context context) {
+        super(context);
+        prepare();
+    }
+
+    public SortOrderPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        prepare();
+    }
+
+    private void prepare() {
+        mContext = getContext();
+        mPreferences = new ContactsPreferences(mContext);
+        setEntries(new String[]{
+                mContext.getString(R.string.display_options_sort_by_given_name),
+                mContext.getString(R.string.display_options_sort_by_family_name),
+        });
+        setEntryValues(new String[]{
+                String.valueOf(ContactsPreferences.SORT_ORDER_PRIMARY),
+                String.valueOf(ContactsPreferences.SORT_ORDER_ALTERNATIVE),
+        });
+        setValue(String.valueOf(mPreferences.getSortOrder()));
+    }
+
+    @Override
+    protected boolean shouldPersist() {
+        return false;   // This preference takes care of its own storage
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        switch (mPreferences.getSortOrder()) {
+            case ContactsPreferences.SORT_ORDER_PRIMARY:
+                return mContext.getString(R.string.display_options_sort_by_given_name);
+            case ContactsPreferences.SORT_ORDER_ALTERNATIVE:
+                return mContext.getString(R.string.display_options_sort_by_family_name);
+        }
+        return null;
+    }
+
+    @Override
+    protected boolean persistString(String value) {
+        int newValue = Integer.parseInt(value);
+        if (newValue != mPreferences.getSortOrder()) {
+            mPreferences.setSortOrder(newValue);
+            notifyChanged();
+        }
+        return true;
+    }
+
+    @Override
+    // UX recommendation is not to show cancel button on such lists.
+    protected void onPrepareDialogBuilder(Builder builder) {
+        super.onPrepareDialogBuilder(builder);
+        builder.setNegativeButton(null, null);
+    }
+}
diff --git a/src/com/android/contacts/common/testing/InjectedServices.java b/src/com/android/contacts/common/testing/InjectedServices.java
new file mode 100644
index 0000000..e89cec7
--- /dev/null
+++ b/src/com/android/contacts/common/testing/InjectedServices.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.testing;
+
+import android.content.ContentResolver;
+import android.content.SharedPreferences;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+
+import java.util.HashMap;
+
+/**
+ * A mechanism for providing alternative (mock) services to the application
+ * while running tests. Activities, Services and the Application should check
+ * with this class to see if a particular service has been overridden.
+ */
+@NeededForTesting
+public class InjectedServices {
+
+    private ContentResolver mContentResolver;
+    private SharedPreferences mSharedPreferences;
+    private HashMap<String, Object> mSystemServices;
+
+    @NeededForTesting
+    public void setContentResolver(ContentResolver contentResolver) {
+        this.mContentResolver = contentResolver;
+    }
+
+    public ContentResolver getContentResolver() {
+        return mContentResolver;
+    }
+
+    @NeededForTesting
+    public void setSharedPreferences(SharedPreferences sharedPreferences) {
+        this.mSharedPreferences = sharedPreferences;
+    }
+
+    public SharedPreferences getSharedPreferences() {
+        return mSharedPreferences;
+    }
+
+    @NeededForTesting
+    public void setSystemService(String name, Object service) {
+        if (mSystemServices == null) {
+            mSystemServices = Maps.newHashMap();
+        }
+
+        mSystemServices.put(name, service);
+    }
+
+    public Object getSystemService(String name) {
+        if (mSystemServices != null) {
+            return mSystemServices.get(name);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/contacts/common/testing/NeededForTesting.java b/src/com/android/contacts/common/testing/NeededForTesting.java
new file mode 100644
index 0000000..f841d55
--- /dev/null
+++ b/src/com/android/contacts/common/testing/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.testing;
+
+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/AccountFilterUtil.java b/src/com/android/contacts/common/util/AccountFilterUtil.java
new file mode 100644
index 0000000..76975a6
--- /dev/null
+++ b/src/com/android/contacts/common/util/AccountFilterUtil.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.list.AccountFilterActivity;
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.common.list.ContactListFilterController;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for account filter manipulation.
+ */
+public class AccountFilterUtil {
+    private static final String TAG = AccountFilterUtil.class.getSimpleName();
+
+     /**
+      * Launches account filter setting Activity using
+      * {@link Fragment#startActivityForResult(Intent, int)}.
+      *
+      * @param requestCode requestCode for {@link Activity#startActivityForResult(Intent, int)}
+      * @param currentFilter currently-selected filter, so that it can be displayed as activated.
+      */
+     public static void startAccountFilterActivityForResult(
+             Fragment fragment, int requestCode, ContactListFilter currentFilter) {
+         final Activity activity = fragment.getActivity();
+         if (activity != null) {
+             final Intent intent = new Intent(activity, AccountFilterActivity.class);
+             fragment.startActivityForResult(intent, requestCode);
+         } else {
+             Log.w(TAG, "getActivity() returned null. Ignored");
+         }
+     }
+
+    /**
+     * Useful method to handle onActivityResult() for
+     * {@link #startAccountFilterActivityForResult(Fragment, int, ContactListFilter)}.
+     *
+     * This will update filter via a given ContactListFilterController.
+     */
+    public static void handleAccountFilterResult(
+            ContactListFilterController filterController, int resultCode, Intent data) {
+        if (resultCode == Activity.RESULT_OK) {
+            final ContactListFilter filter = (ContactListFilter)
+                    data.getParcelableExtra(AccountFilterActivity.EXTRA_CONTACT_LIST_FILTER);
+            if (filter == null) {
+                return;
+            }
+            if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+                filterController.selectCustomFilter();
+            } else {
+                filterController.setContactListFilter(filter, /* persistent */
+                        filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
+            }
+        }
+    }
+
+    /**
+     * Loads a list of contact list filters
+     */
+    public static class FilterLoader extends AsyncTaskLoader<List<ContactListFilter>> {
+        private Context mContext;
+
+        public FilterLoader(Context context) {
+            super(context);
+            mContext = context;
+        }
+
+        @Override
+        public List<ContactListFilter> loadInBackground() {
+            return loadAccountFilters(mContext);
+        }
+
+        @Override
+        protected void onStartLoading() {
+            forceLoad();
+        }
+
+        @Override
+        protected void onStopLoading() {
+            cancelLoad();
+        }
+
+        @Override
+        protected void onReset() {
+            onStopLoading();
+        }
+    }
+
+    private static List<ContactListFilter> loadAccountFilters(Context context) {
+        final ArrayList<ContactListFilter> accountFilters = Lists.newArrayList();
+        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(context);
+        accountTypeManager.sortAccounts(/* defaultAccount */ getDefaultAccount(context));
+        final List<AccountWithDataSet> accounts =
+                accountTypeManager.getAccounts(/* contactWritableOnly */ true);
+
+        for (AccountWithDataSet account : accounts) {
+            final AccountType accountType =
+                    accountTypeManager.getAccountType(account.type, account.dataSet);
+            if (accountType.isExtension() && !account.hasData(context)) {
+                // Hide extensions with no raw_contacts.
+                continue;
+            }
+            final Drawable icon = accountType != null ? accountType.getDisplayIcon(context) : null;
+            if (account.isLocalAccount()) {
+                accountFilters.add(ContactListFilter.createDeviceContactsFilter(icon));
+            } else {
+                accountFilters.add(ContactListFilter.createAccountFilter(
+                        account.type, account.name, account.dataSet, icon));
+            }
+        }
+
+        final ArrayList<ContactListFilter> result = Lists.newArrayList();
+        result.addAll(accountFilters);
+        return result;
+    }
+
+    private static AccountWithDataSet getDefaultAccount(Context context) {
+        final SharedPreferences prefs =
+                context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
+        final String defaultAccountKey =
+                context.getResources().getString(R.string.contact_editor_default_account_key);
+        final String defaultAccountString = prefs.getString(defaultAccountKey, null);
+        if (TextUtils.isEmpty(defaultAccountString)) {
+            return null;
+        }
+        try {
+            return AccountWithDataSet.unstringify(defaultAccountString);
+        } catch (IllegalArgumentException exception) {
+            Log.e(TAG, "Error with retrieving default account " + exception.toString(), exception);
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/util/AccountSelectionUtil.java b/src/com/android/contacts/common/util/AccountSelectionUtil.java
new file mode 100644
index 0000000..4dc6ec9
--- /dev/null
+++ b/src/com/android/contacts/common/util/AccountSelectionUtil.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.vcard.ImportVCardActivity;
+
+import java.util.List;
+
+/**
+ * Utility class for selecting an Account for importing contact(s)
+ */
+public class AccountSelectionUtil {
+    // TODO: maybe useful for EditContactActivity.java...
+    private static final String LOG_TAG = "AccountSelectionUtil";
+
+    public static boolean mVCardShare = false;
+
+    public static Uri mPath;
+
+    public static class AccountSelectedListener
+            implements DialogInterface.OnClickListener {
+
+        final private Context mContext;
+        final private int mResId;
+        final private int mSubscriptionId;
+
+        final protected List<AccountWithDataSet> mAccountList;
+
+        public AccountSelectedListener(Context context, List<AccountWithDataSet> accountList,
+                int resId, int subscriptionId) {
+            if (accountList == null || accountList.size() == 0) {
+                Log.e(LOG_TAG, "The size of Account list is 0.");
+            }
+            mContext = context;
+            mAccountList = accountList;
+            mResId = resId;
+            mSubscriptionId = subscriptionId;
+        }
+
+        public AccountSelectedListener(Context context, List<AccountWithDataSet> accountList,
+                int resId) {
+            // Subscription id is only needed for importing from SIM card. We can safely ignore
+            // its value for SD card importing.
+            this(context, accountList, resId, /* subscriptionId = */ -1);
+        }
+
+        public void onClick(DialogInterface dialog, int which) {
+            dialog.dismiss();
+            doImport(mContext, mResId, mAccountList.get(which), mSubscriptionId);
+        }
+    }
+
+    public static Dialog getSelectAccountDialog(Context context, int resId) {
+        return getSelectAccountDialog(context, resId, null, null);
+    }
+
+    public static Dialog getSelectAccountDialog(Context context, int resId,
+            DialogInterface.OnClickListener onClickListener) {
+        return getSelectAccountDialog(context, resId, onClickListener, null);
+    }
+
+    /**
+     * When OnClickListener or OnCancelListener is null, uses a default listener.
+     * The default OnCancelListener just closes itself with {@link Dialog#dismiss()}.
+     */
+    public static Dialog getSelectAccountDialog(Context context, int resId,
+            DialogInterface.OnClickListener onClickListener,
+            DialogInterface.OnCancelListener onCancelListener) {
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
+        final List<AccountWithDataSet> writableAccountList = accountTypes.getAccounts(true);
+
+        Log.i(LOG_TAG, "The number of available accounts: " + writableAccountList.size());
+
+        // Assume accountList.size() > 1
+
+        // Wrap our context to inflate list items using correct theme
+        final Context dialogContext = new ContextThemeWrapper(
+                context, android.R.style.Theme_Light);
+        final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        final ArrayAdapter<AccountWithDataSet> accountAdapter =
+            new ArrayAdapter<AccountWithDataSet>(
+                    context, R.layout.account_selector_list_item_condensed, writableAccountList) {
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                if (convertView == null) {
+                    convertView = dialogInflater.inflate(
+                            R.layout.account_selector_list_item_condensed,
+                            parent, false);
+                }
+
+                final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
+                final TextView text2 = (TextView) convertView.findViewById(android.R.id.text2);
+                final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
+
+                final AccountWithDataSet account = this.getItem(position);
+                final AccountType accountType = accountTypes.getAccountType(
+                        account.type, account.dataSet);
+                final Context context = getContext();
+
+                text1.setText(accountType.getDisplayLabel(context));
+                text2.setText(account.name);
+                icon.setImageDrawable(accountType.getDisplayIcon(getContext()));
+
+                return convertView;
+            }
+        };
+
+        if (onClickListener == null) {
+            AccountSelectedListener accountSelectedListener =
+                new AccountSelectedListener(context, writableAccountList, resId);
+            onClickListener = accountSelectedListener;
+        }
+        if (onCancelListener == null) {
+            onCancelListener = new DialogInterface.OnCancelListener() {
+                public void onCancel(DialogInterface dialog) {
+                    dialog.dismiss();
+                }
+            };
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        final TextView title = (TextView) View.inflate(context, R.layout.dialog_title, null);
+        title.setText(R.string.dialog_new_contact_account);
+        builder.setCustomTitle(title);
+        builder.setSingleChoiceItems(accountAdapter, 0, onClickListener);
+        builder.setOnCancelListener(onCancelListener);
+        final AlertDialog result = builder.create();
+        return result;
+    }
+
+    public static void doImport(Context context, int resId, AccountWithDataSet account,
+            int subscriptionId) {
+        if (resId == R.string.import_from_sim) {
+            doImportFromSim(context, account, subscriptionId);
+        } else if (resId == R.string.import_from_vcf_file) {
+            doImportFromVcfFile(context, account);
+        }
+    }
+
+    public static void doImportFromSim(Context context, AccountWithDataSet account,
+            int subscriptionId) {
+        Intent importIntent = new Intent(Intent.ACTION_VIEW);
+        importIntent.setType("vnd.android.cursor.item/sim-contact");
+        if (account != null) {
+            importIntent.putExtra("account_name", account.name);
+            importIntent.putExtra("account_type", account.type);
+            importIntent.putExtra("data_set", account.dataSet);
+        }
+        importIntent.putExtra("subscription_id", (Integer) subscriptionId);
+        importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts");
+        context.startActivity(importIntent);
+    }
+
+    public static void doImportFromVcfFile(Context context, AccountWithDataSet account) {
+        Intent importIntent = new Intent(context, ImportVCardActivity.class);
+        if (account != null) {
+            importIntent.putExtra("account_name", account.name);
+            importIntent.putExtra("account_type", account.type);
+            importIntent.putExtra("data_set", account.dataSet);
+        }
+
+        if (mVCardShare) {
+            importIntent.setAction(Intent.ACTION_VIEW);
+            importIntent.setData(mPath);
+        }
+        mVCardShare = false;
+        mPath = null;
+        context.startActivity(importIntent);
+    }
+}
diff --git a/src/com/android/contacts/common/util/AccountsListAdapter.java b/src/com/android/contacts/common/util/AccountsListAdapter.java
new file mode 100644
index 0000000..ef43a30
--- /dev/null
+++ b/src/com/android/contacts/common/util/AccountsListAdapter.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.text.TextUtils.TruncateAt;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * List-Adapter for Account selection
+ */
+public final class AccountsListAdapter extends BaseAdapter {
+    private final LayoutInflater mInflater;
+    private final List<AccountWithDataSet> mAccounts;
+    private final AccountTypeManager mAccountTypes;
+    private final Context mContext;
+    private int mCustomLayout = -1;
+
+    /**
+     * Filters that affect the list of accounts that is displayed by this adapter.
+     */
+    public enum AccountListFilter {
+        ALL_ACCOUNTS,                   // All read-only and writable accounts
+        ACCOUNTS_CONTACT_WRITABLE,      // Only where the account type is contact writable
+        ACCOUNTS_GROUP_WRITABLE         // Only accounts where the account type is group writable
+    }
+
+    public AccountsListAdapter(Context context, AccountListFilter accountListFilter) {
+        this(context, accountListFilter, null);
+    }
+
+    /**
+     * @param currentAccount the Account currently selected by the user, which should come
+     * first in the list. Can be null.
+     */
+    public AccountsListAdapter(Context context, AccountListFilter accountListFilter,
+            AccountWithDataSet currentAccount) {
+        mContext = context;
+        mAccountTypes = AccountTypeManager.getInstance(context);
+        mAccounts = getAccounts(accountListFilter);
+        if (currentAccount != null
+                && !mAccounts.isEmpty()
+                && !mAccounts.get(0).equals(currentAccount)
+                && mAccounts.remove(currentAccount)) {
+            mAccounts.add(0, currentAccount);
+        }
+        mInflater = LayoutInflater.from(context);
+    }
+
+    private List<AccountWithDataSet> getAccounts(AccountListFilter accountListFilter) {
+        if (accountListFilter == AccountListFilter.ACCOUNTS_GROUP_WRITABLE) {
+            return new ArrayList<AccountWithDataSet>(mAccountTypes.getGroupWritableAccounts());
+        }
+        return new ArrayList<AccountWithDataSet>(mAccountTypes.getAccounts(
+                accountListFilter == AccountListFilter.ACCOUNTS_CONTACT_WRITABLE));
+    }
+
+    public void setCustomLayout(int customLayout) {
+        mCustomLayout = customLayout;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        final View resultView = convertView != null ? convertView :
+                mInflater.inflate(mCustomLayout > 0 ? mCustomLayout :
+                        R.layout.account_selector_list_item_condensed, parent, false);
+
+        final TextView text1 = (TextView) resultView.findViewById(android.R.id.text1);
+        final TextView text2 = (TextView) resultView.findViewById(android.R.id.text2);
+        final ImageView icon = (ImageView) resultView.findViewById(android.R.id.icon);
+
+        final AccountWithDataSet account = mAccounts.get(position);
+        final AccountType accountType = mAccountTypes.getAccountType(account.type, account.dataSet);
+
+        text1.setText(accountType.getDisplayLabel(mContext));
+        text2.setText(account.name);
+
+        icon.setImageDrawable(accountType.getDisplayIcon(mContext));
+
+        return resultView;
+    }
+
+    @Override
+    public int getCount() {
+        return mAccounts.size();
+    }
+
+    @Override
+    public AccountWithDataSet getItem(int position) {
+        return mAccounts.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+}
+
diff --git a/src/com/android/contacts/common/util/BitmapUtil.java b/src/com/android/contacts/common/util/BitmapUtil.java
new file mode 100644
index 0000000..5cd856e
--- /dev/null
+++ b/src/com/android/contacts/common/util/BitmapUtil.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.BitmapDrawable;
+
+/**
+ * Provides static functions to decode bitmaps at the optimal size
+ */
+public class BitmapUtil {
+    private BitmapUtil() {}
+
+    /**
+     * Returns Width or Height of the picture, depending on which size is smaller. Doesn't actually
+     * decode the picture, so it is pretty efficient to run.
+     */
+    public static int getSmallerExtentFromBytes(byte[] bytes) {
+        final BitmapFactory.Options options = new BitmapFactory.Options();
+
+        // don't actually decode the picture, just return its bounds
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+
+        // test what the best sample size is
+        return Math.min(options.outWidth, options.outHeight);
+    }
+
+    /**
+     * Finds the optimal sampleSize for loading the picture
+     * @param originalSmallerExtent Width or height of the picture, whichever is smaller
+     * @param targetExtent Width or height of the target view, whichever is bigger.
+     *
+     * If either one of the parameters is 0 or smaller, no sampling is applied
+     */
+    public static int findOptimalSampleSize(int originalSmallerExtent, int targetExtent) {
+        // If we don't know sizes, we can't do sampling.
+        if (targetExtent < 1) return 1;
+        if (originalSmallerExtent < 1) return 1;
+
+        // Test what the best sample size is. To do that, we find the sample size that gives us
+        // the best trade-off between resulting image size and memory requirement. We allow
+        // the down-sampled image to be 20% smaller than the target size. That way we can get around
+        // unfortunate cases where e.g. a 720 picture is requested for 362 and not down-sampled at
+        // all. Why 20%? Why not. Prove me wrong.
+        int extent = originalSmallerExtent;
+        int sampleSize = 1;
+        while ((extent >> 1) >= targetExtent * 0.8f) {
+            sampleSize <<= 1;
+            extent >>= 1;
+        }
+
+        return sampleSize;
+    }
+
+    /**
+     * Decodes the bitmap with the given sample size
+     */
+    public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) {
+        final BitmapFactory.Options options;
+        if (sampleSize <= 1) {
+            options = null;
+        } else {
+            options = new BitmapFactory.Options();
+            options.inSampleSize = sampleSize;
+        }
+        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+    }
+
+    /**
+     * Retrieves a copy of the specified drawable resource, rotated by a specified angle.
+     *
+     * @param resources The current resources.
+     * @param resourceId The resource ID of the drawable to rotate.
+     * @param angle The angle of rotation.
+     * @return Rotated drawable.
+     */
+    public static Drawable getRotatedDrawable(
+            android.content.res.Resources resources, int resourceId, float angle) {
+
+        // Get the original drawable and make a copy which will be rotated.
+        Bitmap original = BitmapFactory.decodeResource(resources, resourceId);
+        Bitmap rotated = Bitmap.createBitmap(
+                original.getWidth(), original.getHeight(), Bitmap.Config.ARGB_8888);
+
+        // Perform the rotation.
+        Canvas tempCanvas = new Canvas(rotated);
+        tempCanvas.rotate(angle, original.getWidth()/2, original.getHeight()/2);
+        tempCanvas.drawBitmap(original, 0, 0, null);
+
+        return new BitmapDrawable(resources,rotated);
+    }
+
+    /**
+     * Given an input bitmap, scales it to the given width/height and makes it round.
+     *
+     * @param input {@link Bitmap} to scale and crop
+     * @param targetWidth desired output width
+     * @param targetHeight desired output height
+     * @return output bitmap scaled to the target width/height and cropped to an oval. The
+     *         cropping algorithm will try to fit as much of the input into the output as possible,
+     *         while preserving the target width/height ratio.
+     */
+    public static Bitmap getRoundedBitmap(Bitmap input, int targetWidth, int targetHeight) {
+        if (input == null) {
+            return null;
+        }
+        final Bitmap.Config inputConfig = input.getConfig();
+        final Bitmap result = Bitmap.createBitmap(targetWidth, targetHeight,
+                inputConfig != null ? inputConfig : Bitmap.Config.ARGB_8888);
+        final Canvas canvas = new Canvas(result);
+        final Paint paint = new Paint();
+        canvas.drawARGB(0, 0, 0, 0);
+        paint.setAntiAlias(true);
+        final RectF dst = new RectF(0, 0, targetWidth, targetHeight);
+        canvas.drawOval(dst, paint);
+
+        // Specifies that only pixels present in the destination (i.e. the drawn oval) should
+        // be overwritten with pixels from the input bitmap.
+        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+
+        final int inputWidth = input.getWidth();
+        final int inputHeight = input.getHeight();
+
+        // Choose the largest scale factor that will fit inside the dimensions of the
+        // input bitmap.
+        final float scaleBy = Math.min((float) inputWidth / targetWidth,
+            (float) inputHeight / targetHeight);
+
+        final int xCropAmountHalved = (int) (scaleBy * targetWidth / 2);
+        final int yCropAmountHalved = (int) (scaleBy * targetHeight / 2);
+
+        final Rect src = new Rect(
+                inputWidth / 2 - xCropAmountHalved,
+                inputHeight / 2 - yCropAmountHalved,
+                inputWidth / 2 + xCropAmountHalved,
+                inputHeight / 2 + yCropAmountHalved);
+
+        canvas.drawBitmap(input, src, dst, paint);
+        return result;
+    }
+}
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..bba910a
--- /dev/null
+++ b/src/com/android/contacts/common/util/CommonDateUtils.java
@@ -0,0 +1,41 @@
+/*
+ * 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);
+
+    /**
+     * Exchange requires 8:00 for birthdays
+     */
+    public final static int DEFAULT_HOUR = 8;
+}
diff --git a/src/com/android/contacts/common/util/Constants.java b/src/com/android/contacts/common/util/Constants.java
new file mode 100644
index 0000000..c0d6755
--- /dev/null
+++ b/src/com/android/contacts/common/util/Constants.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+public class Constants {
+
+    /**
+     * Log tag for performance measurement.
+     * To enable: adb shell setprop log.tag.ContactsPerf VERBOSE
+     */
+    public static final String PERFORMANCE_TAG = "ContactsPerf";
+
+    // Used for lookup URI that contains an encoded JSON string.
+    public static final String LOOKUP_URI_ENCODED = "encoded";
+}
diff --git a/src/com/android/contacts/common/util/ContactDisplayUtils.java b/src/com/android/contacts/common/util/ContactDisplayUtils.java
new file mode 100644
index 0000000..0a50748
--- /dev/null
+++ b/src/com/android/contacts/common/util/ContactDisplayUtils.java
@@ -0,0 +1,297 @@
+/*
+ * 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 static android.provider.ContactsContract.CommonDataKinds.Phone;
+
+import com.google.common.base.Preconditions;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.annotation.Nullable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TtsSpan;
+import android.util.Log;
+import android.util.Patterns;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/**
+ * Methods for handling various contact data labels.
+ */
+public class ContactDisplayUtils {
+
+    private static final String TAG = ContactDisplayUtils.class.getSimpleName();
+
+    public static final int INTERACTION_CALL = 1;
+    public static final int INTERACTION_SMS = 2;
+
+    /**
+     * Checks if the given data type is a custom type.
+     *
+     * @param type Phone data type.
+     * @return {@literal true} if the type is custom.  {@literal false} if not.
+     */
+    public static boolean isCustomPhoneType(Integer type) {
+        return type == Phone.TYPE_CUSTOM || type == Phone.TYPE_ASSISTANT;
+    }
+
+    /**
+     * Gets a display label for a given phone type.
+     *
+     * @param type The type of number.
+     * @param customLabel A custom label to use if the phone is determined to be of custom type
+     * determined by {@link #isCustomPhoneType(Integer))}
+     * @param interactionType whether this is a call or sms.  Either {@link #INTERACTION_CALL} or
+     * {@link #INTERACTION_SMS}.
+     * @param context The application context.
+     * @return An appropriate string label
+     */
+    public static CharSequence getLabelForCallOrSms(Integer type, CharSequence customLabel,
+            int interactionType, Context context) {
+        Preconditions.checkNotNull(context);
+
+        if (isCustomPhoneType(type)) {
+            return (customLabel == null) ? "" : customLabel;
+        } else {
+            int resId;
+            if (interactionType == INTERACTION_SMS) {
+                resId = getSmsLabelResourceId(type);
+            } else {
+                resId = getPhoneLabelResourceId(type);
+                if (interactionType != INTERACTION_CALL) {
+                    Log.e(TAG, "Un-recognized interaction type: " + interactionType +
+                            ". Defaulting to ContactDisplayUtils.INTERACTION_CALL.");
+                }
+            }
+
+            return context.getResources().getText(resId);
+        }
+    }
+
+    /**
+     * Find a label for calling.
+     *
+     * @param type The type of number.
+     * @return An appropriate string label.
+     */
+    public static int getPhoneLabelResourceId(Integer type) {
+        if (type == null) return R.string.call_other;
+        switch (type) {
+            case Phone.TYPE_HOME:
+                return R.string.call_home;
+            case Phone.TYPE_MOBILE:
+                return R.string.call_mobile;
+            case Phone.TYPE_WORK:
+                return R.string.call_work;
+            case Phone.TYPE_FAX_WORK:
+                return R.string.call_fax_work;
+            case Phone.TYPE_FAX_HOME:
+                return R.string.call_fax_home;
+            case Phone.TYPE_PAGER:
+                return R.string.call_pager;
+            case Phone.TYPE_OTHER:
+                return R.string.call_other;
+            case Phone.TYPE_CALLBACK:
+                return R.string.call_callback;
+            case Phone.TYPE_CAR:
+                return R.string.call_car;
+            case Phone.TYPE_COMPANY_MAIN:
+                return R.string.call_company_main;
+            case Phone.TYPE_ISDN:
+                return R.string.call_isdn;
+            case Phone.TYPE_MAIN:
+                return R.string.call_main;
+            case Phone.TYPE_OTHER_FAX:
+                return R.string.call_other_fax;
+            case Phone.TYPE_RADIO:
+                return R.string.call_radio;
+            case Phone.TYPE_TELEX:
+                return R.string.call_telex;
+            case Phone.TYPE_TTY_TDD:
+                return R.string.call_tty_tdd;
+            case Phone.TYPE_WORK_MOBILE:
+                return R.string.call_work_mobile;
+            case Phone.TYPE_WORK_PAGER:
+                return R.string.call_work_pager;
+            case Phone.TYPE_ASSISTANT:
+                return R.string.call_assistant;
+            case Phone.TYPE_MMS:
+                return R.string.call_mms;
+            default:
+                return R.string.call_custom;
+        }
+
+    }
+
+    /**
+     * Find a label for sending an sms.
+     *
+     * @param type The type of number.
+     * @return An appropriate string label.
+     */
+    public static int getSmsLabelResourceId(Integer type) {
+        if (type == null) return R.string.sms_other;
+        switch (type) {
+            case Phone.TYPE_HOME:
+                return R.string.sms_home;
+            case Phone.TYPE_MOBILE:
+                return R.string.sms_mobile;
+            case Phone.TYPE_WORK:
+                return R.string.sms_work;
+            case Phone.TYPE_FAX_WORK:
+                return R.string.sms_fax_work;
+            case Phone.TYPE_FAX_HOME:
+                return R.string.sms_fax_home;
+            case Phone.TYPE_PAGER:
+                return R.string.sms_pager;
+            case Phone.TYPE_OTHER:
+                return R.string.sms_other;
+            case Phone.TYPE_CALLBACK:
+                return R.string.sms_callback;
+            case Phone.TYPE_CAR:
+                return R.string.sms_car;
+            case Phone.TYPE_COMPANY_MAIN:
+                return R.string.sms_company_main;
+            case Phone.TYPE_ISDN:
+                return R.string.sms_isdn;
+            case Phone.TYPE_MAIN:
+                return R.string.sms_main;
+            case Phone.TYPE_OTHER_FAX:
+                return R.string.sms_other_fax;
+            case Phone.TYPE_RADIO:
+                return R.string.sms_radio;
+            case Phone.TYPE_TELEX:
+                return R.string.sms_telex;
+            case Phone.TYPE_TTY_TDD:
+                return R.string.sms_tty_tdd;
+            case Phone.TYPE_WORK_MOBILE:
+                return R.string.sms_work_mobile;
+            case Phone.TYPE_WORK_PAGER:
+                return R.string.sms_work_pager;
+            case Phone.TYPE_ASSISTANT:
+                return R.string.sms_assistant;
+            case Phone.TYPE_MMS:
+                return R.string.sms_mms;
+            default:
+                return R.string.sms_custom;
+        }
+    }
+
+    /**
+     * Whether the given text could be a phone number.
+     *
+     * Note this will miss many things that are legitimate phone numbers, for example,
+     * phone numbers with letters.
+     */
+    public static boolean isPossiblePhoneNumber(CharSequence text) {
+        return text == null ? false : Patterns.PHONE.matcher(text.toString()).matches();
+    }
+
+    /**
+     * Returns a Spannable for the given message with a telephone {@link TtsSpan} set for
+     * the given phone number text wherever it is found within the message.
+     */
+    public static Spannable getTelephoneTtsSpannable(String message, String phoneNumber) {
+        if (message == null) {
+            return null;
+        }
+        final Spannable spannable = new SpannableString(message);
+        int start = phoneNumber == null ? -1 : message.indexOf(phoneNumber);
+        while (start >= 0) {
+            final int end = start + phoneNumber.length();
+            final TtsSpan ttsSpan = PhoneNumberUtilsCompat.createTtsSpan(phoneNumber);
+            spannable.setSpan(ttsSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);             // this is consistenly done in a misleading way..
+            start = message.indexOf(phoneNumber, end);
+        }
+        return spannable;
+    }
+
+    /**
+     * Retrieves a string from a string template that takes 1 phone number as argument,
+     * span the number with a telephone {@link TtsSpan}, and return the spanned string.
+     *
+     * @param resources to retrieve the string from
+     * @param stringId ID of the string
+     * @param number to pass in the template
+     * @return CharSequence with the phone number wrapped in a TtsSpan
+     */
+    public static CharSequence getTtsSpannedPhoneNumber(Resources resources,
+            int stringId, String number){
+        String msg = resources.getString(stringId, number);
+        return ContactDisplayUtils.getTelephoneTtsSpannable(msg, number);
+    }
+
+    /**
+     * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}.
+     * Defaults to the name that is non-null.
+     *
+     * @param namePrimary the primary name.
+     * @param nameAlternative the alternative name.
+     * @param contactsPreferences the ContactsPreferences used to determine the preferred
+     * display name.
+     * @return namePrimary or nameAlternative depending on the value of displayOrderPreference.
+     */
+    public static String getPreferredDisplayName(String namePrimary, String nameAlternative,
+            @Nullable ContactsPreferences contactsPreferences) {
+        if (contactsPreferences == null) {
+            return namePrimary != null ? namePrimary : nameAlternative;
+        }
+        if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+            return namePrimary;
+        }
+
+        if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE
+                && !TextUtils.isEmpty(nameAlternative)) {
+            return nameAlternative;
+        }
+
+        return namePrimary;
+    }
+
+    /**
+     * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}.
+     * Defaults to the name that is non-null.
+     *
+     * @param namePrimary the primary name.
+     * @param nameAlternative the alternative name.
+     * @param contactsPreferences the ContactsPreferences used to determine the preferred sort
+     * order.
+     * @return namePrimary or nameAlternative depending on the value of displayOrderPreference.
+     */
+    public static String getPreferredSortName(String namePrimary, String nameAlternative,
+            @Nullable ContactsPreferences contactsPreferences) {
+        if (contactsPreferences == null) {
+            return namePrimary != null ? namePrimary : nameAlternative;
+        }
+
+        if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+            return namePrimary;
+        }
+
+        if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_ALTERNATIVE &&
+                !TextUtils.isEmpty(nameAlternative)) {
+            return nameAlternative;
+        }
+
+        return namePrimary;
+    }
+}
diff --git a/src/com/android/contacts/common/util/ContactLoaderUtils.java b/src/com/android/contacts/common/util/ContactLoaderUtils.java
new file mode 100644
index 0000000..0ec8887
--- /dev/null
+++ b/src/com/android/contacts/common/util/ContactLoaderUtils.java
@@ -0,0 +1,78 @@
+/*
+ * 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.util;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.net.Uri;
+import android.provider.Contacts;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+
+/**
+ * Utility methods for the {@link ContactLoader}.
+ */
+public final class ContactLoaderUtils {
+
+    /** Static helper, not instantiable. */
+    private ContactLoaderUtils() {}
+
+    /**
+     * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
+     * For legacy contacts, a raw-contact lookup is performed. An {@link IllegalArgumentException}
+     * can be thrown if the URI is null or the authority is not recognized.
+     *
+     * Do not call from the UI thread.
+     */
+    @SuppressWarnings("deprecation")
+    public static Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri)
+            throws IllegalArgumentException {
+        if (uri == null) throw new IllegalArgumentException("uri must not be null");
+
+        final String authority = uri.getAuthority();
+
+        // Current Style Uri?
+        if (ContactsContract.AUTHORITY.equals(authority)) {
+            final String type = resolver.getType(uri);
+            // Contact-Uri? Good, return it
+            if (ContactsContract.Contacts.CONTENT_ITEM_TYPE.equals(type)) {
+                return uri;
+            }
+
+            // RawContact-Uri? Transform it to ContactUri
+            if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
+                final long rawContactId = ContentUris.parseId(uri);
+                return RawContacts.getContactLookupUri(resolver,
+                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+            }
+
+            // Anything else? We don't know what this is
+            throw new IllegalArgumentException("uri format is unknown");
+        }
+
+        // Legacy Style? Convert to RawContact
+        final String OBSOLETE_AUTHORITY = Contacts.AUTHORITY;
+        if (OBSOLETE_AUTHORITY.equals(authority)) {
+            // Legacy Format. Convert to RawContact-Uri and then lookup the contact
+            final long rawContactId = ContentUris.parseId(uri);
+            return RawContacts.getContactLookupUri(resolver,
+                    ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+        }
+
+        throw new IllegalArgumentException("uri authority is unknown");
+    }
+}
diff --git a/src/com/android/contacts/common/util/DataStatus.java b/src/com/android/contacts/common/util/DataStatus.java
new file mode 100644
index 0000000..76f11b6
--- /dev/null
+++ b/src/com/android/contacts/common/util/DataStatus.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.Data;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+
+import com.android.contacts.common.R;
+
+/**
+ * Storage for a social status update. Holds a single update, but can use
+ * {@link #possibleUpdate(Cursor)} to consider updating when a better status
+ * exists. Statuses with timestamps, or with newer timestamps win.
+ */
+public class DataStatus {
+    private int mPresence = -1;
+    private String mStatus = null;
+    private long mTimestamp = -1;
+
+    private String mResPackage = null;
+    private int mIconRes = -1;
+    private int mLabelRes = -1;
+
+    public DataStatus() {
+    }
+
+    public DataStatus(Cursor cursor) {
+        // When creating from cursor row, fill normally
+        fromCursor(cursor);
+    }
+
+    /**
+     * Attempt updating this {@link DataStatus} based on values at the
+     * current row of the given {@link Cursor}.
+     */
+    public void possibleUpdate(Cursor cursor) {
+        final boolean hasStatus = !isNull(cursor, Data.STATUS);
+        final boolean hasTimestamp = !isNull(cursor, Data.STATUS_TIMESTAMP);
+
+        // Bail early when not valid status, or when previous status was
+        // found and we can't compare this one.
+        if (!hasStatus) return;
+        if (isValid() && !hasTimestamp) return;
+
+        if (hasTimestamp) {
+            // Compare timestamps and bail if older status
+            final long newTimestamp = getLong(cursor, Data.STATUS_TIMESTAMP, -1);
+            if (newTimestamp < mTimestamp) return;
+
+            mTimestamp = newTimestamp;
+        }
+
+        // Fill in remaining details from cursor
+        fromCursor(cursor);
+    }
+
+    private void fromCursor(Cursor cursor) {
+        mPresence = getInt(cursor, Data.PRESENCE, -1);
+        mStatus = getString(cursor, Data.STATUS);
+        mTimestamp = getLong(cursor, Data.STATUS_TIMESTAMP, -1);
+        mResPackage = getString(cursor, Data.STATUS_RES_PACKAGE);
+        mIconRes = getInt(cursor, Data.STATUS_ICON, -1);
+        mLabelRes = getInt(cursor, Data.STATUS_LABEL, -1);
+    }
+
+    public boolean isValid() {
+        return !TextUtils.isEmpty(mStatus);
+    }
+
+    public int getPresence() {
+        return mPresence;
+    }
+
+    public CharSequence getStatus() {
+        return mStatus;
+    }
+
+    public long getTimestamp() {
+        return mTimestamp;
+    }
+
+    /**
+     * Build any timestamp and label into a single string.
+     */
+    public CharSequence getTimestampLabel(Context context) {
+        final PackageManager pm = context.getPackageManager();
+
+        // Use local package for resources when none requested
+        if (mResPackage == null) mResPackage = context.getPackageName();
+
+        final boolean validTimestamp = mTimestamp > 0;
+        final boolean validLabel = mResPackage != null && mLabelRes != -1;
+
+        final CharSequence timeClause = validTimestamp ? DateUtils.getRelativeTimeSpanString(
+                mTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
+                DateUtils.FORMAT_ABBREV_RELATIVE) : null;
+        final CharSequence labelClause = validLabel ? pm.getText(mResPackage, mLabelRes,
+                null) : null;
+
+        if (validTimestamp && validLabel) {
+            return context.getString(
+                    R.string.contact_status_update_attribution_with_date,
+                    timeClause, labelClause);
+        } else if (validLabel) {
+            return context.getString(
+                    R.string.contact_status_update_attribution,
+                    labelClause);
+        } else if (validTimestamp) {
+            return timeClause;
+        } else {
+            return null;
+        }
+    }
+
+    public Drawable getIcon(Context context) {
+        final PackageManager pm = context.getPackageManager();
+
+        // Use local package for resources when none requested
+        if (mResPackage == null) mResPackage = context.getPackageName();
+
+        final boolean validIcon = mResPackage != null && mIconRes != -1;
+        return validIcon ? pm.getDrawable(mResPackage, mIconRes, null) : null;
+    }
+
+    private static String getString(Cursor cursor, String columnName) {
+        return cursor.getString(cursor.getColumnIndex(columnName));
+    }
+
+    private static int getInt(Cursor cursor, String columnName) {
+        return cursor.getInt(cursor.getColumnIndex(columnName));
+    }
+
+    private static int getInt(Cursor cursor, String columnName, int missingValue) {
+        final int columnIndex = cursor.getColumnIndex(columnName);
+        return cursor.isNull(columnIndex) ? missingValue : cursor.getInt(columnIndex);
+    }
+
+    private static long getLong(Cursor cursor, String columnName, long missingValue) {
+        final int columnIndex = cursor.getColumnIndex(columnName);
+        return cursor.isNull(columnIndex) ? missingValue : cursor.getLong(columnIndex);
+    }
+
+    private static boolean isNull(Cursor cursor, String columnName) {
+        return cursor.isNull(cursor.getColumnIndex(columnName));
+    }
+}
diff --git a/src/com/android/contacts/common/util/DateUtils.java b/src/com/android/contacts/common/util/DateUtils.java
new file mode 100644
index 0000000..c695ec6
--- /dev/null
+++ b/src/com/android/contacts/common/util/DateUtils.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.Time;
+
+
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Utility methods for processing dates.
+ */
+public class DateUtils {
+    public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
+
+    /**
+     * When parsing a date without a year, the system assumes 1970, which wasn't a leap-year.
+     * Let's add a one-off hack for that day of the year
+     */
+    public static final String NO_YEAR_DATE_FEB29TH = "--02-29";
+
+    // Variations of ISO 8601 date format.  Do not change the order - it does affect the
+    // result in ambiguous cases.
+    private static final SimpleDateFormat[] DATE_FORMATS = {
+        CommonDateUtils.FULL_DATE_FORMAT,
+        CommonDateUtils.DATE_AND_TIME_FORMAT,
+        new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US),
+        new SimpleDateFormat("yyyyMMdd", Locale.US),
+        new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS'Z'", Locale.US),
+        new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US),
+        new SimpleDateFormat("yyyyMMdd'T'HHmm'Z'", Locale.US),
+    };
+
+    static {
+        for (SimpleDateFormat format : DATE_FORMATS) {
+            format.setLenient(true);
+            format.setTimeZone(UTC_TIMEZONE);
+        }
+        CommonDateUtils.NO_YEAR_DATE_FORMAT.setTimeZone(UTC_TIMEZONE);
+    }
+
+    /**
+     * Parses the supplied string to see if it looks like a date.
+     *
+     * @param string The string representation of the provided date
+     * @param mustContainYear If true, the string is parsed as a date containing a year. If false,
+     * the string is parsed into a valid date even if the year field is missing.
+     * @return A Calendar object corresponding to the date if the string is successfully parsed.
+     * If not, null is returned.
+     */
+    public static Calendar parseDate(String string, boolean mustContainYear) {
+        ParsePosition parsePosition = new ParsePosition(0);
+        Date date;
+        if (!mustContainYear) {
+            final boolean noYearParsed;
+            // Unfortunately, we can't parse Feb 29th correctly, so let's handle this day seperately
+            if (NO_YEAR_DATE_FEB29TH.equals(string)) {
+                return getUtcDate(0, Calendar.FEBRUARY, 29);
+            } else {
+                synchronized (CommonDateUtils.NO_YEAR_DATE_FORMAT) {
+                    date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(string, parsePosition);
+                }
+                noYearParsed = parsePosition.getIndex() == string.length();
+            }
+
+            if (noYearParsed) {
+                return getUtcDate(date, true);
+            }
+        }
+        for (int i = 0; i < DATE_FORMATS.length; i++) {
+            SimpleDateFormat f = DATE_FORMATS[i];
+            synchronized (f) {
+                parsePosition.setIndex(0);
+                date = f.parse(string, parsePosition);
+                if (parsePosition.getIndex() == string.length()) {
+                    return getUtcDate(date, false);
+                }
+            }
+        }
+        return null;
+    }
+
+    private static final Calendar getUtcDate(Date date, boolean noYear) {
+        final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
+        calendar.setTime(date);
+        if (noYear) {
+            calendar.set(Calendar.YEAR, 0);
+        }
+        return calendar;
+    }
+
+    private static final Calendar getUtcDate(int year, int month, int dayOfMonth) {
+        final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
+        calendar.clear();
+        calendar.set(Calendar.YEAR, year);
+        calendar.set(Calendar.MONTH, month);
+        calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
+        return calendar;
+    }
+
+    public static boolean isYearSet(Calendar cal) {
+        // use the Calendar.YEAR field to track whether or not the year is set instead of
+        // Calendar.isSet() because doing Calendar.get() causes Calendar.isSet() to become
+        // true irregardless of what the previous value was
+        return cal.get(Calendar.YEAR) > 1;
+    }
+
+    /**
+     * Same as {@link #formatDate(Context context, String string, boolean longForm)}, with
+     * longForm set to {@code true} by default.
+     *
+     * @param context Valid context
+     * @param string String representation of a date to parse
+     * @return Returns the same date in a cleaned up format. If the supplied string does not look
+     * like a date, return it unchanged.
+     */
+
+    public static String formatDate(Context context, String string) {
+        return formatDate(context, string, true);
+    }
+
+    /**
+     * Parses the supplied string to see if it looks like a date.
+     *
+     * @param context Valid context
+     * @param string String representation of a date to parse
+     * @param longForm If true, return the date formatted into its long string representation.
+     * If false, return the date formatted using its short form representation (i.e. 12/11/2012)
+     * @return Returns the same date in a cleaned up format. If the supplied string does not look
+     * like a date, return it unchanged.
+     */
+    public static String formatDate(Context context, String string, boolean longForm) {
+        if (string == null) {
+            return null;
+        }
+
+        string = string.trim();
+        if (string.length() == 0) {
+            return string;
+        }
+        final Calendar cal = parseDate(string, false);
+
+        // we weren't able to parse the string successfully so just return it unchanged
+        if (cal == null) {
+            return string;
+        }
+
+        final boolean isYearSet = isYearSet(cal);
+        final java.text.DateFormat outFormat;
+        if (!isYearSet) {
+            outFormat = getLocalizedDateFormatWithoutYear(context);
+        } else {
+            outFormat =
+                    longForm ? DateFormat.getLongDateFormat(context) :
+                    DateFormat.getDateFormat(context);
+        }
+        synchronized (outFormat) {
+            outFormat.setTimeZone(UTC_TIMEZONE);
+            return outFormat.format(cal.getTime());
+        }
+    }
+
+    public static boolean isMonthBeforeDay(Context context) {
+        char[] dateFormatOrder = DateFormat.getDateFormatOrder(context);
+        for (int i = 0; i < dateFormatOrder.length; i++) {
+            if (dateFormatOrder[i] == 'd') {
+                return false;
+            }
+            if (dateFormatOrder[i] == 'M') {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns a SimpleDateFormat object without the year fields by using a regular expression
+     * to eliminate the year in the string pattern. In the rare occurence that the resulting
+     * pattern cannot be reconverted into a SimpleDateFormat, it uses the provided context to
+     * determine whether the month field should be displayed before the day field, and returns
+     * either "MMMM dd" or "dd MMMM" converted into a SimpleDateFormat.
+     */
+    public static java.text.DateFormat getLocalizedDateFormatWithoutYear(Context context) {
+        final String pattern = ((SimpleDateFormat) SimpleDateFormat.getDateInstance(
+                java.text.DateFormat.LONG)).toPattern();
+        // Determine the correct regex pattern for year.
+        // Special case handling for Spanish locale by checking for "de"
+        final String yearPattern = pattern.contains(
+                "de") ? "[^Mm]*[Yy]+[^Mm]*" : "[^DdMm]*[Yy]+[^DdMm]*";
+        try {
+         // Eliminate the substring in pattern that matches the format for that of year
+            return new SimpleDateFormat(pattern.replaceAll(yearPattern, ""));
+        } catch (IllegalArgumentException e) {
+            return new SimpleDateFormat(
+                    DateUtils.isMonthBeforeDay(context) ? "MMMM dd" : "dd MMMM");
+        }
+    }
+
+    /**
+     * Given a calendar (possibly containing only a day of the year), returns the earliest possible
+     * anniversary of the date that is equal to or after the current point in time if the date
+     * does not contain a year, or the date converted to the local time zone (if the date contains
+     * a year.
+     *
+     * @param target The date we wish to convert(in the UTC time zone).
+     * @return If date does not contain a year (year < 1900), returns the next earliest anniversary
+     * that is after the current point in time (in the local time zone). Otherwise, returns the
+     * adjusted Date in the local time zone.
+     */
+    public static Date getNextAnnualDate(Calendar target) {
+        final Calendar today = Calendar.getInstance();
+        today.setTime(new Date());
+
+        // Round the current time to the exact start of today so that when we compare
+        // today against the target date, both dates are set to exactly 0000H.
+        today.set(Calendar.HOUR_OF_DAY, 0);
+        today.set(Calendar.MINUTE, 0);
+        today.set(Calendar.SECOND, 0);
+        today.set(Calendar.MILLISECOND, 0);
+
+        final boolean isYearSet = isYearSet(target);
+        final int targetYear = target.get(Calendar.YEAR);
+        final int targetMonth = target.get(Calendar.MONTH);
+        final int targetDay = target.get(Calendar.DAY_OF_MONTH);
+        final boolean isFeb29 = (targetMonth == Calendar.FEBRUARY && targetDay == 29);
+        final GregorianCalendar anniversary = new GregorianCalendar();
+        // Convert from the UTC date to the local date. Set the year to today's year if the
+        // there is no provided year (targetYear < 1900)
+        anniversary.set(!isYearSet ? today.get(Calendar.YEAR) : targetYear,
+                targetMonth, targetDay);
+        // If the anniversary's date is before the start of today and there is no year set,
+        // increment the year by 1 so that the returned date is always equal to or greater than
+        // today. If the day is a leap year, keep going until we get the next leap year anniversary
+        // Otherwise if there is already a year set, simply return the exact date.
+        if (!isYearSet) {
+            int anniversaryYear = today.get(Calendar.YEAR);
+            if (anniversary.before(today) ||
+                    (isFeb29 && !anniversary.isLeapYear(anniversaryYear))) {
+                // If the target date is not Feb 29, then set the anniversary to the next year.
+                // Otherwise, keep going until we find the next leap year (this is not guaranteed
+                // to be in 4 years time).
+                do {
+                    anniversaryYear +=1;
+                } while (isFeb29 && !anniversary.isLeapYear(anniversaryYear));
+                anniversary.set(anniversaryYear, targetMonth, targetDay);
+            }
+        }
+        return anniversary.getTime();
+    }
+
+    /**
+     * Determine the difference, in days between two dates.  Uses similar logic as the
+     * {@link android.text.format.DateUtils.getRelativeTimeSpanString} method.
+     *
+     * @param time Instance of time object to use for calculations.
+     * @param date1 First date to check.
+     * @param date2 Second date to check.
+     * @return The absolute difference in days between the two dates.
+     */
+    public static int getDayDifference(Time time, long date1, long date2) {
+        time.set(date1);
+        int startDay = Time.getJulianDay(date1, time.gmtoff);
+
+        time.set(date2);
+        int currentDay = Time.getJulianDay(date2, time.gmtoff);
+
+        return Math.abs(currentDay - startDay);
+    }
+}
diff --git a/src/com/android/contacts/common/util/EmptyService.java b/src/com/android/contacts/common/util/EmptyService.java
new file mode 100644
index 0000000..c5c3608
--- /dev/null
+++ b/src/com/android/contacts/common/util/EmptyService.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Background {@link Service} that is used to keep our process alive long enough
+ * for background threads to finish. Started and stopped directly by specific
+ * background tasks when needed.
+ */
+public class EmptyService extends Service {
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+}
diff --git a/src/com/android/contacts/common/util/ImplicitIntentsUtil.java b/src/com/android/contacts/common/util/ImplicitIntentsUtil.java
new file mode 100644
index 0000000..c6a875b
--- /dev/null
+++ b/src/com/android/contacts/common/util/ImplicitIntentsUtil.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.QuickContact;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import com.android.contacts.common.model.account.GoogleAccountType;
+
+import java.util.List;
+
+/**
+ * Utility for forcing intents to be started inside the current app. This is useful for avoiding
+ * senseless disambiguation dialogs. Ie, if a user clicks a contact inside Contacts we assume
+ * they want to view the contact inside the Contacts app as opposed to a 3rd party contacts app.
+ *
+ * Methods are designed to replace the use of startActivity() for implicit intents. This class isn't
+ * necessary for explicit intents. No attempt is made to replace startActivityForResult(), since
+ * startActivityForResult() is always used with explicit intents in this project.
+ *
+ * Why not just always use explicit intents? The Contacts/Dialer app implements standard intent
+ * actions used by others apps. We want to continue exercising these intent filters to make sure
+ * they still work. Plus we sometimes don't know an explicit intent would work. See
+ * {@link #startActivityInAppIfPossible}.
+ *
+ * Some ContactsCommon code that is only used by Dialer doesn't use ImplicitIntentsUtil.
+ */
+public class ImplicitIntentsUtil {
+
+    /**
+     * Start an intent. If it is possible for this app to handle the intent, force this app's
+     * activity to handle the intent. Sometimes it is impossible to know whether this app
+     * can handle an intent while coding since the code is used inside both Dialer and Contacts.
+     * This method is particularly useful in such circumstances.
+     *
+     * On a Nexus 5 with a small number of apps, this method consistently added 3-16ms of delay
+     * in order to talk to the package manager.
+     */
+    public static void startActivityInAppIfPossible(Context context, Intent intent) {
+        final Intent appIntent = getIntentInAppIfExists(context, intent);
+        if (appIntent != null) {
+            context.startActivity(appIntent);
+        } else {
+            context.startActivity(intent);
+        }
+    }
+
+    /**
+     * Start intent using an activity inside this app. This method is useful if you are certain
+     * that the intent can be handled inside this app, and you care about shaving milliseconds.
+     */
+    public static void startActivityInApp(Context context, Intent intent) {
+        String packageName = context.getPackageName();
+        intent.setPackage(packageName);
+        context.startActivity(intent);
+    }
+
+    /**
+     * Start an intent normally. Assert that the intent can't be opened inside this app.
+     */
+    public static void startActivityOutsideApp(Context context, Intent intent) {
+        final boolean isPlatformDebugBuild = Build.TYPE.equals("eng")
+                || Build.TYPE.equals("userdebug");
+        if (isPlatformDebugBuild) {
+            if (getIntentInAppIfExists(context, intent) != null) {
+                throw new AssertionError("startActivityOutsideApp() was called for an intent" +
+                        " that can be handled inside the app");
+            }
+        }
+        context.startActivity(intent);
+    }
+
+    /**
+     * Returns an implicit intent for opening QuickContacts.
+     */
+    public static Intent composeQuickContactIntent(Uri contactLookupUri,
+            int extraMode) {
+        final Intent intent = new Intent(QuickContact.ACTION_QUICK_CONTACT);
+        intent.setData(contactLookupUri);
+        intent.putExtra(QuickContact.EXTRA_MODE, extraMode);
+        // Make sure not to show QuickContacts on top of another QuickContacts.
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        return intent;
+    }
+
+    /**
+     * Returns an Intent to open the Settings add account activity filtered to only
+     * display contact provider account types.
+     */
+    public static Intent getIntentForAddingAccount() {
+        final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+        intent.putExtra(Settings.EXTRA_AUTHORITIES,
+                new String[]{ContactsContract.AUTHORITY});
+        return intent;
+    }
+
+    /**
+     * Returns an Intent to add a google account.
+     */
+    public static Intent getIntentForAddingGoogleAccount() {
+        final Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+        intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES,
+                new String[]{GoogleAccountType.ACCOUNT_TYPE});
+        return intent;
+    }
+
+    /**
+     * Returns a copy of {@param intent} with a class name set, if a class inside this app
+     * has a corresponding intent filter.
+     */
+    private static Intent getIntentInAppIfExists(Context context, Intent intent) {
+        try {
+            final Intent intentCopy = new Intent(intent);
+            // Force this intentCopy to open inside the current app.
+            intentCopy.setPackage(context.getPackageName());
+            final List<ResolveInfo> list = context.getPackageManager().queryIntentActivities(
+                    intentCopy, PackageManager.MATCH_DEFAULT_ONLY);
+            if (list != null && list.size() != 0) {
+                // Now that we know the intentCopy will work inside the current app, we
+                // can return this intent non-null.
+                if (list.get(0).activityInfo != null
+                        && !TextUtils.isEmpty(list.get(0).activityInfo.name)) {
+                    // Now that we know the class name, we may as well attach it to intentCopy
+                    // to prevent the package manager from needing to find it again inside
+                    // startActivity(). This is only needed for efficiency.
+                    intentCopy.setClassName(context.getPackageName(),
+                            list.get(0).activityInfo.name);
+                }
+                return intentCopy;
+            }
+            return null;
+        } catch (Exception e) {
+            // Don't let the package manager crash our app. If the package manager can't resolve the
+            // intent here, then we can still call startActivity without calling setClass() first.
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/util/LocalizedNameResolver.java b/src/com/android/contacts/common/util/LocalizedNameResolver.java
new file mode 100644
index 0000000..92104c4
--- /dev/null
+++ b/src/com/android/contacts/common/util/LocalizedNameResolver.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.util;
+
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.account.ExternalAccountType;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Retrieves localized names per account type. This allows customizing texts like
+ * "All Contacts" for certain account types, but e.g. "All Friends" or "All Connections" for others.
+ */
+public class LocalizedNameResolver  {
+    private static final String TAG = "LocalizedNameResolver";
+
+    private static final String CONTACTS_DATA_KIND = "ContactsDataKind";
+
+    /**
+     * Returns the name for All Contacts for the specified account type.
+     */
+    public static String getAllContactsName(Context context, String accountType) {
+        if (context == null) throw new IllegalArgumentException("Context must not be null");
+        if (accountType == null) return null;
+
+        return resolveAllContactsName(context, accountType);
+     }
+
+    /**
+     * Finds "All Contacts"-Name for the specified account type.
+     */
+    private static String resolveAllContactsName(Context context, String accountType) {
+        final AccountManager am = AccountManager.get(context);
+
+        for (AuthenticatorDescription auth : am.getAuthenticatorTypes()) {
+            if (accountType.equals(auth.type)) {
+                return resolveAllContactsNameFromMetaData(context, auth.packageName);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Finds the meta-data XML containing the contacts configuration and
+     * reads the picture priority from that file.
+     */
+    private static String resolveAllContactsNameFromMetaData(Context context, String packageName) {
+        final XmlResourceParser parser = ExternalAccountType.loadContactsXml(context, packageName);
+        if (parser != null) {
+            return loadAllContactsNameFromXml(context, parser, packageName);
+        }
+        return null;
+    }
+
+    private static String loadAllContactsNameFromXml(Context context, XmlPullParser parser,
+            String packageName) {
+        try {
+            final AttributeSet attrs = Xml.asAttributeSet(parser);
+            int type;
+            while ((type = parser.next()) != XmlPullParser.START_TAG
+                    && type != XmlPullParser.END_DOCUMENT) {
+                // Drain comments and whitespace
+            }
+
+            if (type != XmlPullParser.START_TAG) {
+                throw new IllegalStateException("No start tag found");
+            }
+
+            final int depth = parser.getDepth();
+            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+                    && type != XmlPullParser.END_DOCUMENT) {
+                String name = parser.getName();
+                if (type == XmlPullParser.START_TAG && CONTACTS_DATA_KIND.equals(name)) {
+                    final TypedArray typedArray = context.obtainStyledAttributes(attrs,
+                            R.styleable.ContactsDataKind);
+                    try {
+                        // See if a string has been hardcoded directly into the xml
+                        final String nonResourceString = typedArray.getNonResourceString(
+                                R.styleable.ContactsDataKind_android_allContactsName);
+                        if (nonResourceString != null) {
+                            return nonResourceString;
+                        }
+
+                        // See if a resource is referenced. We can't rely on getString
+                        // to automatically resolve it as the resource lives in a different package
+                        int id = typedArray.getResourceId(
+                                R.styleable.ContactsDataKind_android_allContactsName, 0);
+                        if (id == 0) return null;
+
+                        // Resolve the resource Id
+                        final PackageManager packageManager = context.getPackageManager();
+                        final Resources resources;
+                        try {
+                            resources = packageManager.getResourcesForApplication(packageName);
+                        } catch (NameNotFoundException e) {
+                            return null;
+                        }
+                        try {
+                            return resources.getString(id);
+                        } catch (NotFoundException e) {
+                            return null;
+                        }
+                    } finally {
+                        typedArray.recycle();
+                    }
+                }
+            }
+            return null;
+        } catch (XmlPullParserException e) {
+            throw new IllegalStateException("Problem reading XML", e);
+        } catch (IOException e) {
+            throw new IllegalStateException("Problem reading XML", e);
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/util/MaterialColorMapUtils.java b/src/com/android/contacts/common/util/MaterialColorMapUtils.java
new file mode 100644
index 0000000..a8fbf42
--- /dev/null
+++ b/src/com/android/contacts/common/util/MaterialColorMapUtils.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2014 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 com.android.contacts.common.R;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Trace;
+
+public class MaterialColorMapUtils {
+    private final TypedArray sPrimaryColors;
+    private final TypedArray sSecondaryColors;
+
+    public MaterialColorMapUtils(Resources resources) {
+        sPrimaryColors = resources.obtainTypedArray(
+                com.android.contacts.common.R.array.letter_tile_colors);
+        sSecondaryColors = resources.obtainTypedArray(
+                com.android.contacts.common.R.array.letter_tile_colors_dark);
+    }
+
+    public static class MaterialPalette implements Parcelable {
+        public MaterialPalette(int primaryColor, int secondaryColor) {
+            mPrimaryColor = primaryColor;
+            mSecondaryColor = secondaryColor;
+        }
+        public final int mPrimaryColor;
+        public final int mSecondaryColor;
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            MaterialPalette other = (MaterialPalette) obj;
+            if (mPrimaryColor != other.mPrimaryColor) {
+                return false;
+            }
+            if (mSecondaryColor != other.mSecondaryColor) {
+                return false;
+            }
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + mPrimaryColor;
+            result = prime * result + mSecondaryColor;
+            return result;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mPrimaryColor);
+            dest.writeInt(mSecondaryColor);
+        }
+
+        private MaterialPalette(Parcel in) {
+            mPrimaryColor = in.readInt();
+            mSecondaryColor = in.readInt();
+        }
+
+        public static final Creator<MaterialPalette> CREATOR = new Creator<MaterialPalette>() {
+                @Override
+                public MaterialPalette createFromParcel(Parcel in) {
+                    return new MaterialPalette(in);
+                }
+
+                @Override
+                public MaterialPalette[] newArray(int size) {
+                    return new MaterialPalette[size];
+                }
+        };
+    }
+
+    /**
+     * Return primary and secondary colors from the Material color palette that are similar to
+     * {@param color}.
+     */
+    public MaterialPalette calculatePrimaryAndSecondaryColor(int color) {
+        Trace.beginSection("calculatePrimaryAndSecondaryColor");
+
+        final float colorHue = hue(color);
+        float minimumDistance = Float.MAX_VALUE;
+        int indexBestMatch = 0;
+        for (int i = 0; i < sPrimaryColors.length(); i++) {
+            final int primaryColor = sPrimaryColors.getColor(i, 0);
+            final float comparedHue = hue(primaryColor);
+            // No need to be perceptually accurate when calculating color distances since
+            // we are only mapping to 15 colors. Being slightly inaccurate isn't going to change
+            // the mapping very often.
+            final float distance = Math.abs(comparedHue - colorHue);
+            if (distance < minimumDistance) {
+                minimumDistance = distance;
+                indexBestMatch = i;
+            }
+        }
+
+        Trace.endSection();
+        return new MaterialPalette(sPrimaryColors.getColor(indexBestMatch, 0),
+                sSecondaryColors.getColor(indexBestMatch, 0));
+    }
+
+    public static MaterialPalette getDefaultPrimaryAndSecondaryColors(Resources resources) {
+        final int primaryColor = resources.getColor(
+                R.color.quickcontact_default_photo_tint_color);
+        final int secondaryColor = resources.getColor(
+                R.color.quickcontact_default_photo_tint_color_dark);
+        return new MaterialPalette(primaryColor, secondaryColor);
+    }
+
+    /**
+     * Returns the hue component of a color int.
+     *
+     * @return A value between 0.0f and 1.0f
+     */
+    public static float hue(int color) {
+        int r = (color >> 16) & 0xFF;
+        int g = (color >> 8) & 0xFF;
+        int b = color & 0xFF;
+
+        int V = Math.max(b, Math.max(r, g));
+        int temp = Math.min(b, Math.min(r, g));
+
+        float H;
+
+        if (V == temp) {
+            H = 0;
+        } else {
+            final float vtemp = V - temp;
+            final float cr = (V - r) / vtemp;
+            final float cg = (V - g) / vtemp;
+            final float cb = (V - b) / vtemp;
+
+            if (r == V) {
+                H = cb - cg;
+            } else if (g == V) {
+                H = 2 + cr - cb;
+            } else {
+                H = 4 + cg - cr;
+            }
+
+            H /= 6.f;
+            if (H < 0) {
+                H++;
+            }
+        }
+
+        return H;
+    }
+}
diff --git a/src/com/android/contacts/common/util/NameConverter.java b/src/com/android/contacts/common/util/NameConverter.java
new file mode 100644
index 0000000..9706353
--- /dev/null
+++ b/src/com/android/contacts/common/util/NameConverter.java
@@ -0,0 +1,240 @@
+/*
+ * 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.util;
+
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
+
+import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Utility class for converting between a display name and structured name (and vice-versa), via
+ * calls to the contact provider.
+ */
+public class NameConverter {
+
+    /**
+     * The array of fields that comprise a structured name.
+     */
+    public static final String[] STRUCTURED_NAME_FIELDS = new String[] {
+            StructuredName.PREFIX,
+            StructuredName.GIVEN_NAME,
+            StructuredName.MIDDLE_NAME,
+            StructuredName.FAMILY_NAME,
+            StructuredName.SUFFIX
+    };
+
+    /**
+     * Converts the given structured name (provided as a map from {@link StructuredName} fields to
+     * corresponding values) into a display name string.
+     * <p>
+     * Note that this operates via a call back to the ContactProvider, but it does not access the
+     * database, so it should be safe to call from the UI thread.  See
+     * ContactsProvider2.completeName() for the underlying method call.
+     * @param context Activity context.
+     * @param structuredName The structured name map to convert.
+     * @return The display name computed from the structured name map.
+     */
+    public static String structuredNameToDisplayName(Context context,
+            Map<String, String> structuredName) {
+        Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+        for (String key : STRUCTURED_NAME_FIELDS) {
+            if (structuredName.containsKey(key)) {
+                appendQueryParameter(builder, key, structuredName.get(key));
+            }
+        }
+        return fetchDisplayName(context, builder.build());
+    }
+
+    /**
+     * Converts the given structured name (provided as ContentValues) into a display name string.
+     * @param context Activity context.
+     * @param values The content values containing values comprising the structured name.
+     * @return
+     */
+    public static String structuredNameToDisplayName(Context context, ContentValues values) {
+        Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+        for (String key : STRUCTURED_NAME_FIELDS) {
+            if (values.containsKey(key)) {
+                appendQueryParameter(builder, key, values.getAsString(key));
+            }
+        }
+        return fetchDisplayName(context, builder.build());
+    }
+
+    /**
+     * Helper method for fetching the display name via the given URI.
+     */
+    private static String fetchDisplayName(Context context, Uri uri) {
+        String displayName = null;
+        Cursor cursor = context.getContentResolver().query(uri, new String[]{
+                StructuredName.DISPLAY_NAME,
+        }, null, null, null);
+
+        if (cursor != null) {
+            try {
+                if (cursor.moveToFirst()) {
+                    displayName = cursor.getString(0);
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+        return displayName;
+    }
+
+    /**
+     * Converts the given display name string into a structured name (as a map from
+     * {@link StructuredName} fields to corresponding values).
+     * <p>
+     * Note that this operates via a call back to the ContactProvider, but it does not access the
+     * database, so it should be safe to call from the UI thread.
+     * @param context Activity context.
+     * @param displayName The display name to convert.
+     * @return The structured name map computed from the display name.
+     */
+    public static Map<String, String> displayNameToStructuredName(Context context,
+            String displayName) {
+        Map<String, String> structuredName = new TreeMap<String, String>();
+        Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+
+        appendQueryParameter(builder, StructuredName.DISPLAY_NAME, displayName);
+        Cursor cursor = context.getContentResolver().query(builder.build(), STRUCTURED_NAME_FIELDS,
+                null, null, null);
+
+        if (cursor != null) {
+            try {
+                if (cursor.moveToFirst()) {
+                    for (int i = 0; i < STRUCTURED_NAME_FIELDS.length; i++) {
+                        structuredName.put(STRUCTURED_NAME_FIELDS[i], cursor.getString(i));
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+        return structuredName;
+    }
+
+    /**
+     * Converts the given display name string into a structured name (inserting the structured
+     * values into a new or existing ContentValues object).
+     * <p>
+     * Note that this operates via a call back to the ContactProvider, but it does not access the
+     * database, so it should be safe to call from the UI thread.
+     * @param context Activity context.
+     * @param displayName The display name to convert.
+     * @param contentValues The content values object to place the structured name values into.  If
+     *     null, a new one will be created and returned.
+     * @return The ContentValues object containing the structured name fields derived from the
+     *     display name.
+     */
+    public static ContentValues displayNameToStructuredName(Context context, String displayName,
+            ContentValues contentValues) {
+        if (contentValues == null) {
+            contentValues = new ContentValues();
+        }
+        Map<String, String> mapValues = displayNameToStructuredName(context, displayName);
+        for (String key : mapValues.keySet()) {
+            contentValues.put(key, mapValues.get(key));
+        }
+        return contentValues;
+    }
+
+    private static void appendQueryParameter(Builder builder, String field, String value) {
+        if (!TextUtils.isEmpty(value)) {
+            builder.appendQueryParameter(field, value);
+        }
+    }
+
+    /**
+     * Parses phonetic name and returns parsed data (family, middle, given) as ContentValues.
+     * Parsed data should be {@link StructuredName#PHONETIC_FAMILY_NAME},
+     * {@link StructuredName#PHONETIC_MIDDLE_NAME}, and
+     * {@link StructuredName#PHONETIC_GIVEN_NAME}.
+     * If this method cannot parse given phoneticName, null values will be stored.
+     *
+     * @param phoneticName Phonetic name to be parsed
+     * @param values ContentValues to be used for storing data. If null, new instance will be
+     * created.
+     * @return ContentValues with parsed data. Those data can be null.
+     */
+    public static StructuredNameDataItem parsePhoneticName(String phoneticName,
+            StructuredNameDataItem item) {
+        String family = null;
+        String middle = null;
+        String given = null;
+
+        if (!TextUtils.isEmpty(phoneticName)) {
+            String[] strings = phoneticName.split(" ", 3);
+            switch (strings.length) {
+                case 1:
+                    family = strings[0];
+                    break;
+                case 2:
+                    family = strings[0];
+                    given = strings[1];
+                    break;
+                case 3:
+                    family = strings[0];
+                    middle = strings[1];
+                    given = strings[2];
+                    break;
+            }
+        }
+
+        if (item == null) {
+            item = new StructuredNameDataItem();
+        }
+        item.setPhoneticFamilyName(family);
+        item.setPhoneticMiddleName(middle);
+        item.setPhoneticGivenName(given);
+        return item;
+    }
+
+    /**
+     * Constructs and returns a phonetic full name from given parts.
+     */
+    public static String buildPhoneticName(String family, String middle, String given) {
+        if (!TextUtils.isEmpty(family) || !TextUtils.isEmpty(middle)
+                || !TextUtils.isEmpty(given)) {
+            StringBuilder sb = new StringBuilder();
+            if (!TextUtils.isEmpty(family)) {
+                sb.append(family.trim()).append(' ');
+            }
+            if (!TextUtils.isEmpty(middle)) {
+                sb.append(middle.trim()).append(' ');
+            }
+            if (!TextUtils.isEmpty(given)) {
+                sb.append(given.trim()).append(' ');
+            }
+            sb.setLength(sb.length() - 1); // Yank the last space
+            return sb.toString();
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/util/PermissionsUtil.java b/src/com/android/contacts/common/util/PermissionsUtil.java
new file mode 100644
index 0000000..37c1762
--- /dev/null
+++ b/src/com/android/contacts/common/util/PermissionsUtil.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.Manifest.permission;
+import android.app.AppOpsManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.Process;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.LocalBroadcastManager;
+
+/**
+ * Utility class to help with runtime permissions.
+ */
+public class PermissionsUtil {
+    // Each permission in this list is a cherry-picked permission from a particular permission
+    // group. Granting a permission group enables access to all permissions in that group so we
+    // only need to check a single permission in each group.
+    // Note: This assumes that the app has correctly requested for all the relevant permissions
+    // in its Manifest file.
+    public static final String PHONE = permission.CALL_PHONE;
+    public static final String CONTACTS = permission.READ_CONTACTS;
+    public static final String LOCATION = permission.ACCESS_FINE_LOCATION;
+
+    public static boolean hasPhonePermissions(Context context) {
+        return hasPermission(context, PHONE);
+    }
+
+    public static boolean hasContactsPermissions(Context context) {
+        return hasPermission(context, CONTACTS);
+    }
+
+    public static boolean hasLocationPermissions(Context context) {
+        return hasPermission(context, LOCATION);
+    }
+
+    public static boolean hasPermission(Context context, String permission) {
+        return ContextCompat.checkSelfPermission(context, permission)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
+    public static boolean hasAppOp(Context context, String appOp) {
+        final AppOpsManager appOpsManager =
+                (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+        final int mode = appOpsManager.checkOpNoThrow(appOp, Process.myUid(),
+                context.getPackageName());
+        return mode == AppOpsManager.MODE_ALLOWED;
+    }
+
+    /**
+     * Rudimentary methods wrapping the use of a LocalBroadcastManager to simplify the process
+     * of notifying other classes when a particular fragment is notified that a permission is
+     * granted.
+     *
+     * To be notified when a permission has been granted, create a new broadcast receiver
+     * and register it using {@link #registerPermissionReceiver(Context, BroadcastReceiver, String)}
+     *
+     * E.g.
+     *
+     * final BroadcastReceiver receiver = new BroadcastReceiver() {
+     *     @Override
+     *     public void onReceive(Context context, Intent intent) {
+     *         refreshContactsView();
+     *     }
+     * }
+     *
+     * PermissionsUtil.registerPermissionReceiver(getActivity(), receiver, READ_CONTACTS);
+     *
+     * If you register to listen for multiple permissions, you can identify which permission was
+     * granted by inspecting {@link Intent#getAction()}.
+     *
+     * In the fragment that requests for the permission, be sure to call
+     * {@link #notifyPermissionGranted(Context, String)} when the permission is granted so that
+     * any interested listeners are notified of the change.
+     */
+    public static void registerPermissionReceiver(Context context, BroadcastReceiver receiver,
+            String permission) {
+        final IntentFilter filter = new IntentFilter(permission);
+        LocalBroadcastManager.getInstance(context).registerReceiver(receiver, filter);
+    }
+
+    public static void unregisterPermissionReceiver(Context context, BroadcastReceiver receiver) {
+        LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);
+    }
+
+    public static void notifyPermissionGranted(Context context, String permission) {
+        final Intent intent = new Intent(permission);
+        LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+    }
+}
diff --git a/src/com/android/contacts/common/util/PhoneNumberFormatter.java b/src/com/android/contacts/common/util/PhoneNumberFormatter.java
new file mode 100644
index 0000000..bbf9785
--- /dev/null
+++ b/src/com/android/contacts/common/util/PhoneNumberFormatter.java
@@ -0,0 +1,89 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.telephony.PhoneNumberFormattingTextWatcher;
+import android.widget.TextView;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.compat.PhoneNumberFormattingTextWatcherCompat;
+
+public final class PhoneNumberFormatter {
+    private PhoneNumberFormatter() {}
+
+    /**
+     * Load {@link TextWatcherLoadAsyncTask} in a worker thread and set it to a {@link TextView}.
+     */
+    private static class TextWatcherLoadAsyncTask extends
+            AsyncTask<Void, Void, PhoneNumberFormattingTextWatcher> {
+        private final String mCountryCode;
+        private final TextView mTextView;
+        private final boolean mFormatAfterWatcherSet;
+
+        public TextWatcherLoadAsyncTask(
+                String countryCode, TextView textView, boolean formatAfterWatcherSet) {
+            mCountryCode = countryCode;
+            mTextView = textView;
+            mFormatAfterWatcherSet = formatAfterWatcherSet;
+        }
+
+        @Override
+        protected PhoneNumberFormattingTextWatcher doInBackground(Void... params) {
+            return PhoneNumberFormattingTextWatcherCompat.newInstance(mCountryCode);
+        }
+
+        @Override
+        protected void onPostExecute(PhoneNumberFormattingTextWatcher watcher) {
+            if (watcher == null || isCancelled()) {
+                return; // May happen if we cancel the task.
+            }
+
+            // Setting a text changed listener is safe even after the view is detached.
+            mTextView.addTextChangedListener(watcher);
+
+            // Forcing formatting the existing phone number
+            if (mFormatAfterWatcherSet) {
+                watcher.afterTextChanged(mTextView.getEditableText());
+            }
+        }
+    }
+
+    /**
+     * Delay-set {@link PhoneNumberFormattingTextWatcher} to a {@link TextView}.
+     */
+    public static final void setPhoneNumberFormattingTextWatcher(Context context,
+            TextView textView) {
+        setPhoneNumberFormattingTextWatcher(context, textView,
+                /* formatAfterWatcherSet =*/ false);
+    }
+
+    /**
+     * Delay-sets {@link PhoneNumberFormattingTextWatcher} to a {@link TextView}
+     * and formats the number immediately if formatAfterWaterSet is true.
+     * In some cases, formatting before user editing might cause unwanted results
+     * (e.g. the editor thinks the user changed the content, and would save
+     * when closed even when the user didn't make other changes.)
+     */
+    public static final void setPhoneNumberFormattingTextWatcher(
+            Context context, TextView textView, boolean formatAfterWatcherSet) {
+        new TextWatcherLoadAsyncTask(GeoUtil.getCurrentCountryIso(context),
+                textView, formatAfterWatcherSet)
+                .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
+    }
+}
diff --git a/src/com/android/contacts/common/util/PhoneNumberHelper.java b/src/com/android/contacts/common/util/PhoneNumberHelper.java
new file mode 100644
index 0000000..794b6dd
--- /dev/null
+++ b/src/com/android/contacts/common/util/PhoneNumberHelper.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
+import com.google.i18n.phonenumbers.ShortNumberInfo;
+
+import java.util.Locale;
+
+/**
+ * This class wraps several PhoneNumberUtil calls and TelephonyManager calls. Some of them are
+ * the same as the ones in the framework's code base. We can remove those once they are part of
+ * the public API.
+ */
+public class PhoneNumberHelper {
+
+    private static final String LOG_TAG = PhoneNumberHelper.class.getSimpleName();
+
+    private static final String KOREA_ISO_COUNTRY_CODE = "KR";
+    /**
+     * Determines if the specified number is actually a URI (i.e. a SIP address) rather than a
+     * regular PSTN phone number, based on whether or not the number contains an "@" character.
+     *
+     * @param number Phone number
+     * @return true if number contains @
+     *
+     * TODO: Remove if PhoneNumberUtils.isUriNumber(String number) is made public.
+     */
+    public static boolean isUriNumber(String number) {
+        // Note we allow either "@" or "%40" to indicate a URI, in case
+        // the passed-in string is URI-escaped.  (Neither "@" nor "%40"
+        // will ever be found in a legal PSTN number.)
+        return number != null && (number.contains("@") || number.contains("%40"));
+    }
+
+    /**
+     * Normalize a phone number by removing the characters other than digits. If
+     * the given number has keypad letters, the letters will be converted to
+     * digits first.
+     *
+     * @param phoneNumber The number to be normalized.
+     * @return The normalized number.
+     *
+     * TODO: Remove if PhoneNumberUtils.normalizeNumber(String phoneNumber) is made public.
+     */
+    public static String normalizeNumber(String phoneNumber) {
+        StringBuilder sb = new StringBuilder();
+        int len = phoneNumber.length();
+        for (int i = 0; i < len; i++) {
+            char c = phoneNumber.charAt(i);
+            // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.)
+            int digit = Character.digit(c, 10);
+            if (digit != -1) {
+                sb.append(digit);
+            } else if (i == 0 && c == '+') {
+                sb.append(c);
+            } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
+                return normalizeNumber(PhoneNumberUtils.convertKeypadLettersToDigits(phoneNumber));
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * @return the "username" part of the specified SIP address, i.e. the part before the "@"
+     * character (or "%40").
+     *
+     * @param number SIP address of the form "username@domainname" (or the URI-escaped equivalent
+     * "username%40domainname")
+     *
+     * TODO: Remove if PhoneNumberUtils.getUsernameFromUriNumber(String number) is made public.
+     */
+    public static String getUsernameFromUriNumber(String number) {
+        // The delimiter between username and domain name can be
+        // either "@" or "%40" (the URI-escaped equivalent.)
+        int delimiterIndex = number.indexOf('@');
+        if (delimiterIndex < 0) {
+            delimiterIndex = number.indexOf("%40");
+        }
+        if (delimiterIndex < 0) {
+            Log.w(LOG_TAG,
+                    "getUsernameFromUriNumber: no delimiter found in SIP addr '" + number + "'");
+            return number;
+        }
+        return number.substring(0, delimiterIndex);
+    }
+}
diff --git a/src/com/android/contacts/common/util/SchedulingUtils.java b/src/com/android/contacts/common/util/SchedulingUtils.java
new file mode 100644
index 0000000..1dfa153
--- /dev/null
+++ b/src/com/android/contacts/common/util/SchedulingUtils.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+
+/** Static methods that are useful for scheduling actions to occur at a later time. */
+public class SchedulingUtils {
+
+
+    /** Runs a piece of code after the next layout run */
+    public static void doAfterLayout(final View view, final Runnable runnable) {
+        final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                // Layout pass done, unregister for further events
+                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                runnable.run();
+            }
+        };
+        view.getViewTreeObserver().addOnGlobalLayoutListener(listener);
+    }
+
+    /** Runs a piece of code just before the next draw, after layout and measurement */
+    public static void doOnPreDraw(final View view, final boolean drawNextFrame,
+            final Runnable runnable) {
+        final OnPreDrawListener listener = new OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                view.getViewTreeObserver().removeOnPreDrawListener(this);
+                runnable.run();
+                return drawNextFrame;
+            }
+        };
+        view.getViewTreeObserver().addOnPreDrawListener(listener);
+    }
+}
diff --git a/src/com/android/contacts/common/util/SearchUtil.java b/src/com/android/contacts/common/util/SearchUtil.java
new file mode 100644
index 0000000..ed41d6c
--- /dev/null
+++ b/src/com/android/contacts/common/util/SearchUtil.java
@@ -0,0 +1,204 @@
+/*
+ * 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 com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Methods related to search.
+ */
+public class SearchUtil {
+
+    public static class MatchedLine {
+
+        public int startIndex = -1;
+        public String line;
+
+        @Override
+        public String toString() {
+            return "MatchedLine{" +
+                    "line='" + line + '\'' +
+                    ", startIndex=" + startIndex +
+                    '}';
+        }
+    }
+
+    /**
+     * Given a string with lines delimited with '\n', finds the matching line to the given
+     * substring.
+     *
+     * @param contents The string to search.
+     * @param substring The substring to search for.
+     * @return A MatchedLine object containing the matching line and the startIndex of the substring
+     * match within that line.
+     */
+    public static MatchedLine findMatchingLine(String contents, String substring) {
+        final MatchedLine matched = new MatchedLine();
+
+        // Snippet may contain multiple lines separated by "\n".
+        // Locate the lines of the content that contain the substring.
+        final int index = SearchUtil.contains(contents, substring);
+        if (index != -1) {
+            // Match found.  Find the corresponding line.
+            int start = index - 1;
+            while (start > -1) {
+                if (contents.charAt(start) == '\n') {
+                    break;
+                }
+                start--;
+            }
+            int end = index + 1;
+            while (end < contents.length()) {
+                if (contents.charAt(end) == '\n') {
+                    break;
+                }
+                end++;
+            }
+            matched.line = contents.substring(start + 1, end);
+            matched.startIndex = index - (start + 1);
+        }
+        return matched;
+    }
+
+    /**
+     * Similar to String.contains() with two main differences:
+     * <p>
+     * 1) Only searches token prefixes.  A token is defined as any combination of letters or
+     * numbers.
+     * <p>
+     * 2) Returns the starting index where the substring is found.
+     *
+     * @param value The string to search.
+     * @param substring The substring to look for.
+     * @return The starting index where the substring is found. {@literal -1} if substring is not
+     *         found in value.
+     */
+    @VisibleForTesting
+    static int contains(String value, String substring) {
+        if (value.length() < substring.length()) {
+            return -1;
+        }
+
+        // i18n support
+        // Generate the code points for the substring once.
+        // There will be a maximum of substring.length code points.  But may be fewer.
+        // Since the array length is not an accurate size, we need to keep a separate variable.
+        final int[] substringCodePoints = new int[substring.length()];
+        int substringLength = 0;  // may not equal substring.length()!!
+        for (int i = 0; i < substring.length(); ) {
+            final int codePoint = Character.codePointAt(substring, i);
+            substringCodePoints[substringLength] = codePoint;
+            substringLength++;
+            i += Character.charCount(codePoint);
+        }
+
+        for (int i = 0; i < value.length(); i = findNextTokenStart(value, i)) {
+            int numMatch = 0;
+            for (int j = i; j < value.length() && numMatch < substringLength; ++numMatch) {
+                int valueCp = Character.toLowerCase(value.codePointAt(j));
+                int substringCp = substringCodePoints[numMatch];
+                if (valueCp != substringCp) {
+                    break;
+                }
+                j += Character.charCount(valueCp);
+            }
+            if (numMatch == substringLength) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Find the start of the next token.  A token is composed of letters and numbers. Any other
+     * character are considered delimiters.
+     *
+     * @param line The string to search for the next token.
+     * @param startIndex The index to start searching.  0 based indexing.
+     * @return The index for the start of the next token.  line.length() if next token not found.
+     */
+    @VisibleForTesting
+    static int findNextTokenStart(String line, int startIndex) {
+        int index = startIndex;
+
+        // If already in token, eat remainder of token.
+        while (index <= line.length()) {
+            if (index == line.length()) {
+                // No more tokens.
+                return index;
+            }
+            final int codePoint = line.codePointAt(index);
+            if (!Character.isLetterOrDigit(codePoint)) {
+                break;
+            }
+            index += Character.charCount(codePoint);
+        }
+
+        // Out of token, eat all consecutive delimiters.
+        while (index <= line.length()) {
+            if (index == line.length()) {
+                return index;
+            }
+            final int codePoint = line.codePointAt(index);
+            if (Character.isLetterOrDigit(codePoint)) {
+                break;
+            }
+            index += Character.charCount(codePoint);
+        }
+
+        return index;
+    }
+
+    /**
+     * Anything other than letter and numbers are considered delimiters.  Remove start and end
+     * delimiters since they are not relevant to search.
+     *
+     * @param query The query string to clean.
+     * @return The cleaned query. Empty string if all characters are cleaned out.
+     */
+    public static String cleanStartAndEndOfSearchQuery(String query) {
+        int start = 0;
+        while (start < query.length()) {
+            int codePoint = query.codePointAt(start);
+            if (Character.isLetterOrDigit(codePoint)) {
+                break;
+            }
+            start += Character.charCount(codePoint);
+        }
+
+        if (start == query.length()) {
+            // All characters are delimiters.
+            return "";
+        }
+
+        int end = query.length() - 1;
+        while (end > -1) {
+            if (Character.isLowSurrogate(query.charAt(end))) {
+                // Assume valid i18n string.  There should be a matching high surrogate before it.
+                end--;
+            }
+            int codePoint = query.codePointAt(end);
+            if (Character.isLetterOrDigit(codePoint)) {
+                break;
+            }
+            end--;
+        }
+
+        // end is a letter or digit.
+        return query.substring(start, end + 1);
+    }
+}
diff --git a/src/com/android/contacts/common/util/StopWatch.java b/src/com/android/contacts/common/util/StopWatch.java
new file mode 100644
index 0000000..581d6ee
--- /dev/null
+++ b/src/com/android/contacts/common/util/StopWatch.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.util.Log;
+
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * A {@link StopWatch} records start, laps and stop, and print them to logcat.
+ */
+public class StopWatch {
+
+    private final String mLabel;
+
+    private final ArrayList<Long> mTimes = Lists.newArrayList();
+    private final ArrayList<String> mLapLabels = Lists.newArrayList();
+
+    private StopWatch(String label) {
+        mLabel = label;
+        lap("");
+    }
+
+    /**
+     * Create a new instance and start it.
+     */
+    public static StopWatch start(String label) {
+        return new StopWatch(label);
+    }
+
+    /**
+     * Record a lap.
+     */
+    public void lap(String lapLabel) {
+        mTimes.add(System.currentTimeMillis());
+        mLapLabels.add(lapLabel);
+    }
+
+    /**
+     * Stop it and log the result, if the total time >= {@code timeThresholdToLog}.
+     */
+    public void stopAndLog(String TAG, int timeThresholdToLog) {
+
+        lap("");
+
+        final long start = mTimes.get(0);
+        final long stop = mTimes.get(mTimes.size() - 1);
+
+        final long total = stop - start;
+        if (total < timeThresholdToLog) return;
+
+        final StringBuilder sb = new StringBuilder();
+        sb.append(mLabel);
+        sb.append(",");
+        sb.append(total);
+        sb.append(": ");
+
+        long last = start;
+        for (int i = 1; i < mTimes.size(); i++) {
+            final long current = mTimes.get(i);
+            sb.append(mLapLabels.get(i));
+            sb.append(",");
+            sb.append((current - last));
+            sb.append(" ");
+            last = current;
+        }
+        Log.v(TAG, sb.toString());
+    }
+
+    /**
+     * Return a dummy instance that does no operations.
+     */
+    public static StopWatch getNullStopWatch() {
+        return NullStopWatch.INSTANCE;
+    }
+
+    private static class NullStopWatch extends StopWatch {
+        public static final NullStopWatch INSTANCE = new NullStopWatch();
+
+        public NullStopWatch() {
+            super(null);
+        }
+
+        @Override
+        public void lap(String lapLabel) {
+            // noop
+        }
+
+        @Override
+        public void stopAndLog(String TAG, int timeThresholdToLog) {
+            // noop
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/util/TelephonyManagerUtils.java b/src/com/android/contacts/common/util/TelephonyManagerUtils.java
new file mode 100644
index 0000000..7c322ca
--- /dev/null
+++ b/src/com/android/contacts/common/util/TelephonyManagerUtils.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Locale;
+
+/**
+ * This class provides several TelephonyManager util functions.
+ */
+public class TelephonyManagerUtils {
+
+    private static final String LOG_TAG = TelephonyManagerUtils.class.getSimpleName();
+
+    /**
+     * Gets the voicemail tag from Telephony Manager.
+     * @param context Current application context
+     * @return Voicemail tag, the alphabetic identifier associated with the voice mail number.
+     */
+    public static String getVoiceMailAlphaTag(Context context) {
+        final TelephonyManager telephonyManager =
+                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        final String voiceMailLabel = telephonyManager.getVoiceMailAlphaTag();
+        return voiceMailLabel;
+    }
+
+    /**
+     * @return The ISO 3166-1 two letters country code of the country the user
+     *         is in based on the network location. If the network location does not exist, fall
+     *         back to the locale setting.
+     */
+    public static String getCurrentCountryIso(Context context, Locale locale) {
+        // Without framework function calls, this seems to be the most accurate location service
+        // we can rely on.
+        final TelephonyManager telephonyManager =
+            (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+        String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
+
+        if (countryIso == null) {
+            countryIso = locale.getCountry();
+            Log.w(LOG_TAG, "No CountryDetector; falling back to countryIso based on locale: "
+                    + countryIso);
+        }
+        return countryIso;
+    }
+
+    /**
+     * @param context Current application context.
+     * @return True if there is a subscription which supports video calls. False otherwise.
+     */
+    public static boolean hasVideoCallSubscription(Context context) {
+        // TODO: Check the telephony manager's subscriptions to see if any support video calls.
+        return true;
+    }
+}
diff --git a/src/com/android/contacts/common/util/TrafficStatsTags.java b/src/com/android/contacts/common/util/TrafficStatsTags.java
new file mode 100644
index 0000000..78faa94
--- /dev/null
+++ b/src/com/android/contacts/common/util/TrafficStatsTags.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2015 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;
+
+public class TrafficStatsTags {
+    public static final int CONTACT_PHOTO_DOWNLOAD_TAG = 0x0001;
+    public static final int TAG_MAX = 0x9999;
+}
diff --git a/src/com/android/contacts/common/util/UriUtils.java b/src/com/android/contacts/common/util/UriUtils.java
new file mode 100644
index 0000000..41ef62f
--- /dev/null
+++ b/src/com/android/contacts/common/util/UriUtils.java
@@ -0,0 +1,94 @@
+/*
+ * 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.util;
+
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import java.util.List;
+
+/**
+ * Utility methods for dealing with URIs.
+ */
+public class UriUtils {
+    /** Static helper, not instantiable. */
+    private UriUtils() {}
+
+    /** Checks whether two URI are equal, taking care of the case where either is null. */
+    public static boolean areEqual(Uri uri1, Uri uri2) {
+        if (uri1 == null && uri2 == null) {
+            return true;
+        }
+        if (uri1 == null || uri2 == null) {
+            return false;
+        }
+        return uri1.equals(uri2);
+    }
+
+    /** Parses a string into a URI and returns null if the given string is null. */
+    public static Uri parseUriOrNull(String uriString) {
+        if (uriString == null) {
+            return null;
+        }
+        return Uri.parse(uriString);
+    }
+
+    /** Converts a URI into a string, returns null if the given URI is null. */
+    public static String uriToString(Uri uri) {
+        return uri == null ? null : uri.toString();
+    }
+
+    public static boolean isEncodedContactUri(Uri uri) {
+        if (uri == null) {
+            return false;
+        }
+        final String lastPathSegment = uri.getLastPathSegment();
+        if (lastPathSegment == null) {
+            return false;
+        }
+        return lastPathSegment.equals(Constants.LOOKUP_URI_ENCODED);
+    }
+
+    /**
+     * @return {@code uri} as-is if the authority is of contacts provider.  Otherwise
+     * or {@code uri} is null, return null otherwise
+     */
+    public static Uri nullForNonContactsUri(Uri uri) {
+        if (uri == null) {
+            return null;
+        }
+        return ContactsContract.AUTHORITY.equals(uri.getAuthority()) ? uri : null;
+    }
+
+    /**
+     * Parses the given URI to determine the original lookup key of the contact.
+     */
+    public static String getLookupKeyFromUri(Uri lookupUri) {
+        // Would be nice to be able to persist the lookup key somehow to avoid having to parse
+        // the uri entirely just to retrieve the lookup key, but every uri is already parsed
+        // once anyway to check if it is an encoded JSON uri, so this has negligible effect
+        // on performance.
+        if (lookupUri != null && !UriUtils.isEncodedContactUri(lookupUri)) {
+            final List<String> segments = lookupUri.getPathSegments();
+            // This returns the third path segment of the uri, where the lookup key is located.
+            // See {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+            return (segments.size() < 3) ? null : Uri.encode(segments.get(2));
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/util/ViewUtil.java b/src/com/android/contacts/common/util/ViewUtil.java
new file mode 100644
index 0000000..895b757
--- /dev/null
+++ b/src/com/android/contacts/common/util/ViewUtil.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.res.Resources;
+import android.graphics.Outline;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.widget.ListView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.CompatUtils;
+
+/**
+ * Provides static functions to work with views
+ */
+public class ViewUtil {
+    private ViewUtil() {}
+
+    /**
+     * Returns the width as specified in the LayoutParams
+     * @throws IllegalStateException Thrown if the view's width is unknown before a layout pass
+     * s
+     */
+    public static int getConstantPreLayoutWidth(View view) {
+        // We haven't been layed out yet, so get the size from the LayoutParams
+        final ViewGroup.LayoutParams p = view.getLayoutParams();
+        if (p.width < 0) {
+            throw new IllegalStateException("Expecting view's width to be a constant rather " +
+                    "than a result of the layout pass");
+        }
+        return p.width;
+    }
+
+    /**
+     * Returns a boolean indicating whether or not the view's layout direction is RTL
+     *
+     * @param view - A valid view
+     * @return True if the view's layout direction is RTL
+     */
+    public static boolean isViewLayoutRtl(View view) {
+        return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+    }
+
+    private static final ViewOutlineProvider OVAL_OUTLINE_PROVIDER;
+    static {
+        if (CompatUtils.isLollipopCompatible()) {
+            OVAL_OUTLINE_PROVIDER = new ViewOutlineProvider() {
+                @Override
+                public void getOutline(View view, Outline outline) {
+                    outline.setOval(0, 0, view.getWidth(), view.getHeight());
+                }
+            };
+        } else {
+            OVAL_OUTLINE_PROVIDER = null;
+        }
+    }
+
+    private static final ViewOutlineProvider RECT_OUTLINE_PROVIDER;
+    static {
+        if (CompatUtils.isLollipopCompatible()) {
+            RECT_OUTLINE_PROVIDER = new ViewOutlineProvider() {
+                @Override
+                public void getOutline(View view, Outline outline) {
+                    outline.setRect(0, 0, view.getWidth(), view.getHeight());
+                }
+            };
+        } else {
+            RECT_OUTLINE_PROVIDER = null;
+        }
+    }
+
+    /**
+     * Adds a rectangular outline to a view. This can be useful when you want to add a shadow
+     * to a transparent view. See b/16856049.
+     * @param view view that the outline is added to
+     * @param res The resources file.
+     */
+    public static void addRectangularOutlineProvider(View view, Resources res) {
+        if (CompatUtils.isLollipopCompatible()) {
+            view.setOutlineProvider(RECT_OUTLINE_PROVIDER);
+        }
+    }
+
+    /**
+     * Configures the floating action button, clipping it to a circle and setting its translation z.
+     * @param view The float action button's view.
+     * @param res The resources file.
+     */
+    public static void setupFloatingActionButton(View view, Resources res) {
+        if (CompatUtils.isLollipopCompatible()) {
+            view.setOutlineProvider(OVAL_OUTLINE_PROVIDER);
+            view.setTranslationZ(
+                    res.getDimensionPixelSize(R.dimen.floating_action_button_translation_z));
+        }
+    }
+
+    /**
+     * Adds padding to the bottom of the given {@link ListView} so that the floating action button
+     * does not obscure any content.
+     *
+     * @param listView to add the padding to
+     * @param res valid resources object
+     */
+    public static void addBottomPaddingToListViewForFab(ListView listView, Resources res) {
+        final int fabPadding = res.getDimensionPixelSize(
+                R.dimen.floating_action_button_list_bottom_padding);
+        listView.setPaddingRelative(listView.getPaddingStart(), listView.getPaddingTop(),
+                listView.getPaddingEnd(), listView.getPaddingBottom() + fabPadding);
+        listView.setClipToPadding(false);
+    }
+}
diff --git a/src/com/android/contacts/common/util/WeakAsyncTask.java b/src/com/android/contacts/common/util/WeakAsyncTask.java
new file mode 100644
index 0000000..f46e514
--- /dev/null
+++ b/src/com/android/contacts/common/util/WeakAsyncTask.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.os.AsyncTask;
+
+import java.lang.ref.WeakReference;
+
+public abstract class WeakAsyncTask<Params, Progress, Result, WeakTarget> extends
+        AsyncTask<Params, Progress, Result> {
+    protected WeakReference<WeakTarget> mTarget;
+
+    public WeakAsyncTask(WeakTarget target) {
+        mTarget = new WeakReference<WeakTarget>(target);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected final void onPreExecute() {
+        final WeakTarget target = mTarget.get();
+        if (target != null) {
+            this.onPreExecute(target);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected final Result doInBackground(Params... params) {
+        final WeakTarget target = mTarget.get();
+        if (target != null) {
+            return this.doInBackground(target, params);
+        } else {
+            return null;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected final void onPostExecute(Result result) {
+        final WeakTarget target = mTarget.get();
+        if (target != null) {
+            this.onPostExecute(target, result);
+        }
+    }
+
+    protected void onPreExecute(WeakTarget target) {
+        // No default action
+    }
+
+    protected abstract Result doInBackground(WeakTarget target, Params... params);
+
+    protected void onPostExecute(WeakTarget target, Result result) {
+        // No default action
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/CancelActivity.java b/src/com/android/contacts/common/vcard/CancelActivity.java
new file mode 100644
index 0000000..8e393e1
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/CancelActivity.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.vcard;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+
+/**
+ * The Activity for canceling vCard import/export.
+ */
+public class CancelActivity extends Activity implements ServiceConnection {
+    private final String LOG_TAG = "VCardCancel";
+
+    /* package */ final static String JOB_ID = "job_id";
+    /* package */ final static String DISPLAY_NAME = "display_name";
+
+    /**
+     * Type of the process to be canceled. Only used for choosing appropriate title/message.
+     * Must be {@link VCardService#TYPE_IMPORT} or {@link VCardService#TYPE_EXPORT}.
+     */
+    /* package */ final static String TYPE = "type";
+
+    private class RequestCancelListener implements DialogInterface.OnClickListener {
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            bindService(new Intent(CancelActivity.this,
+                    VCardService.class), CancelActivity.this, Context.BIND_AUTO_CREATE);
+        }
+    }
+
+    private class CancelListener
+            implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            finish();
+        }
+        @Override
+        public void onCancel(DialogInterface dialog) {
+            finish();
+        }
+    }
+
+    private final CancelListener mCancelListener = new CancelListener();
+    private int mJobId;
+    private String mDisplayName;
+    private int mType;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final Uri uri = getIntent().getData();
+        mJobId = Integer.parseInt(uri.getQueryParameter(JOB_ID));
+        mDisplayName = uri.getQueryParameter(DISPLAY_NAME);
+        mType = Integer.parseInt(uri.getQueryParameter(TYPE));
+        showDialog(R.id.dialog_cancel_confirmation);
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id, Bundle bundle) {
+        if (id == R.id.dialog_cancel_confirmation) {
+            final String message;
+            if (mType == VCardService.TYPE_IMPORT) {
+                message = getString(R.string.cancel_import_confirmation_message, mDisplayName);
+            } else {
+                message = getString(R.string.cancel_export_confirmation_message, mDisplayName);
+            }
+            final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                    .setMessage(message)
+                    .setPositiveButton(android.R.string.ok, new RequestCancelListener())
+                    .setOnCancelListener(mCancelListener)
+                    .setNegativeButton(android.R.string.cancel, mCancelListener);
+            return builder.create();
+        } else if (id == R.id.dialog_cancel_failed) {
+            final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                    .setTitle(R.string.cancel_vcard_import_or_export_failed)
+                    .setIconAttribute(android.R.attr.alertDialogIcon)
+                    .setMessage(getString(R.string.fail_reason_unknown))
+                    .setOnCancelListener(mCancelListener)
+                    .setPositiveButton(android.R.string.ok, mCancelListener);
+            return builder.create();
+        } else {
+            Log.w(LOG_TAG, "Unknown dialog id: " + id);
+            return super.onCreateDialog(id, bundle);
+        }
+    }
+
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder binder) {
+        VCardService service = ((VCardService.MyBinder) binder).getService();
+
+        try {
+            final CancelRequest request = new CancelRequest(mJobId, mDisplayName);
+            service.handleCancelRequest(request, null);
+        } finally {
+            unbindService(this);
+        }
+
+        finish();
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        // do nothing
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/CancelRequest.java b/src/com/android/contacts/common/vcard/CancelRequest.java
new file mode 100644
index 0000000..a5eb4aa
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/CancelRequest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.vcard;
+
+/**
+ * Class representing one request for canceling vCard import/export.
+ */
+public class CancelRequest {
+    public final int jobId;
+    /**
+     * Name used for showing users some useful info. Typically a file name.
+     * Must not be used to do some actual operations.
+     */
+    public final String displayName;
+    public CancelRequest(int jobId, String displayName) {
+        this.jobId = jobId;
+        this.displayName = displayName;
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/ExportProcessor.java b/src/com/android/contacts/common/vcard/ExportProcessor.java
new file mode 100644
index 0000000..2aef379
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ExportProcessor.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.vcard;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.contacts.common.R;
+import com.android.vcard.VCardComposer;
+import com.android.vcard.VCardConfig;
+
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+/**
+ * Class for processing one export request from a user. Dropped after exporting requested Uri(s).
+ * {@link VCardService} will create another object when there is another export request.
+ */
+public class ExportProcessor extends ProcessorBase {
+    private static final String LOG_TAG = "VCardExport";
+    private static final boolean DEBUG = VCardService.DEBUG;
+
+    private final VCardService mService;
+    private final ContentResolver mResolver;
+    private final NotificationManager mNotificationManager;
+    private final ExportRequest mExportRequest;
+    private final int mJobId;
+    private final String mCallingActivity;
+
+    private volatile boolean mCanceled;
+    private volatile boolean mDone;
+
+    private final int SHOW_READY_TOAST = 1;
+    private final Handler handler = new Handler() {
+        public void handleMessage(Message msg) {
+            if (msg.arg1 == SHOW_READY_TOAST) {
+                // This message is long, so we set the duration to LENGTH_LONG.
+                Toast.makeText(mService,
+                        R.string.exporting_vcard_finished_toast, Toast.LENGTH_LONG).show();
+            }
+
+        }
+    };
+
+    public ExportProcessor(VCardService service, ExportRequest exportRequest, int jobId,
+            String callingActivity) {
+        mService = service;
+        mResolver = service.getContentResolver();
+        mNotificationManager =
+                (NotificationManager)mService.getSystemService(Context.NOTIFICATION_SERVICE);
+        mExportRequest = exportRequest;
+        mJobId = jobId;
+        mCallingActivity = callingActivity;
+    }
+
+    @Override
+    public final int getType() {
+        return VCardService.TYPE_EXPORT;
+    }
+
+    @Override
+    public void run() {
+        // ExecutorService ignores RuntimeException, so we need to show it here.
+        try {
+            runInternal();
+
+            if (isCancelled()) {
+                doCancelNotification();
+            }
+        } catch (OutOfMemoryError e) {
+            Log.e(LOG_TAG, "OutOfMemoryError thrown during import", e);
+            throw e;
+        } catch (RuntimeException e) {
+            Log.e(LOG_TAG, "RuntimeException thrown during export", e);
+            throw e;
+        } finally {
+            synchronized (this) {
+                mDone = true;
+            }
+        }
+    }
+
+    private void runInternal() {
+        if (DEBUG) Log.d(LOG_TAG, String.format("vCard export (id: %d) has started.", mJobId));
+        final ExportRequest request = mExportRequest;
+        VCardComposer composer = null;
+        Writer writer = null;
+        boolean successful = false;
+        try {
+            if (isCancelled()) {
+                Log.i(LOG_TAG, "Export request is cancelled before handling the request");
+                return;
+            }
+            final Uri uri = request.destUri;
+            final OutputStream outputStream;
+            try {
+                outputStream = mResolver.openOutputStream(uri);
+            } catch (FileNotFoundException e) {
+                Log.w(LOG_TAG, "FileNotFoundException thrown", e);
+                // Need concise title.
+
+                final String errorReason =
+                    mService.getString(R.string.fail_reason_could_not_open_file,
+                            uri, e.getMessage());
+                doFinishNotification(errorReason, null);
+                return;
+            }
+
+            final String exportType = request.exportType;
+            final int vcardType;
+            if (TextUtils.isEmpty(exportType)) {
+                vcardType = VCardConfig.getVCardTypeFromString(
+                        mService.getString(R.string.config_export_vcard_type));
+            } else {
+                vcardType = VCardConfig.getVCardTypeFromString(exportType);
+            }
+
+            composer = new VCardComposer(mService, vcardType, true);
+
+            // for test
+            // int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
+            //     VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES);
+            // composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);
+
+            writer = new BufferedWriter(new OutputStreamWriter(outputStream));
+            final Uri contentUriForRawContactsEntity = RawContactsEntity.CONTENT_URI;
+            // TODO: should provide better selection.
+            if (!composer.init(Contacts.CONTENT_URI, new String[] {Contacts._ID},
+                    null, null,
+                    null, contentUriForRawContactsEntity)) {
+                final String errorReason = composer.getErrorReason();
+                Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
+                final String translatedErrorReason =
+                        translateComposerError(errorReason);
+                final String title =
+                        mService.getString(R.string.fail_reason_could_not_initialize_exporter,
+                                translatedErrorReason);
+                doFinishNotification(title, null);
+                return;
+            }
+
+            final int total = composer.getCount();
+            if (total == 0) {
+                final String title =
+                        mService.getString(R.string.fail_reason_no_exportable_contact);
+                doFinishNotification(title, null);
+                return;
+            }
+
+            int current = 1;  // 1-origin
+            while (!composer.isAfterLast()) {
+                if (isCancelled()) {
+                    Log.i(LOG_TAG, "Export request is cancelled during composing vCard");
+                    return;
+                }
+                try {
+                    writer.write(composer.createOneEntry());
+                } catch (IOException e) {
+                    final String errorReason = composer.getErrorReason();
+                    Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
+                    final String translatedErrorReason =
+                            translateComposerError(errorReason);
+                    final String title =
+                            mService.getString(R.string.fail_reason_error_occurred_during_export,
+                                    translatedErrorReason);
+                    doFinishNotification(title, null);
+                    return;
+                }
+
+                // vCard export is quite fast (compared to import), and frequent notifications
+                // bother notification bar too much.
+                if (current % 100 == 1) {
+                    doProgressNotification(uri, total, current);
+                }
+                current++;
+            }
+            Log.i(LOG_TAG, "Successfully finished exporting vCard " + request.destUri);
+
+            if (DEBUG) {
+                Log.d(LOG_TAG, "Ask MediaScanner to scan the file: " + request.destUri.getPath());
+            }
+            mService.updateMediaScanner(request.destUri.getPath());
+
+            successful = true;
+            final String filename = ExportVCardActivity.getOpenableUriDisplayName(mService, uri);
+            // If it is a local file (i.e. not a file from Drive), we need to allow user to share
+            // the file by pressing the notification; otherwise, it would be a file in Drive, we
+            // don't need to enable this action in notification since the file is already uploaded.
+            if (isLocalFile(uri)) {
+                final Message msg = handler.obtainMessage();
+                msg.arg1 = SHOW_READY_TOAST;
+                handler.sendMessage(msg);
+                doFinishNotificationWithShareAction(
+                        mService.getString(R.string.exporting_vcard_finished_title_fallback),
+                        mService.getString(R.string.touch_to_share_contacts), uri);
+            } else {
+                final String title = filename == null
+                        ? mService.getString(R.string.exporting_vcard_finished_title_fallback)
+                        : mService.getString(R.string.exporting_vcard_finished_title, filename);
+                doFinishNotification(title, null);
+            }
+        } finally {
+            if (composer != null) {
+                composer.terminate();
+            }
+            if (writer != null) {
+                try {
+                    writer.close();
+                } catch (IOException e) {
+                    Log.w(LOG_TAG, "IOException is thrown during close(). Ignored. " + e);
+                }
+            }
+            mService.handleFinishExportNotification(mJobId, successful);
+        }
+    }
+
+    private boolean isLocalFile(Uri uri) {
+        final String authority = uri.getAuthority();
+        return mService.getString(R.string.contacts_file_provider_authority).equals(authority);
+    }
+
+    private String translateComposerError(String errorMessage) {
+        final Resources resources = mService.getResources();
+        if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
+            return resources.getString(R.string.composer_failed_to_get_database_infomation);
+        } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
+            return resources.getString(R.string.composer_has_no_exportable_contact);
+        } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
+            return resources.getString(R.string.composer_not_initialized);
+        } else {
+            return errorMessage;
+        }
+    }
+
+    private void doProgressNotification(Uri uri, int totalCount, int currentCount) {
+        final String displayName = uri.getLastPathSegment();
+        final String description =
+                mService.getString(R.string.exporting_contact_list_message, displayName);
+        final String tickerText =
+                mService.getString(R.string.exporting_contact_list_title);
+        final Notification notification =
+                NotificationImportExportListener.constructProgressNotification(mService,
+                        VCardService.TYPE_EXPORT, description, tickerText, mJobId, displayName,
+                        totalCount, currentCount);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                mJobId, notification);
+    }
+
+    private void doCancelNotification() {
+        if (DEBUG) Log.d(LOG_TAG, "send cancel notification");
+        final String description = mService.getString(R.string.exporting_vcard_canceled_title,
+                mExportRequest.destUri.getLastPathSegment());
+        final Notification notification =
+                NotificationImportExportListener.constructCancelNotification(mService, description);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                mJobId, notification);
+    }
+
+    private void doFinishNotification(final String title, final String description) {
+        if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
+        final Intent intent = new Intent();
+        intent.setClassName(mService, mCallingActivity);
+        final Notification notification =
+                NotificationImportExportListener.constructFinishNotification(mService, title,
+                        description, intent);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                mJobId, notification);
+    }
+
+    /**
+     * Pass intent with ACTION_SEND to notification so that user can press the notification to
+     * share contacts.
+     */
+    private void doFinishNotificationWithShareAction(final String title, final String
+            description, Uri uri) {
+        if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
+        final Intent intent = new Intent(Intent.ACTION_SEND);
+        intent.setType(Contacts.CONTENT_VCARD_TYPE);
+        intent.putExtra(Intent.EXTRA_STREAM, uri);
+        // Securely grant access using temporary access permissions
+        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        // Build notification
+        final Notification notification =
+                NotificationImportExportListener.constructFinishNotificationWithFlags(
+                        mService, title, description, intent, Intent.FLAG_ACTIVITY_NEW_TASK);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                mJobId, notification);
+    }
+
+    @Override
+    public synchronized boolean cancel(boolean mayInterruptIfRunning) {
+        if (DEBUG) Log.d(LOG_TAG, "received cancel request");
+        if (mDone || mCanceled) {
+            return false;
+        }
+        mCanceled = true;
+        return true;
+    }
+
+    @Override
+    public synchronized boolean isCancelled() {
+        return mCanceled;
+    }
+
+    @Override
+    public synchronized boolean isDone() {
+        return mDone;
+    }
+
+    public ExportRequest getRequest() {
+        return mExportRequest;
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/ExportRequest.java b/src/com/android/contacts/common/vcard/ExportRequest.java
new file mode 100644
index 0000000..e05a32c
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ExportRequest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.vcard;
+
+import android.net.Uri;
+
+public class ExportRequest {
+    public final Uri destUri;
+    /**
+     * Can be null.
+     */
+    public final String exportType;
+
+    public ExportRequest(Uri destUri) {
+        this(destUri, null);
+    }
+
+    public ExportRequest(Uri destUri, String exportType) {
+        this.destUri = destUri;
+        this.exportType = exportType;
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/ExportVCardActivity.java b/src/com/android/contacts/common/vcard/ExportVCardActivity.java
new file mode 100644
index 0000000..18de505
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ExportVCardActivity.java
@@ -0,0 +1,246 @@
+/*
+ * 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.vcard;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.OpenableColumns;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.activity.RequestImportVCardPermissionsActivity;
+
+import java.util.List;
+
+/**
+ * Shows a dialog confirming the export and asks actual vCard export to {@link VCardService}
+ *
+ * This Activity first connects to VCardService and ask an available file name and shows it to
+ * a user. After the user's confirmation, it send export request with the file name, assuming the
+ * file name is not reserved yet.
+ */
+public class ExportVCardActivity extends Activity implements ServiceConnection,
+        DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+    private static final String LOG_TAG = "VCardExport";
+    protected static final boolean DEBUG = VCardService.DEBUG;
+    private static final int REQUEST_CREATE_DOCUMENT = 100;
+
+    /**
+     * True when this Activity is connected to {@link VCardService}.
+     *
+     * Should be touched inside synchronized block.
+     */
+    protected boolean mConnected;
+
+    /**
+     * True when users need to do something and this Activity should not disconnect from
+     * VCardService. False when all necessary procedures are done (including sending export request)
+     * or there's some error occured.
+     */
+    private volatile boolean mProcessOngoing = true;
+
+    protected VCardService mService;
+    private static final BidiFormatter mBidiFormatter = BidiFormatter.getInstance();
+
+    // String for storing error reason temporarily.
+    private String mErrorReason;
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        if (RequestImportVCardPermissionsActivity.startPermissionActivity(this)) {
+            return;
+        }
+
+        if (!hasExportIntentHandler()) {
+            Log.e(LOG_TAG, "Couldn't find export intent handler");
+            showErrorDialog();
+            return;
+        }
+
+        connectVCardService();
+    }
+
+    private void connectVCardService() {
+        final String callingActivity = getIntent().getExtras()
+                .getString(VCardCommonArguments.ARG_CALLING_ACTIVITY);
+        Intent intent = new Intent(this, VCardService.class);
+        intent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY, callingActivity);
+
+        if (startService(intent) == null) {
+            Log.e(LOG_TAG, "Failed to start vCard service");
+            showErrorDialog();
+            return;
+        }
+
+        if (!bindService(intent, this, Context.BIND_AUTO_CREATE)) {
+            Log.e(LOG_TAG, "Failed to connect to vCard service.");
+            showErrorDialog();
+        }
+        // Continued to onServiceConnected()
+    }
+
+    private boolean hasExportIntentHandler() {
+        final Intent intent = getCreateDocIntent();
+        final List<ResolveInfo> receivers = getPackageManager().queryIntentActivities(intent,
+                PackageManager.MATCH_DEFAULT_ONLY);
+        return receivers != null && receivers.size() > 0;
+    }
+
+    private Intent getCreateDocIntent() {
+        final Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        intent.setType(VCardService.X_VCARD_MIME_TYPE);
+        intent.putExtra(Intent.EXTRA_TITLE, mBidiFormatter.unicodeWrap(
+                getString(R.string.exporting_vcard_filename), TextDirectionHeuristics.LTR));
+        return intent;
+    }
+
+    private void showErrorDialog() {
+        mErrorReason = getString(R.string.fail_reason_unknown);
+        showDialog(R.id.dialog_fail_to_export_with_reason);
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CREATE_DOCUMENT) {
+            if (resultCode == Activity.RESULT_OK && mService != null &&
+                    data != null && data.getData() != null) {
+                final Uri targetFileName = data.getData();
+                if (DEBUG) Log.d(LOG_TAG, "exporting to " + targetFileName);
+                final ExportRequest request = new ExportRequest(targetFileName);
+                // The connection object will call finish().
+                mService.handleExportRequest(request, new NotificationImportExportListener(
+                        ExportVCardActivity.this));
+            } else if (DEBUG) {
+                if (mService == null) {
+                    Log.d(LOG_TAG, "No vCard service.");
+                } else {
+                    Log.d(LOG_TAG, "create document cancelled or no data returned");
+                }
+            }
+            finish();
+        }
+    }
+
+    @Override
+    public synchronized void onServiceConnected(ComponentName name, IBinder binder) {
+        if (DEBUG) Log.d(LOG_TAG, "connected to service, requesting a destination file name");
+        mConnected = true;
+        mService = ((VCardService.MyBinder) binder).getService();
+
+        // Have the user choose where vcards will be exported to
+        startActivityForResult(getCreateDocIntent(), REQUEST_CREATE_DOCUMENT);
+    }
+
+    // Use synchronized since we don't want to call finish() just after this call.
+    @Override
+    public synchronized void onServiceDisconnected(ComponentName name) {
+        if (DEBUG) Log.d(LOG_TAG, "onServiceDisconnected()");
+        mService = null;
+        mConnected = false;
+        if (mProcessOngoing) {
+            // Unexpected disconnect event.
+            Log.w(LOG_TAG, "Disconnected from service during the process ongoing.");
+            showErrorDialog();
+        }
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id, Bundle bundle) {
+        if (id == R.id.dialog_fail_to_export_with_reason) {
+            mProcessOngoing = false;
+            return new AlertDialog.Builder(this)
+                    .setTitle(R.string.exporting_contact_failed_title)
+                    .setMessage(getString(R.string.exporting_contact_failed_message,
+                            mErrorReason != null ? mErrorReason :
+                                    getString(R.string.fail_reason_unknown)))
+                    .setPositiveButton(android.R.string.ok, this)
+                    .setOnCancelListener(this)
+                    .create();
+        }
+        return super.onCreateDialog(id, bundle);
+    }
+
+    @Override
+    protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
+        if (id == R.id.dialog_fail_to_export_with_reason) {
+            ((AlertDialog)dialog).setMessage(mErrorReason);
+        } else {
+            super.onPrepareDialog(id, dialog, args);
+        }
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        if (DEBUG) Log.d(LOG_TAG, "ExportVCardActivity#onClick() is called");
+        finish();
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        if (DEBUG) Log.d(LOG_TAG, "ExportVCardActivity#onCancel() is called");
+        mProcessOngoing = false;
+        finish();
+    }
+
+    @Override
+    public void unbindService(ServiceConnection conn) {
+        mProcessOngoing = false;
+        super.unbindService(conn);
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mConnected) {
+            unbindService(this);
+            mConnected = false;
+        }
+        super.onDestroy();
+    }
+
+    /**
+     * Returns the display name for the given openable Uri or null if it could not be resolved. */
+    static String getOpenableUriDisplayName(Context context, Uri uri) {
+        if (uri == null) return null;
+        final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
+        try {
+            if (cursor != null && cursor.moveToFirst()) {
+                return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
+            }
+        } finally {
+            if (cursor != null)  {
+                cursor.close();
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/ImportProcessor.java b/src/com/android/contacts/common/vcard/ImportProcessor.java
new file mode 100644
index 0000000..219ec14
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ImportProcessor.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.vcard;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.util.Log;
+
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntryCommitter;
+import com.android.vcard.VCardEntryConstructor;
+import com.android.vcard.VCardEntryHandler;
+import com.android.vcard.VCardInterpreter;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardNotSupportedException;
+import com.android.vcard.exception.VCardVersionException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for processing one import request from a user. Dropped after importing requested Uri(s).
+ * {@link VCardService} will create another object when there is another import request.
+ */
+public class ImportProcessor extends ProcessorBase implements VCardEntryHandler {
+    private static final String LOG_TAG = "VCardImport";
+    private static final boolean DEBUG = VCardService.DEBUG;
+
+    private final VCardService mService;
+    private final ContentResolver mResolver;
+    private final ImportRequest mImportRequest;
+    private final int mJobId;
+    private final VCardImportExportListener mListener;
+
+    // TODO: remove and show appropriate message instead.
+    private final List<Uri> mFailedUris = new ArrayList<Uri>();
+
+    private VCardParser mVCardParser;
+
+    private volatile boolean mCanceled;
+    private volatile boolean mDone;
+
+    private int mCurrentCount = 0;
+    private int mTotalCount = 0;
+
+    public ImportProcessor(final VCardService service, final VCardImportExportListener listener,
+            final ImportRequest request, final int jobId) {
+        mService = service;
+        mResolver = mService.getContentResolver();
+        mListener = listener;
+
+        mImportRequest = request;
+        mJobId = jobId;
+    }
+
+    @Override
+    public void onStart() {
+        // do nothing
+    }
+
+    @Override
+    public void onEnd() {
+        // do nothing
+    }
+
+    @Override
+    public void onEntryCreated(VCardEntry entry) {
+        mCurrentCount++;
+        if (mListener != null) {
+            mListener.onImportParsed(mImportRequest, mJobId, entry, mCurrentCount, mTotalCount);
+        }
+    }
+
+    @Override
+    public final int getType() {
+        return VCardService.TYPE_IMPORT;
+    }
+
+    @Override
+    public void run() {
+        // ExecutorService ignores RuntimeException, so we need to show it here.
+        try {
+            runInternal();
+
+            if (isCancelled() && mListener != null) {
+                mListener.onImportCanceled(mImportRequest, mJobId);
+            }
+        } catch (OutOfMemoryError e) {
+            Log.e(LOG_TAG, "OutOfMemoryError thrown during import", e);
+            throw e;
+        } catch (RuntimeException e) {
+            Log.e(LOG_TAG, "RuntimeException thrown during import", e);
+            throw e;
+        } finally {
+            synchronized (this) {
+                mDone = true;
+            }
+        }
+    }
+
+    private void runInternal() {
+        Log.i(LOG_TAG, String.format("vCard import (id: %d) has started.", mJobId));
+        final ImportRequest request = mImportRequest;
+        if (isCancelled()) {
+            Log.i(LOG_TAG, "Canceled before actually handling parameter (" + request.uri + ")");
+            return;
+        }
+        final int[] possibleVCardVersions;
+        if (request.vcardVersion == ImportVCardActivity.VCARD_VERSION_AUTO_DETECT) {
+            /**
+             * Note: this code assumes that a given Uri is able to be opened more than once,
+             * which may not be true in certain conditions.
+             */
+            possibleVCardVersions = new int[] {
+                    ImportVCardActivity.VCARD_VERSION_V21,
+                    ImportVCardActivity.VCARD_VERSION_V30
+            };
+        } else {
+            possibleVCardVersions = new int[] {
+                    request.vcardVersion
+            };
+        }
+
+        final Uri uri = request.uri;
+        final Account account = request.account;
+        final int estimatedVCardType = request.estimatedVCardType;
+        final String estimatedCharset = request.estimatedCharset;
+        final int entryCount = request.entryCount;
+        mTotalCount += entryCount;
+
+        final VCardEntryConstructor constructor =
+                new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
+        final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
+        constructor.addEntryHandler(committer);
+        constructor.addEntryHandler(this);
+
+        InputStream is = null;
+        boolean successful = false;
+        try {
+            if (uri != null) {
+                Log.i(LOG_TAG, "start importing one vCard (Uri: " + uri + ")");
+                is = mResolver.openInputStream(uri);
+            } else if (request.data != null){
+                Log.i(LOG_TAG, "start importing one vCard (byte[])");
+                is = new ByteArrayInputStream(request.data);
+            }
+
+            if (is != null) {
+                successful = readOneVCard(is, estimatedVCardType, estimatedCharset, constructor,
+                        possibleVCardVersions);
+            }
+        } catch (IOException e) {
+            successful = false;
+        } finally {
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (Exception e) {
+                    // ignore
+                }
+            }
+        }
+
+        mService.handleFinishImportNotification(mJobId, successful);
+
+        if (successful) {
+            // TODO: successful becomes true even when cancelled. Should return more appropriate
+            // value
+            if (isCancelled()) {
+                Log.i(LOG_TAG, "vCard import has been canceled (uri: " + uri + ")");
+                // Cancel notification will be done outside this method.
+            } else {
+                Log.i(LOG_TAG, "Successfully finished importing one vCard file: " + uri);
+                List<Uri> uris = committer.getCreatedUris();
+                if (mListener != null) {
+                    if (uris != null && uris.size() == 1) {
+                        mListener.onImportFinished(mImportRequest, mJobId, uris.get(0));
+                    } else {
+                        if (uris == null || uris.size() == 0) {
+                            // Not critical, but suspicious.
+                            Log.w(LOG_TAG,  "Created Uris is null or 0 length " +
+                                    "though the creation itself is successful.");
+                        }
+                        mListener.onImportFinished(mImportRequest, mJobId, null);
+                    }
+                }
+            }
+        } else {
+            Log.w(LOG_TAG, "Failed to read one vCard file: " + uri);
+            mFailedUris.add(uri);
+        }
+    }
+
+    private boolean readOneVCard(InputStream is, int vcardType, String charset,
+            final VCardInterpreter interpreter,
+            final int[] possibleVCardVersions) {
+        boolean successful = false;
+        final int length = possibleVCardVersions.length;
+        for (int i = 0; i < length; i++) {
+            final int vcardVersion = possibleVCardVersions[i];
+            try {
+                if (i > 0 && (interpreter instanceof VCardEntryConstructor)) {
+                    // Let the object clean up internal temporary objects,
+                    ((VCardEntryConstructor) interpreter).clear();
+                }
+
+                // We need synchronized block here,
+                // since we need to handle mCanceled and mVCardParser at once.
+                // In the worst case, a user may call cancel() just before creating
+                // mVCardParser.
+                synchronized (this) {
+                    mVCardParser = (vcardVersion == ImportVCardActivity.VCARD_VERSION_V30 ?
+                            new VCardParser_V30(vcardType) :
+                                new VCardParser_V21(vcardType));
+                    if (isCancelled()) {
+                        Log.i(LOG_TAG, "ImportProcessor already recieves cancel request, so " +
+                                "send cancel request to vCard parser too.");
+                        mVCardParser.cancel();
+                    }
+                }
+                mVCardParser.parse(is, interpreter);
+
+                successful = true;
+                break;
+            } catch (IOException e) {
+                Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
+            } catch (VCardNestedException e) {
+                // This exception should not be thrown here. We should instead handle it
+                // in the preprocessing session in ImportVCardActivity, as we don't try
+                // to detect the type of given vCard here.
+                //
+                // TODO: Handle this case appropriately, which should mean we have to have
+                // code trying to auto-detect the type of given vCard twice (both in
+                // ImportVCardActivity and ImportVCardService).
+                Log.e(LOG_TAG, "Nested Exception is found.");
+            } catch (VCardNotSupportedException e) {
+                Log.e(LOG_TAG, e.toString());
+            } catch (VCardVersionException e) {
+                if (i == length - 1) {
+                    Log.e(LOG_TAG, "Appropriate version for this vCard is not found.");
+                } else {
+                    // We'll try the other (v30) version.
+                }
+            } catch (VCardException e) {
+                Log.e(LOG_TAG, e.toString());
+            } finally {
+                if (is != null) {
+                    try {
+                        is.close();
+                    } catch (IOException e) {
+                    }
+                }
+            }
+        }
+
+        return successful;
+    }
+
+    @Override
+    public synchronized boolean cancel(boolean mayInterruptIfRunning) {
+        if (DEBUG) Log.d(LOG_TAG, "ImportProcessor received cancel request");
+        if (mDone || mCanceled) {
+            return false;
+        }
+        mCanceled = true;
+        synchronized (this) {
+            if (mVCardParser != null) {
+                mVCardParser.cancel();
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public synchronized boolean isCancelled() {
+        return mCanceled;
+    }
+
+
+    @Override
+    public synchronized boolean isDone() {
+        return mDone;
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/ImportRequest.java b/src/com/android/contacts/common/vcard/ImportRequest.java
new file mode 100644
index 0000000..32efb99
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ImportRequest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.vcard;
+
+import android.accounts.Account;
+import android.net.Uri;
+
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.vcard.VCardSourceDetector;
+
+/**
+ * Class representing one request for importing vCard (given as a Uri).
+ *
+ * Mainly used when {@link ImportVCardActivity} requests {@link VCardService}
+ * to import some specific Uri.
+ *
+ * Note: This object's accepting only One Uri does NOT mean that
+ * there's only one vCard entry inside the instance, as one Uri often has multiple
+ * vCard entries inside it.
+ */
+public class ImportRequest {
+    /**
+     * Can be null (typically when there's no Account available in the system).
+     */
+    public final Account account;
+
+    /**
+     * Uri to be imported. May have different content than originally given from users, so
+     * when displaying user-friendly information (e.g. "importing xxx.vcf"), use
+     * {@link #displayName} instead.
+     *
+     * If this is null {@link #data} contains the byte stream of the vcard.
+     */
+    public final Uri uri;
+
+    /**
+     * Holds the byte stream of the vcard, if {@link #uri} is null.
+     */
+    public final byte[] data;
+
+    /**
+     * String to be displayed to the user to indicate the source of the VCARD.
+     */
+    public final String displayName;
+
+    /**
+     * Can be {@link VCardSourceDetector#PARSE_TYPE_UNKNOWN}.
+     */
+    public final int estimatedVCardType;
+
+    /**
+     * Can be null, meaning no preferable charset is available.
+     */
+    public final String estimatedCharset;
+
+    /**
+     * Assumes that one Uri contains only one version, while there's a (tiny) possibility
+     * we may have two types in one vCard.
+     *
+     * e.g.
+     * BEGIN:VCARD
+     * VERSION:2.1
+     * ...
+     * END:VCARD
+     * BEGIN:VCARD
+     * VERSION:3.0
+     * ...
+     * END:VCARD
+     *
+     * We've never seen this kind of a file, but we may have to cope with it in the future.
+     */
+    public final int vcardVersion;
+
+    /**
+     * The count of vCard entries in {@link #uri}. A receiver of this object can use it
+     * when showing the progress of import. Thus a receiver must be able to torelate this
+     * variable being invalid because of vCard's limitation.
+     *
+     * vCard does not let us know this count without looking over a whole file content,
+     * which means we have to open and scan over {@link #uri} to know this value, while
+     * it may not be opened more than once (Uri does not require it to be opened multiple times
+     * and may become invalid after its close() request).
+     */
+    public final int entryCount;
+
+    public ImportRequest(AccountWithDataSet account,
+            byte[] data, Uri uri, String displayName, int estimatedType, String estimatedCharset,
+            int vcardVersion, int entryCount) {
+        this.account = account != null ? account.getAccountOrNull() : null;
+        this.data = data;
+        this.uri = uri;
+        this.displayName = displayName;
+        this.estimatedVCardType = estimatedType;
+        this.estimatedCharset = estimatedCharset;
+        this.vcardVersion = vcardVersion;
+        this.entryCount = entryCount;
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/ImportVCardActivity.java b/src/com/android/contacts/common/vcard/ImportVCardActivity.java
new file mode 100644
index 0000000..9da8c0b
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ImportVCardActivity.java
@@ -0,0 +1,769 @@
+/*
+ * 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.vcard;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.ProgressDialog;
+import android.content.ClipData;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.provider.OpenableColumns;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.activity.RequestImportVCardPermissionsActivity;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.vcard.VCardEntryCounter;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+import com.android.vcard.VCardSourceDetector;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardVersionException;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The class letting users to import vCard. This includes the UI part for letting them select
+ * an Account and posssibly a file if there's no Uri is given from its caller Activity.
+ *
+ * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
+ * finished (with the method {@link Activity#finish()}) after the import and never reuse
+ * any Dialog in the instance. So this code is careless about the management around managed
+ * dialogs stuffs (like how onCreateDialog() is used).
+ */
+public class ImportVCardActivity extends Activity {
+    private static final String LOG_TAG = "VCardImport";
+
+    private static final int SELECT_ACCOUNT = 0;
+
+    /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0;
+    /* package */ final static int VCARD_VERSION_V21 = 1;
+    /* package */ final static int VCARD_VERSION_V30 = 2;
+
+    private static final int REQUEST_OPEN_DOCUMENT = 100;
+
+    /**
+     * Notification id used when error happened before sending an import request to VCardServer.
+     */
+    private static final int FAILURE_NOTIFICATION_ID = 1;
+
+    private static final String LOCAL_TMP_FILE_NAME_EXTRA =
+            "com.android.contacts.common.vcard.LOCAL_TMP_FILE_NAME";
+
+    private static final String SOURCE_URI_DISPLAY_NAME =
+            "com.android.contacts.common.vcard.SOURCE_URI_DISPLAY_NAME";
+
+    private static final String STORAGE_VCARD_URI_PREFIX = "file:///storage";
+
+    private AccountWithDataSet mAccount;
+
+    private ProgressDialog mProgressDialogForCachingVCard;
+
+    private VCardCacheThread mVCardCacheThread;
+    private ImportRequestConnection mConnection;
+    /* package */ VCardImportExportListener mListener;
+
+    private String mErrorMessage;
+
+    private Handler mHandler = new Handler();
+
+    // Runs on the UI thread.
+    private class DialogDisplayer implements Runnable {
+        private final int mResId;
+        public DialogDisplayer(int resId) {
+            mResId = resId;
+        }
+        public DialogDisplayer(String errorMessage) {
+            mResId = R.id.dialog_error_with_message;
+            mErrorMessage = errorMessage;
+        }
+        @Override
+        public void run() {
+            if (!isFinishing()) {
+                showDialog(mResId);
+            }
+        }
+    }
+
+    private class CancelListener
+        implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            finish();
+        }
+        @Override
+        public void onCancel(DialogInterface dialog) {
+            finish();
+        }
+    }
+
+    private CancelListener mCancelListener = new CancelListener();
+
+    private class ImportRequestConnection implements ServiceConnection {
+        private VCardService mService;
+
+        public void sendImportRequest(final List<ImportRequest> requests) {
+            Log.i(LOG_TAG, "Send an import request");
+            mService.handleImportRequest(requests, mListener);
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder binder) {
+            mService = ((VCardService.MyBinder) binder).getService();
+            Log.i(LOG_TAG,
+                    String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
+                            Arrays.toString(mVCardCacheThread.getSourceUris())));
+            mVCardCacheThread.start();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.i(LOG_TAG, "Disconnected from VCardService");
+        }
+    }
+
+    /**
+     * Caches given vCard files into a local directory, and sends actual import request to
+     * {@link VCardService}.
+     *
+     * We need to cache given files into local storage. One of reasons is that some data (as Uri)
+     * may have special permissions. Callers may allow only this Activity to access that content,
+     * not what this Activity launched (like {@link VCardService}).
+     */
+    private class VCardCacheThread extends Thread
+            implements DialogInterface.OnCancelListener {
+        private boolean mCanceled;
+        private PowerManager.WakeLock mWakeLock;
+        private VCardParser mVCardParser;
+        private final Uri[] mSourceUris;  // Given from a caller.
+        private final String[] mSourceDisplayNames; // Display names for each Uri in mSourceUris.
+        private final byte[] mSource;
+        private final String mDisplayName;
+
+        public VCardCacheThread(final Uri[] sourceUris, String[] sourceDisplayNames) {
+            mSourceUris = sourceUris;
+            mSourceDisplayNames = sourceDisplayNames;
+            mSource = null;
+            final Context context = ImportVCardActivity.this;
+            final PowerManager powerManager =
+                    (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+            mWakeLock = powerManager.newWakeLock(
+                    PowerManager.SCREEN_DIM_WAKE_LOCK |
+                    PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+            mDisplayName = null;
+        }
+
+        @Override
+        public void finalize() {
+            if (mWakeLock != null && mWakeLock.isHeld()) {
+                Log.w(LOG_TAG, "WakeLock is being held.");
+                mWakeLock.release();
+            }
+        }
+
+        @Override
+        public void run() {
+            Log.i(LOG_TAG, "vCard cache thread starts running.");
+            if (mConnection == null) {
+                throw new NullPointerException("vCard cache thread must be launched "
+                        + "after a service connection is established");
+            }
+
+            mWakeLock.acquire();
+            try {
+                if (mCanceled == true) {
+                    Log.i(LOG_TAG, "vCard cache operation is canceled.");
+                    return;
+                }
+
+                final Context context = ImportVCardActivity.this;
+                // Uris given from caller applications may not be opened twice: consider when
+                // it is not from local storage (e.g. "file:///...") but from some special
+                // provider (e.g. "content://...").
+                // Thus we have to once copy the content of Uri into local storage, and read
+                // it after it.
+                //
+                // We may be able to read content of each vCard file during copying them
+                // to local storage, but currently vCard code does not allow us to do so.
+                int cache_index = 0;
+                ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
+                if (mSource != null) {
+                    try {
+                        requests.add(constructImportRequest(mSource, null, mDisplayName));
+                    } catch (VCardException e) {
+                        Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
+                        showFailureNotification(R.string.fail_reason_not_supported);
+                        return;
+                    }
+                } else {
+                    int i = 0;
+                    for (Uri sourceUri : mSourceUris) {
+                        if (mCanceled) {
+                            Log.i(LOG_TAG, "vCard cache operation is canceled.");
+                            break;
+                        }
+
+                        String sourceDisplayName = mSourceDisplayNames[i++];
+
+                        final ImportRequest request;
+                        try {
+                            request = constructImportRequest(null, sourceUri, sourceDisplayName);
+                        } catch (VCardException e) {
+                            Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
+                            showFailureNotification(R.string.fail_reason_not_supported);
+                            return;
+                        } catch (IOException e) {
+                            Log.e(LOG_TAG, "Unexpected IOException", e);
+                            showFailureNotification(R.string.fail_reason_io_error);
+                            return;
+                        }
+                        if (mCanceled) {
+                            Log.i(LOG_TAG, "vCard cache operation is canceled.");
+                            return;
+                        }
+                        requests.add(request);
+                    }
+                }
+                if (!requests.isEmpty()) {
+                    mConnection.sendImportRequest(requests);
+                } else {
+                    Log.w(LOG_TAG, "Empty import requests. Ignore it.");
+                }
+            } catch (OutOfMemoryError e) {
+                Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard");
+                System.gc();
+                runOnUiThread(new DialogDisplayer(
+                        getString(R.string.fail_reason_low_memory_during_import)));
+            } catch (IOException e) {
+                Log.e(LOG_TAG, "IOException during caching vCard", e);
+                runOnUiThread(new DialogDisplayer(
+                        getString(R.string.fail_reason_io_error)));
+            } finally {
+                Log.i(LOG_TAG, "Finished caching vCard.");
+                mWakeLock.release();
+                unbindService(mConnection);
+                mProgressDialogForCachingVCard.dismiss();
+                mProgressDialogForCachingVCard = null;
+                finish();
+            }
+        }
+
+        /**
+         * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
+         * its content.
+         *
+         * @arg localDataUri Uri actually used for the import. Should be stored in
+         * app local storage, as we cannot guarantee other types of Uris can be read
+         * multiple times. This variable populates {@link ImportRequest#uri}.
+         * @arg displayName Used for displaying information to the user. This variable populates
+         * {@link ImportRequest#displayName}.
+         */
+        private ImportRequest constructImportRequest(final byte[] data,
+                final Uri localDataUri, final String displayName)
+                throws IOException, VCardException {
+            final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
+            VCardEntryCounter counter = null;
+            VCardSourceDetector detector = null;
+            int vcardVersion = VCARD_VERSION_V21;
+            try {
+                boolean shouldUseV30 = false;
+                InputStream is;
+                if (data != null) {
+                    is = new ByteArrayInputStream(data);
+                } else {
+                    is = resolver.openInputStream(localDataUri);
+                }
+                mVCardParser = new VCardParser_V21();
+                try {
+                    counter = new VCardEntryCounter();
+                    detector = new VCardSourceDetector();
+                    mVCardParser.addInterpreter(counter);
+                    mVCardParser.addInterpreter(detector);
+                    mVCardParser.parse(is);
+                } catch (VCardVersionException e1) {
+                    try {
+                        is.close();
+                    } catch (IOException e) {
+                    }
+
+                    shouldUseV30 = true;
+                    if (data != null) {
+                        is = new ByteArrayInputStream(data);
+                    } else {
+                        is = resolver.openInputStream(localDataUri);
+                    }
+                    mVCardParser = new VCardParser_V30();
+                    try {
+                        counter = new VCardEntryCounter();
+                        detector = new VCardSourceDetector();
+                        mVCardParser.addInterpreter(counter);
+                        mVCardParser.addInterpreter(detector);
+                        mVCardParser.parse(is);
+                    } catch (VCardVersionException e2) {
+                        throw new VCardException("vCard with unspported version.");
+                    }
+                } finally {
+                    if (is != null) {
+                        try {
+                            is.close();
+                        } catch (IOException e) {
+                        }
+                    }
+                }
+
+                vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
+            } catch (VCardNestedException e) {
+                Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
+                // Go through without throwing the Exception, as we may be able to detect the
+                // version before it
+            }
+            return new ImportRequest(mAccount,
+                    data, localDataUri, displayName,
+                    detector.getEstimatedType(),
+                    detector.getEstimatedCharset(),
+                    vcardVersion, counter.getCount());
+        }
+
+        public Uri[] getSourceUris() {
+            return mSourceUris;
+        }
+
+        public void cancel() {
+            mCanceled = true;
+            if (mVCardParser != null) {
+                mVCardParser.cancel();
+            }
+        }
+
+        @Override
+        public void onCancel(DialogInterface dialog) {
+            Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
+            cancel();
+        }
+    }
+
+    private void importVCard(final Uri uri, final String sourceDisplayName) {
+        importVCard(new Uri[] {uri}, new String[] {sourceDisplayName});
+    }
+
+    private void importVCard(final Uri[] uris, final String[] sourceDisplayNames) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (!isFinishing()) {
+                    mVCardCacheThread = new VCardCacheThread(uris, sourceDisplayNames);
+                    mListener = new NotificationImportExportListener(ImportVCardActivity.this);
+                    showDialog(R.id.dialog_cache_vcard);
+                }
+            }
+        });
+    }
+
+    private String getDisplayName(Uri sourceUri) {
+        if (sourceUri == null) {
+            return null;
+        }
+        final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
+        String displayName = null;
+        Cursor cursor = null;
+        // Try to get a display name from the given Uri. If it fails, we just
+        // pick up the last part of the Uri.
+        try {
+            cursor = resolver.query(sourceUri,
+                    new String[] { OpenableColumns.DISPLAY_NAME },
+                    null, null, null);
+            if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
+                if (cursor.getCount() > 1) {
+                    Log.w(LOG_TAG, "Unexpected multiple rows: "
+                            + cursor.getCount());
+                }
+                int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+                if (index >= 0) {
+                    displayName = cursor.getString(index);
+                }
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        if (TextUtils.isEmpty(displayName)){
+            displayName = sourceUri.getLastPathSegment();
+        }
+        return displayName;
+    }
+
+    /**
+     * Copy the content of sourceUri to the destination.
+     */
+    private Uri copyTo(final Uri sourceUri, String filename) throws IOException {
+        Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)",
+                sourceUri, filename));
+        final Context context = ImportVCardActivity.this;
+        final ContentResolver resolver = context.getContentResolver();
+        ReadableByteChannel inputChannel = null;
+        WritableByteChannel outputChannel = null;
+        Uri destUri = null;
+        try {
+            inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri));
+            destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
+            outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
+            final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
+            while (inputChannel.read(buffer) != -1) {
+                buffer.flip();
+                outputChannel.write(buffer);
+                buffer.compact();
+            }
+            buffer.flip();
+            while (buffer.hasRemaining()) {
+                outputChannel.write(buffer);
+            }
+        } finally {
+            if (inputChannel != null) {
+                try {
+                    inputChannel.close();
+                } catch (IOException e) {
+                    Log.w(LOG_TAG, "Failed to close inputChannel.");
+                }
+            }
+            if (outputChannel != null) {
+                try {
+                    outputChannel.close();
+                } catch(IOException e) {
+                    Log.w(LOG_TAG, "Failed to close outputChannel");
+                }
+            }
+        }
+        return destUri;
+    }
+
+    /**
+     * Reads the file from {@param sourceUri} and copies it to local cache file.
+     * Returns the local file name which stores the file from sourceUri.
+     */
+    private String readUriToLocalFile(Uri sourceUri) {
+        // Read the uri to local first.
+        int cache_index = 0;
+        String localFilename = null;
+        // Note: caches are removed by VCardService.
+        while (true) {
+            localFilename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
+            final File file = getFileStreamPath(localFilename);
+            if (!file.exists()) {
+                break;
+            } else {
+                if (cache_index == Integer.MAX_VALUE) {
+                    throw new RuntimeException("Exceeded cache limit");
+                }
+                cache_index++;
+            }
+        }
+        try {
+            copyTo(sourceUri, localFilename);
+        } catch (SecurityException e) {
+            Log.e(LOG_TAG, "SecurityException", e);
+            showFailureNotification(R.string.fail_reason_io_error);
+            return null;
+        } catch (IOException e) {
+            Log.e(LOG_TAG, "IOException during caching vCard", e);
+            showFailureNotification(R.string.fail_reason_io_error);
+            return null;
+        }
+
+        if (localFilename == null) {
+            Log.e(LOG_TAG, "Cannot load uri to local storage.");
+            showFailureNotification(R.string.fail_reason_io_error);
+            return null;
+        }
+
+        return localFilename;
+    }
+
+    private Uri readUriToLocalUri(Uri sourceUri) {
+        final String fileName = readUriToLocalFile(sourceUri);
+        if (fileName == null) {
+            return null;
+        }
+        return Uri.parse(getFileStreamPath(fileName).toURI().toString());
+    }
+
+    // Returns true if uri is from Storage.
+    private boolean isStorageUri(Uri uri) {
+        return uri != null && uri.toString().startsWith(STORAGE_VCARD_URI_PREFIX);
+    }
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        Uri sourceUri = getIntent().getData();
+
+        // Reading uris from non-storage needs the permission granted from the source intent,
+        // instead of permissions from RequestImportVCardPermissionActivity. So skipping requesting
+        // permissions from RequestImportVCardPermissionActivity for uris from non-storage source.
+        if (isStorageUri(sourceUri)
+                && RequestImportVCardPermissionsActivity.startPermissionActivity(this)) {
+            return;
+        }
+
+        String sourceDisplayName = null;
+        if (sourceUri != null) {
+            // Read the uri to local first.
+            String localTmpFileName = getIntent().getStringExtra(LOCAL_TMP_FILE_NAME_EXTRA);
+            sourceDisplayName = getIntent().getStringExtra(SOURCE_URI_DISPLAY_NAME);
+            if (TextUtils.isEmpty(localTmpFileName)) {
+                localTmpFileName = readUriToLocalFile(sourceUri);
+                sourceDisplayName = getDisplayName(sourceUri);
+                if (localTmpFileName == null) {
+                    Log.e(LOG_TAG, "Cannot load uri to local storage.");
+                    showFailureNotification(R.string.fail_reason_io_error);
+                    return;
+                }
+                getIntent().putExtra(LOCAL_TMP_FILE_NAME_EXTRA, localTmpFileName);
+                getIntent().putExtra(SOURCE_URI_DISPLAY_NAME, sourceDisplayName);
+            }
+            sourceUri = Uri.parse(getFileStreamPath(localTmpFileName).toURI().toString());
+        }
+
+        // Always request required permission for contacts before importing the vcard.
+        if (RequestImportVCardPermissionsActivity.startPermissionActivity(this)) {
+            return;
+        }
+
+        String accountName = null;
+        String accountType = null;
+        String dataSet = null;
+        final Intent intent = getIntent();
+        if (intent != null) {
+            accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
+            accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
+            dataSet = intent.getStringExtra(SelectAccountActivity.DATA_SET);
+        } else {
+            Log.e(LOG_TAG, "intent does not exist");
+        }
+
+        if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+            mAccount = new AccountWithDataSet(accountName, accountType, dataSet);
+        } else {
+            final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
+            final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
+            if (accountList.size() == 0) {
+                mAccount = null;
+            } else if (accountList.size() == 1) {
+                mAccount = accountList.get(0);
+            } else {
+                startActivityForResult(new Intent(this, SelectAccountActivity.class),
+                        SELECT_ACCOUNT);
+                return;
+            }
+        }
+
+        startImport(sourceUri, sourceDisplayName);
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        if (requestCode == SELECT_ACCOUNT) {
+            if (resultCode == Activity.RESULT_OK) {
+                mAccount = new AccountWithDataSet(
+                        intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
+                        intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
+                        intent.getStringExtra(SelectAccountActivity.DATA_SET));
+                final Uri sourceUri = getIntent().getData();
+                if (sourceUri == null) {
+                    startImport(sourceUri, /* sourceDisplayName =*/ null);
+                } else {
+                    final String sourceDisplayName = getIntent().getStringExtra(
+                            SOURCE_URI_DISPLAY_NAME);
+                    final String localFileName = getIntent().getStringExtra(
+                            LOCAL_TMP_FILE_NAME_EXTRA);
+                    final Uri localUri = Uri.parse(
+                            getFileStreamPath(localFileName).toURI().toString());
+                    startImport(localUri, sourceDisplayName);
+                }
+            } else {
+                if (resultCode != Activity.RESULT_CANCELED) {
+                    Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
+                }
+                finish();
+            }
+        } else if (requestCode == REQUEST_OPEN_DOCUMENT) {
+            if (resultCode == Activity.RESULT_OK) {
+                final ClipData clipData = intent.getClipData();
+                if (clipData != null) {
+                    final ArrayList<Uri> uris = new ArrayList<>();
+                    final ArrayList<String> sourceDisplayNames = new ArrayList<>();
+                    for (int i = 0; i < clipData.getItemCount(); i++) {
+                        ClipData.Item item = clipData.getItemAt(i);
+                        final Uri uri = item.getUri();
+                        if (uri != null) {
+                            final Uri localUri = readUriToLocalUri(uri);
+                            if (localUri != null) {
+                                final String sourceDisplayName = getDisplayName(uri);
+                                uris.add(localUri);
+                                sourceDisplayNames.add(sourceDisplayName);
+                            }
+                        }
+                    }
+                    if (uris.isEmpty()) {
+                        Log.w(LOG_TAG, "No vCard was selected for import");
+                        finish();
+                    } else {
+                        Log.i(LOG_TAG, "Multiple vCards selected for import: " + uris);
+                        importVCard(uris.toArray(new Uri[0]),
+                                sourceDisplayNames.toArray(new String[0]));
+                    }
+                } else {
+                    final Uri uri = intent.getData();
+                    if (uri != null) {
+                        Log.i(LOG_TAG, "vCard selected for import: " + uri);
+                        final Uri localUri = readUriToLocalUri(uri);
+                        if (localUri != null) {
+                            final String sourceDisplayName = getDisplayName(uri);
+                            importVCard(localUri, sourceDisplayName);
+                        } else {
+                            Log.w(LOG_TAG, "No local URI for vCard import");
+                            finish();
+                        }
+                    } else {
+                        Log.w(LOG_TAG, "No vCard was selected for import");
+                        finish();
+                    }
+                }
+            } else {
+                if (resultCode != Activity.RESULT_CANCELED) {
+                    Log.w(LOG_TAG, "Result code was not OK nor CANCELED" + resultCode);
+                }
+                finish();
+            }
+        }
+    }
+
+    private void startImport(Uri uri, String sourceDisplayName) {
+        // Handle inbound files
+        if (uri != null) {
+            Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
+            importVCard(uri, sourceDisplayName);
+        } else {
+            Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
+            final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+            intent.addCategory(Intent.CATEGORY_OPENABLE);
+            intent.setType(VCardService.X_VCARD_MIME_TYPE);
+            intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+            startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
+        }
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int resId, Bundle bundle) {
+        if (resId == R.id.dialog_cache_vcard) {
+            if (mProgressDialogForCachingVCard == null) {
+                final String title = getString(R.string.caching_vcard_title);
+                final String message = getString(R.string.caching_vcard_message);
+                mProgressDialogForCachingVCard = new ProgressDialog(this);
+                mProgressDialogForCachingVCard.setTitle(title);
+                mProgressDialogForCachingVCard.setMessage(message);
+                mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+                mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread);
+                startVCardService();
+            }
+            return mProgressDialogForCachingVCard;
+        } else if (resId == R.id.dialog_error_with_message) {
+            String message = mErrorMessage;
+            if (TextUtils.isEmpty(message)) {
+                Log.e(LOG_TAG, "Error message is null while it must not.");
+                message = getString(R.string.fail_reason_unknown);
+            }
+            final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                .setTitle(getString(R.string.reading_vcard_failed_title))
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setMessage(message)
+                .setOnCancelListener(mCancelListener)
+                .setPositiveButton(android.R.string.ok, mCancelListener);
+            return builder.create();
+        }
+
+        return super.onCreateDialog(resId, bundle);
+    }
+
+    /* package */ void startVCardService() {
+        mConnection = new ImportRequestConnection();
+
+        Log.i(LOG_TAG, "Bind to VCardService.");
+        // We don't want the service finishes itself just after this connection.
+        Intent intent = new Intent(this, VCardService.class);
+        startService(intent);
+        bindService(new Intent(this, VCardService.class),
+                mConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Bundle savedInstanceState) {
+        super.onRestoreInstanceState(savedInstanceState);
+        if (mProgressDialogForCachingVCard != null) {
+            Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again.");
+            showDialog(R.id.dialog_cache_vcard);
+        }
+    }
+
+    /* package */ void showFailureNotification(int reasonId) {
+        final NotificationManager notificationManager =
+                (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
+        final Notification notification =
+                NotificationImportExportListener.constructImportFailureNotification(
+                        ImportVCardActivity.this,
+                        getString(reasonId));
+        notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
+                FAILURE_NOTIFICATION_ID, notification);
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                Toast.makeText(ImportVCardActivity.this,
+                        getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
+            }
+        });
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/NfcImportVCardActivity.java b/src/com/android/contacts/common/vcard/NfcImportVCardActivity.java
new file mode 100644
index 0000000..0634df4
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/NfcImportVCardActivity.java
@@ -0,0 +1,272 @@
+/*
+ * 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.vcard;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.activity.RequestPermissionsActivity;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.ImplicitIntentsUtil;
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntryCounter;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+import com.android.vcard.VCardSourceDetector;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardVersionException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class NfcImportVCardActivity extends Activity implements ServiceConnection,
+        VCardImportExportListener {
+    private static final String TAG = "NfcImportVCardActivity";
+
+    private static final int SELECT_ACCOUNT = 1;
+
+    private NdefRecord mRecord;
+    private AccountWithDataSet mAccount;
+
+    /* package */ class ImportTask extends AsyncTask<VCardService, Void, ImportRequest> {
+        @Override
+        public ImportRequest doInBackground(VCardService... services) {
+            ImportRequest request = createImportRequest();
+            if (request == null) {
+                return null;
+            }
+
+            ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
+            requests.add(request);
+            services[0].handleImportRequest(requests, NfcImportVCardActivity.this);
+            return request;
+        }
+
+        @Override
+        public void onCancelled() {
+            unbindService(NfcImportVCardActivity.this);
+        }
+
+        @Override
+        public void onPostExecute(ImportRequest request) {
+            unbindService(NfcImportVCardActivity.this);
+        }
+    }
+
+    /* package */ ImportRequest createImportRequest() {
+        VCardParser parser;
+        VCardEntryCounter counter = null;
+        VCardSourceDetector detector = null;
+        int vcardVersion = ImportVCardActivity.VCARD_VERSION_V21;
+        try {
+            ByteArrayInputStream is = new ByteArrayInputStream(mRecord.getPayload());
+            is.mark(0);
+            parser = new VCardParser_V21();
+            try {
+                counter = new VCardEntryCounter();
+                detector = new VCardSourceDetector();
+                parser.addInterpreter(counter);
+                parser.addInterpreter(detector);
+                parser.parse(is);
+            } catch (VCardVersionException e1) {
+                is.reset();
+                vcardVersion = ImportVCardActivity.VCARD_VERSION_V30;
+                parser = new VCardParser_V30();
+                try {
+                    counter = new VCardEntryCounter();
+                    detector = new VCardSourceDetector();
+                    parser.addInterpreter(counter);
+                    parser.addInterpreter(detector);
+                    parser.parse(is);
+                } catch (VCardVersionException e2) {
+                    return null;
+                }
+            } finally {
+                try {
+                    if (is != null) is.close();
+                } catch (IOException e) {
+                }
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Failed reading vcard data", e);
+            return null;
+        } catch (VCardNestedException e) {
+            Log.w(TAG, "Nested Exception is found (it may be false-positive).");
+            // Go through without throwing the Exception, as we may be able to detect the
+            // version before it
+        } catch (VCardException e) {
+            Log.e(TAG, "Error parsing vcard", e);
+            return null;
+        }
+
+        return new ImportRequest(mAccount, mRecord.getPayload(), null,
+                getString(R.string.nfc_vcard_file_name), detector.getEstimatedType(),
+                detector.getEstimatedCharset(), vcardVersion, counter.getCount());
+    }
+
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder binder) {
+        VCardService service = ((VCardService.MyBinder) binder).getService();
+        new ImportTask().execute(service);
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        // Do nothing
+    }
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        if (RequestPermissionsActivity.startPermissionActivity(this)) {
+            return;
+        }
+
+        Intent intent = getIntent();
+        if (!NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
+            Log.w(TAG, "Unknowon intent " + intent);
+            finish();
+            return;
+        }
+
+        String type = intent.getType();
+        if (type == null ||
+                (!"text/x-vcard".equals(type) && !"text/vcard".equals(type))) {
+            Log.w(TAG, "Not a vcard");
+            //setStatus(getString(R.string.fail_reason_not_supported));
+            finish();
+            return;
+        }
+        NdefMessage msg = (NdefMessage) intent.getParcelableArrayExtra(
+                NfcAdapter.EXTRA_NDEF_MESSAGES)[0];
+        mRecord = msg.getRecords()[0];
+
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
+        final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
+        if (accountList.size() == 0) {
+            mAccount = null;
+        } else if (accountList.size() == 1) {
+            mAccount = accountList.get(0);
+        } else {
+            startActivityForResult(new Intent(this, SelectAccountActivity.class), SELECT_ACCOUNT);
+            return;
+        }
+
+        startImport();
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+        if (requestCode == SELECT_ACCOUNT) {
+            if (resultCode == RESULT_OK) {
+                mAccount = new AccountWithDataSet(
+                        intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
+                        intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
+                        intent.getStringExtra(SelectAccountActivity.DATA_SET));
+                startImport();
+            } else {
+                finish();
+            }
+        }
+    }
+
+    private void startImport() {
+        // We don't want the service finishes itself just after this connection.
+        Intent intent = new Intent(this, VCardService.class);
+        startService(intent);
+        bindService(intent, this, Context.BIND_AUTO_CREATE);
+    }
+
+    @Override
+    public void onImportProcessed(ImportRequest request, int jobId, int sequence) {
+        // do nothing
+    }
+
+    @Override
+    public void onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount,
+            int totalCount) {
+        // do nothing
+    }
+
+    @Override
+    public void onImportFinished(ImportRequest request, int jobId, Uri uri) {
+        if (isFinishing()) {
+            Log.i(TAG, "Late import -- ignoring");
+            return;
+        }
+
+        if (uri != null) {
+            Uri contactUri = RawContacts.getContactLookupUri(getContentResolver(), uri);
+            Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
+            ImplicitIntentsUtil.startActivityInAppIfPossible(this, intent);
+            finish();
+        }
+    }
+
+    @Override
+    public void onImportFailed(ImportRequest request) {
+        if (isFinishing()) {
+            Log.i(TAG, "Late import failure -- ignoring");
+            return;
+        }
+        // TODO: report failure
+    }
+
+    @Override
+    public void onImportCanceled(ImportRequest request, int jobId) {
+        // do nothing
+    }
+
+    @Override
+    public void onExportProcessed(ExportRequest request, int jobId) {
+        // do nothing
+    }
+
+    @Override
+    public void onExportFailed(ExportRequest request) {
+        // do nothing
+    }
+
+    @Override
+    public void onCancelRequest(CancelRequest request, int type) {
+        // do nothing
+    }
+
+    @Override
+    public void onComplete() {
+        // do nothing
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/NotificationImportExportListener.java b/src/com/android/contacts/common/vcard/NotificationImportExportListener.java
new file mode 100644
index 0000000..2ac8c1c
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/NotificationImportExportListener.java
@@ -0,0 +1,318 @@
+/*
+ * 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.vcard;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.support.v4.app.NotificationCompat;
+import android.widget.Toast;
+
+import com.android.contacts.common.R;
+import com.android.vcard.VCardEntry;
+
+import java.text.NumberFormat;
+
+public class NotificationImportExportListener implements VCardImportExportListener,
+        Handler.Callback {
+    /** The tag used by vCard-related notifications. */
+    /* package */ static final String DEFAULT_NOTIFICATION_TAG = "VCardServiceProgress";
+    /**
+     * The tag used by vCard-related failure notifications.
+     * <p>
+     * Use a different tag from {@link #DEFAULT_NOTIFICATION_TAG} so that failures do not get
+     * replaced by other notifications and vice-versa.
+     */
+    /* package */ static final String FAILURE_NOTIFICATION_TAG = "VCardServiceFailure";
+
+    private final NotificationManager mNotificationManager;
+    private final Activity mContext;
+    private final Handler mHandler;
+
+    public NotificationImportExportListener(Activity activity) {
+        mContext = activity;
+        mNotificationManager = (NotificationManager) activity.getSystemService(
+                Context.NOTIFICATION_SERVICE);
+        mHandler = new Handler(this);
+    }
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        String text = (String) msg.obj;
+        Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
+        return true;
+    }
+
+    @Override
+    public void onImportProcessed(ImportRequest request, int jobId, int sequence) {
+        // Show a notification about the status
+        final String displayName;
+        final String message;
+        if (request.displayName != null) {
+            displayName = request.displayName;
+            message = mContext.getString(R.string.vcard_import_will_start_message, displayName);
+        } else {
+            displayName = mContext.getString(R.string.vcard_unknown_filename);
+            message = mContext.getString(
+                    R.string.vcard_import_will_start_message_with_default_name);
+        }
+
+        // We just want to show notification for the first vCard.
+        if (sequence == 0) {
+            // TODO: Ideally we should detect the current status of import/export and
+            // show "started" when we can import right now and show "will start" when
+            // we cannot.
+            mHandler.obtainMessage(0, message).sendToTarget();
+        }
+
+        final Notification notification = constructProgressNotification(mContext,
+                VCardService.TYPE_IMPORT, message, message, jobId, displayName, -1, 0);
+        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
+    }
+
+    @Override
+    public void onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount,
+            int totalCount) {
+        if (entry.isIgnorable()) {
+            return;
+        }
+
+        final String totalCountString = String.valueOf(totalCount);
+        final String tickerText =
+                mContext.getString(R.string.progress_notifier_message,
+                        String.valueOf(currentCount),
+                        totalCountString,
+                        entry.getDisplayName());
+        final String description = mContext.getString(R.string.importing_vcard_description,
+                entry.getDisplayName());
+
+        final Notification notification = constructProgressNotification(
+                mContext.getApplicationContext(), VCardService.TYPE_IMPORT, description, tickerText,
+                jobId, request.displayName, totalCount, currentCount);
+        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
+    }
+
+    @Override
+    public void onImportFinished(ImportRequest request, int jobId, Uri createdUri) {
+        final String description = mContext.getString(R.string.importing_vcard_finished_title,
+                request.displayName);
+        final Intent intent;
+        if (createdUri != null) {
+            final long rawContactId = ContentUris.parseId(createdUri);
+            final Uri contactUri = RawContacts.getContactLookupUri(
+                    mContext.getContentResolver(), ContentUris.withAppendedId(
+                            RawContacts.CONTENT_URI, rawContactId));
+            intent = new Intent(Intent.ACTION_VIEW, contactUri);
+        } else {
+            intent = new Intent(Intent.ACTION_VIEW);
+            intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
+        }
+        intent.setPackage(mContext.getPackageName());
+        final Notification notification =
+                NotificationImportExportListener.constructFinishNotification(mContext,
+                description, null, intent);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                jobId, notification);
+    }
+
+    @Override
+    public void onImportFailed(ImportRequest request) {
+        // TODO: a little unkind to show Toast in this case, which is shown just a moment.
+        // Ideally we should show some persistent something users can notice more easily.
+        mHandler.obtainMessage(0,
+                mContext.getString(R.string.vcard_import_request_rejected_message)).sendToTarget();
+    }
+
+    @Override
+    public void onImportCanceled(ImportRequest request, int jobId) {
+        final String description = mContext.getString(R.string.importing_vcard_canceled_title,
+                request.displayName);
+        final Notification notification =
+                NotificationImportExportListener.constructCancelNotification(mContext, description);
+        mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+                jobId, notification);
+    }
+
+    @Override
+    public void onExportProcessed(ExportRequest request, int jobId) {
+        final String displayName = ExportVCardActivity.getOpenableUriDisplayName(mContext,
+                request.destUri);
+        final String message = mContext.getString(R.string.contacts_export_will_start_message);
+
+        mHandler.obtainMessage(0, message).sendToTarget();
+        final Notification notification =
+                NotificationImportExportListener.constructProgressNotification(mContext,
+                        VCardService.TYPE_EXPORT, message, message, jobId, displayName, -1, 0);
+        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
+    }
+
+    @Override
+    public void onExportFailed(ExportRequest request) {
+        mHandler.obtainMessage(0,
+                mContext.getString(R.string.vcard_export_request_rejected_message)).sendToTarget();
+    }
+
+    @Override
+    public void onCancelRequest(CancelRequest request, int type) {
+        final String description = type == VCardService.TYPE_IMPORT ?
+                mContext.getString(R.string.importing_vcard_canceled_title, request.displayName) :
+                mContext.getString(R.string.exporting_vcard_canceled_title, request.displayName);
+        final Notification notification = constructCancelNotification(mContext, description);
+        mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, request.jobId, notification);
+    }
+
+    /**
+     * Constructs a {@link Notification} showing the current status of import/export.
+     * Users can cancel the process with the Notification.
+     *
+     * @param context
+     * @param type import/export
+     * @param description Content of the Notification.
+     * @param tickerText
+     * @param jobId
+     * @param displayName Name to be shown to the Notification (e.g. "finished importing XXXX").
+     * Typycally a file name.
+     * @param totalCount The number of vCard entries to be imported. Used to show progress bar.
+     * -1 lets the system show the progress bar with "indeterminate" state.
+     * @param currentCount The index of current vCard. Used to show progress bar.
+     */
+    /* package */ static Notification constructProgressNotification(
+            Context context, int type, String description, String tickerText,
+            int jobId, String displayName, int totalCount, int currentCount) {
+        // Note: We cannot use extra values here (like setIntExtra()), as PendingIntent doesn't
+        // preserve them across multiple Notifications. PendingIntent preserves the first extras
+        // (when flag is not set), or update them when PendingIntent#getActivity() is called
+        // (See PendingIntent#FLAG_UPDATE_CURRENT). In either case, we cannot preserve extras as we
+        // expect (for each vCard import/export request).
+        //
+        // We use query parameter in Uri instead.
+        // Scheme and Authority is arbitorary, assuming CancelActivity never refers them.
+        final Intent intent = new Intent(context, CancelActivity.class);
+        final Uri uri = (new Uri.Builder())
+                .scheme("invalidscheme")
+                .authority("invalidauthority")
+                .appendQueryParameter(CancelActivity.JOB_ID, String.valueOf(jobId))
+                .appendQueryParameter(CancelActivity.DISPLAY_NAME, displayName)
+                .appendQueryParameter(CancelActivity.TYPE, String.valueOf(type)).build();
+        intent.setData(uri);
+
+        final NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+        builder.setOngoing(true)
+                .setProgress(totalCount, currentCount, totalCount == - 1)
+                .setTicker(tickerText)
+                .setContentTitle(description)
+                .setColor(context.getResources().getColor(R.color.dialtacts_theme_color))
+                .setSmallIcon(type == VCardService.TYPE_IMPORT
+                        ? android.R.drawable.stat_sys_download
+                        : android.R.drawable.stat_sys_upload)
+                .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
+        if (totalCount > 0) {
+            String percentage =
+                    NumberFormat.getPercentInstance().format((double) currentCount / totalCount);
+            builder.setContentText(percentage);
+        }
+        return builder.getNotification();
+    }
+
+    /**
+     * Constructs a Notification telling users the process is canceled.
+     *
+     * @param context
+     * @param description Content of the Notification
+     */
+    /* package */ static Notification constructCancelNotification(
+            Context context, String description) {
+        return new NotificationCompat.Builder(context)
+                .setAutoCancel(true)
+                .setSmallIcon(android.R.drawable.stat_notify_error)
+                .setColor(context.getResources().getColor(R.color.dialtacts_theme_color))
+                .setContentTitle(description)
+                .setContentText(description)
+                // Launch an intent that won't resolve to anything. Restrict the intent to this
+                // app to make sure that no other app can steal this pending-intent b/19296918.
+                .setContentIntent(PendingIntent
+                        .getActivity(context, 0, new Intent(context.getPackageName(), null), 0))
+                .getNotification();
+    }
+
+    /**
+     * Constructs a Notification telling users the process is finished.
+     *
+     * @param context
+     * @param description Content of the Notification
+     * @param intent Intent to be launched when the Notification is clicked. Can be null.
+     */
+    /* package */ static Notification constructFinishNotification(
+            Context context, String title, String description, Intent intent) {
+        return constructFinishNotificationWithFlags(context, title, description, intent, 0);
+    }
+
+    /**
+     * @param flags use FLAG_ACTIVITY_NEW_TASK to set it as new task, to get rid of cached files.
+     */
+    /* package */ static Notification constructFinishNotificationWithFlags(
+            Context context, String title, String description, Intent intent, int flags) {
+        return new NotificationCompat.Builder(context)
+                .setAutoCancel(true)
+                .setColor(context.getResources().getColor(R.color.dialtacts_theme_color))
+                .setSmallIcon(R.drawable.ic_check_mark)
+                .setContentTitle(title)
+                .setContentText(description)
+                // If no intent provided, include an intent that won't resolve to anything.
+                // Restrict the intent to this app to make sure that no other app can steal this
+                // pending-intent b/19296918.
+                .setContentIntent(PendingIntent.getActivity(context, 0,
+                        (intent != null ? intent : new Intent(context.getPackageName(), null)),
+                        flags))
+                .getNotification();
+    }
+
+    /**
+     * Constructs a Notification telling the vCard import has failed.
+     *
+     * @param context
+     * @param reason The reason why the import has failed. Shown in description field.
+     */
+    /* package */ static Notification constructImportFailureNotification(
+            Context context, String reason) {
+        return new NotificationCompat.Builder(context)
+                .setAutoCancel(true)
+                .setColor(context.getResources().getColor(R.color.dialtacts_theme_color))
+                .setSmallIcon(android.R.drawable.stat_notify_error)
+                .setContentTitle(context.getString(R.string.vcard_import_failed))
+                .setContentText(reason)
+                // Launch an intent that won't resolve to anything. Restrict the intent to this
+                // app to make sure that no other app can steal this pending-intent b/19296918.
+                .setContentIntent(PendingIntent
+                        .getActivity(context, 0, new Intent(context.getPackageName(), null), 0))
+                .getNotification();
+    }
+
+    @Override
+    public void onComplete() {
+        mContext.finish();
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/ProcessorBase.java b/src/com/android/contacts/common/vcard/ProcessorBase.java
new file mode 100644
index 0000000..abc859d
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ProcessorBase.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.vcard;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.RunnableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A base processor class. One instance processes vCard one import/export request (imports a given
+ * vCard or exports a vCard). Expected to be used with {@link ExecutorService}.
+ *
+ * This instance starts itself with {@link #run()} method, and can be cancelled with
+ * {@link #cancel(boolean)}. Users can check the processor's status using {@link #isCancelled()}
+ * and {@link #isDone()} asynchronously.
+ *
+ * {@link #get()} and {@link #get(long, TimeUnit)}, which are form {@link Future}, aren't
+ * supported and {@link UnsupportedOperationException} will be just thrown when they are called.
+ */
+public abstract class ProcessorBase implements RunnableFuture<Object> {
+
+    /**
+     * @return the type of the processor. Must be {@link VCardService#TYPE_IMPORT} or
+     * {@link VCardService#TYPE_EXPORT}.
+     */
+    public abstract int getType();
+
+    @Override
+    public abstract void run();
+
+    /**
+     * Cancels this operation.
+     *
+     * @param mayInterruptIfRunning ignored. When this method is called, the instance
+     * stops processing and finish itself even if the thread is running.
+     *
+     * @see Future#cancel(boolean)
+     */
+    @Override
+    public abstract boolean cancel(boolean mayInterruptIfRunning);
+    @Override
+    public abstract boolean isCancelled();
+    @Override
+    public abstract boolean isDone();
+
+    /**
+     * Just throws {@link UnsupportedOperationException}.
+     */
+    @Override
+    public final Object get() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Just throws {@link UnsupportedOperationException}.
+     */
+    @Override
+    public final Object get(long timeout, TimeUnit unit) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/SelectAccountActivity.java b/src/com/android/contacts/common/vcard/SelectAccountActivity.java
new file mode 100644
index 0000000..387f3fb
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/SelectAccountActivity.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.vcard;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.AccountSelectionUtil;
+
+import java.util.List;
+
+public class SelectAccountActivity extends Activity {
+    private static final String LOG_TAG = "SelectAccountActivity";
+
+    public static final String ACCOUNT_NAME = "account_name";
+    public static final String ACCOUNT_TYPE = "account_type";
+    public static final String DATA_SET = "data_set";
+
+    private class CancelListener
+            implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+        public void onClick(DialogInterface dialog, int which) {
+            finish();
+        }
+        public void onCancel(DialogInterface dialog) {
+            finish();
+        }
+    }
+
+    private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
+
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        // There's three possibilities:
+        // - more than one accounts -> ask the user
+        // - just one account -> use the account without asking the user
+        // - no account -> use phone-local storage without asking the user
+        final int resId = R.string.import_from_vcf_file;
+        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
+        final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
+        if (accountList.size() == 0) {
+            Log.w(LOG_TAG, "Account does not exist");
+            finish();
+            return;
+        } else if (accountList.size() == 1) {
+            final AccountWithDataSet account = accountList.get(0);
+            final Intent intent = new Intent();
+            intent.putExtra(ACCOUNT_NAME, account.name);
+            intent.putExtra(ACCOUNT_TYPE, account.type);
+            intent.putExtra(DATA_SET, account.dataSet);
+            setResult(RESULT_OK, intent);
+            finish();
+            return;
+        }
+
+        Log.i(LOG_TAG, "The number of available accounts: " + accountList.size());
+
+        // Multiple accounts. Let users to select one.
+        mAccountSelectionListener =
+                new AccountSelectionUtil.AccountSelectedListener(
+                        this, accountList, resId) {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                        final AccountWithDataSet account = mAccountList.get(which);
+                        final Intent intent = new Intent();
+                        intent.putExtra(ACCOUNT_NAME, account.name);
+                        intent.putExtra(ACCOUNT_TYPE, account.type);
+                        intent.putExtra(DATA_SET, account.dataSet);
+                        setResult(RESULT_OK, intent);
+                        finish();
+                    }
+                };
+        showDialog(resId);
+        return;
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int resId, Bundle bundle) {
+        if (resId == R.string.import_from_vcf_file) {
+            if (mAccountSelectionListener == null) {
+                throw new NullPointerException(
+                        "mAccountSelectionListener must not be null.");
+            }
+            return AccountSelectionUtil.getSelectAccountDialog(this, resId,
+                    mAccountSelectionListener,
+                    new CancelListener());
+        }
+        return super.onCreateDialog(resId, bundle);
+    }
+}
diff --git a/src/com/android/contacts/common/vcard/ShareVCardActivity.java b/src/com/android/contacts/common/vcard/ShareVCardActivity.java
new file mode 100644
index 0000000..93868aa
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ShareVCardActivity.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2016 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.vcard;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.IBinder;
+import android.support.v4.content.FileProvider;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * This activity connects to VCardService, creates a .vcf file in cache directory and send export
+ * request with the file URI so as to write contacts data to the file in background.
+ */
+public class ShareVCardActivity extends ExportVCardActivity {
+    private static final String LOG_TAG = "VCardShare";
+    private final String EXPORT_FILE_PREFIX = "vcards_";
+    private final long A_DAY_IN_MILLIS = 1000 * 60 * 60 * 24;
+
+    @Override
+    public synchronized void onServiceConnected(ComponentName name, IBinder binder) {
+        if (DEBUG) Log.d(LOG_TAG, "connected to service, requesting a destination file name");
+        mConnected = true;
+        mService = ((VCardService.MyBinder) binder).getService();
+
+        clearExportFiles();
+
+        final File file = getLocalFile();
+        try {
+            file.createNewFile();
+        } catch (IOException e) {
+            Log.e(LOG_TAG, "Failed to create .vcf file, because: " + e);
+            finish();
+            return;
+        }
+
+        final Uri contentUri = FileProvider.getUriForFile(this,
+                getString(R.string.contacts_file_provider_authority), file);
+        if (DEBUG) Log.d(LOG_TAG, "exporting to " + contentUri);
+
+        final ExportRequest request = new ExportRequest(contentUri);
+        // The connection object will call finish().
+        mService.handleExportRequest(request, new NotificationImportExportListener(
+                ShareVCardActivity.this));
+        finish();
+    }
+
+    /**
+     * Delete the files (that are untouched for more than 1 day) in the cache directory.
+     * We cannot rely on VCardService to delete export files because it will delete export files
+     * right after finishing writing so no files could be shared. Therefore, our approach to
+     * deleting export files is:
+     * 1. put export files in cache directory so that Android may delete them;
+     * 2. manually delete the files that are older than 1 day when service is connected.
+     */
+    private void clearExportFiles() {
+        for (File file : getCacheDir().listFiles()) {
+            final long ageInMillis = System.currentTimeMillis() - file.lastModified();
+            if (file.getName().startsWith(EXPORT_FILE_PREFIX) && ageInMillis > A_DAY_IN_MILLIS) {
+                file.delete();
+            }
+        }
+    }
+
+    private File getLocalFile() {
+        final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
+        final String currentDateString = dateFormat.format(new Date()).toString();
+        final String localFilename = EXPORT_FILE_PREFIX + currentDateString + ".vcf";
+        return new File(getCacheDir(), localFilename);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/common/vcard/VCardCommonArguments.java b/src/com/android/contacts/common/vcard/VCardCommonArguments.java
new file mode 100644
index 0000000..c423ca3
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/VCardCommonArguments.java
@@ -0,0 +1,27 @@
+/*
+ * 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.vcard;
+
+/**
+ * Argument constants used by many activities and services.
+ */
+public class VCardCommonArguments {
+
+    // Argument used to pass calling activities to the target activity or service.
+    // The value should be a string class name (e.g. com.android.contacts.vcard.VCardCommonArgs)
+    public static final String ARG_CALLING_ACTIVITY = "CALLING_ACTIVITY";
+}
diff --git a/src/com/android/contacts/common/vcard/VCardImportExportListener.java b/src/com/android/contacts/common/vcard/VCardImportExportListener.java
new file mode 100644
index 0000000..e4e4893
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/VCardImportExportListener.java
@@ -0,0 +1,36 @@
+/*
+ * 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.vcard;
+
+import android.net.Uri;
+
+import com.android.vcard.VCardEntry;
+
+interface VCardImportExportListener {
+    void onImportProcessed(ImportRequest request, int jobId, int sequence);
+    void onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount,
+            int totalCount);
+    void onImportFinished(ImportRequest request, int jobId, Uri uri);
+    void onImportFailed(ImportRequest request);
+    void onImportCanceled(ImportRequest request, int jobId);
+
+    void onExportProcessed(ExportRequest request, int jobId);
+    void onExportFailed(ExportRequest request);
+
+    void onCancelRequest(CancelRequest request, int type);
+    void onComplete();
+}
diff --git a/src/com/android/contacts/common/vcard/VCardService.java b/src/com/android/contacts/common/vcard/VCardService.java
new file mode 100644
index 0000000..1d7837b
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/VCardService.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.vcard;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.contacts.common.R;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * The class responsible for handling vCard import/export requests.
+ *
+ * This Service creates one ImportRequest/ExportRequest object (as Runnable) per request and push
+ * it to {@link ExecutorService} with single thread executor. The executor handles each request
+ * one by one, and notifies users when needed.
+ */
+// TODO: Using IntentService looks simpler than using Service + ServiceConnection though this
+// works fine enough. Investigate the feasibility.
+public class VCardService extends Service {
+    private final static String LOG_TAG = "VCardService";
+
+    /* package */ final static boolean DEBUG = false;
+
+    /**
+     * Specifies the type of operation. Used when constructing a notification, canceling
+     * some operation, etc.
+     */
+    /* package */ static final int TYPE_IMPORT = 1;
+    /* package */ static final int TYPE_EXPORT = 2;
+
+    /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_";
+
+    /* package */ static final String X_VCARD_MIME_TYPE = "text/x-vcard";
+
+    private class CustomMediaScannerConnectionClient implements MediaScannerConnectionClient {
+        final MediaScannerConnection mConnection;
+        final String mPath;
+
+        public CustomMediaScannerConnectionClient(String path) {
+            mConnection = new MediaScannerConnection(VCardService.this, this);
+            mPath = path;
+        }
+
+        public void start() {
+            mConnection.connect();
+        }
+
+        @Override
+        public void onMediaScannerConnected() {
+            if (DEBUG) { Log.d(LOG_TAG, "Connected to MediaScanner. Start scanning."); }
+            mConnection.scanFile(mPath, null);
+        }
+
+        @Override
+        public void onScanCompleted(String path, Uri uri) {
+            if (DEBUG) { Log.d(LOG_TAG, "scan completed: " + path); }
+            mConnection.disconnect();
+            removeConnectionClient(this);
+        }
+    }
+
+    // Should be single thread, as we don't want to simultaneously handle import and export
+    // requests.
+    private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
+
+    private int mCurrentJobId;
+
+    // Stores all unfinished import/export jobs which will be executed by mExecutorService.
+    // Key is jobId.
+    private final SparseArray<ProcessorBase> mRunningJobMap = new SparseArray<ProcessorBase>();
+    // Stores ScannerConnectionClient objects until they finish scanning requested files.
+    // Uses List class for simplicity. It's not costly as we won't have multiple objects in
+    // almost all cases.
+    private final List<CustomMediaScannerConnectionClient> mRemainingScannerConnections =
+            new ArrayList<CustomMediaScannerConnectionClient>();
+
+    private MyBinder mBinder;
+
+    private String mCallingActivity;
+
+    // File names currently reserved by some export job.
+    private final Set<String> mReservedDestination = new HashSet<String>();
+    /* ** end of vCard exporter params ** */
+
+    public class MyBinder extends Binder {
+        public VCardService getService() {
+            return VCardService.this;
+        }
+    }
+
+   @Override
+    public void onCreate() {
+        super.onCreate();
+        mBinder = new MyBinder();
+        if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created.");
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int id) {
+        if (intent != null && intent.getExtras() != null) {
+            mCallingActivity = intent.getExtras().getString(
+                    VCardCommonArguments.ARG_CALLING_ACTIVITY);
+        } else {
+            mCallingActivity = null;
+        }
+        return START_STICKY;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+
+    @Override
+    public void onDestroy() {
+        if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed.");
+        cancelAllRequestsAndShutdown();
+        clearCache();
+        super.onDestroy();
+    }
+
+    public synchronized void handleImportRequest(List<ImportRequest> requests,
+            VCardImportExportListener listener) {
+        if (DEBUG) {
+            final ArrayList<String> uris = new ArrayList<String>();
+            final ArrayList<String> displayNames = new ArrayList<String>();
+            for (ImportRequest request : requests) {
+                uris.add(request.uri.toString());
+                displayNames.add(request.displayName);
+            }
+            Log.d(LOG_TAG,
+                    String.format("received multiple import request (uri: %s, displayName: %s)",
+                            uris.toString(), displayNames.toString()));
+        }
+        final int size = requests.size();
+        for (int i = 0; i < size; i++) {
+            ImportRequest request = requests.get(i);
+
+            if (tryExecute(new ImportProcessor(this, listener, request, mCurrentJobId))) {
+                if (listener != null) {
+                    listener.onImportProcessed(request, mCurrentJobId, i);
+                }
+                mCurrentJobId++;
+            } else {
+                if (listener != null) {
+                    listener.onImportFailed(request);
+                }
+                // A rejection means executor doesn't run any more. Exit.
+                break;
+            }
+        }
+    }
+
+    public synchronized void handleExportRequest(ExportRequest request,
+            VCardImportExportListener listener) {
+        if (tryExecute(new ExportProcessor(this, request, mCurrentJobId, mCallingActivity))) {
+            final String path = request.destUri.getEncodedPath();
+            if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path);
+            if (!mReservedDestination.add(path)) {
+                Log.w(LOG_TAG,
+                        String.format("The path %s is already reserved. Reject export request",
+                                path));
+                if (listener != null) {
+                    listener.onExportFailed(request);
+                }
+                return;
+            }
+
+            if (listener != null) {
+                listener.onExportProcessed(request, mCurrentJobId);
+            }
+            mCurrentJobId++;
+        } else {
+            if (listener != null) {
+                listener.onExportFailed(request);
+            }
+        }
+    }
+
+    /**
+     * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor.
+     * @return true when successful.
+     */
+    private synchronized boolean tryExecute(ProcessorBase processor) {
+        try {
+            if (DEBUG) {
+                Log.d(LOG_TAG, "Executor service status: shutdown: " + mExecutorService.isShutdown()
+                        + ", terminated: " + mExecutorService.isTerminated());
+            }
+            mExecutorService.execute(processor);
+            mRunningJobMap.put(mCurrentJobId, processor);
+            return true;
+        } catch (RejectedExecutionException e) {
+            Log.w(LOG_TAG, "Failed to excetute a job.", e);
+            return false;
+        }
+    }
+
+    public synchronized void handleCancelRequest(CancelRequest request,
+            VCardImportExportListener listener) {
+        final int jobId = request.jobId;
+        if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
+
+        final ProcessorBase processor = mRunningJobMap.get(jobId);
+        mRunningJobMap.remove(jobId);
+
+        if (processor != null) {
+            processor.cancel(true);
+            final int type = processor.getType();
+            if (listener != null) {
+                listener.onCancelRequest(request, type);
+            }
+            if (type == TYPE_EXPORT) {
+                final String path =
+                        ((ExportProcessor)processor).getRequest().destUri.getEncodedPath();
+                Log.i(LOG_TAG,
+                        String.format("Cancel reservation for the path %s if appropriate", path));
+                if (!mReservedDestination.remove(path)) {
+                    Log.w(LOG_TAG, "Not reserved.");
+                }
+            }
+        } else {
+            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
+        }
+        stopServiceIfAppropriate();
+    }
+
+    /**
+     * Checks job list and call {@link #stopSelf()} when there's no job and no scanner connection
+     * is remaining.
+     * A new job (import/export) cannot be submitted any more after this call.
+     */
+    private synchronized void stopServiceIfAppropriate() {
+        if (mRunningJobMap.size() > 0) {
+            final int size = mRunningJobMap.size();
+
+            // Check if there are processors which aren't finished yet. If we still have ones to
+            // process, we cannot stop the service yet. Also clean up already finished processors
+            // here.
+
+            // Job-ids to be removed. At first all elements in the array are invalid and will
+            // be filled with real job-ids from the array's top. When we find a not-yet-finished
+            // processor, then we start removing those finished jobs. In that case latter half of
+            // this array will be invalid.
+            final int[] toBeRemoved = new int[size];
+            for (int i = 0; i < size; i++) {
+                final int jobId = mRunningJobMap.keyAt(i);
+                final ProcessorBase processor = mRunningJobMap.valueAt(i);
+                if (!processor.isDone()) {
+                    Log.i(LOG_TAG, String.format("Found unfinished job (id: %d)", jobId));
+
+                    // Remove processors which are already "done", all of which should be before
+                    // processors which aren't done yet.
+                    for (int j = 0; j < i; j++) {
+                        mRunningJobMap.remove(toBeRemoved[j]);
+                    }
+                    return;
+                }
+
+                // Remember the finished processor.
+                toBeRemoved[i] = jobId;
+            }
+
+            // We're sure we can remove all. Instead of removing one by one, just call clear().
+            mRunningJobMap.clear();
+        }
+
+        if (!mRemainingScannerConnections.isEmpty()) {
+            Log.i(LOG_TAG, "MediaScanner update is in progress.");
+            return;
+        }
+
+        Log.i(LOG_TAG, "No unfinished job. Stop this service.");
+        mExecutorService.shutdown();
+        stopSelf();
+    }
+
+    /* package */ synchronized void updateMediaScanner(String path) {
+        if (DEBUG) {
+            Log.d(LOG_TAG, "MediaScanner is being updated: " + path);
+        }
+
+        if (mExecutorService.isShutdown()) {
+            Log.w(LOG_TAG, "MediaScanner update is requested after executor's being shut down. " +
+                    "Ignoring the update request");
+            return;
+        }
+        final CustomMediaScannerConnectionClient client =
+                new CustomMediaScannerConnectionClient(path);
+        mRemainingScannerConnections.add(client);
+        client.start();
+    }
+
+    private synchronized void removeConnectionClient(
+            CustomMediaScannerConnectionClient client) {
+        if (DEBUG) {
+            Log.d(LOG_TAG, "Removing custom MediaScannerConnectionClient.");
+        }
+        mRemainingScannerConnections.remove(client);
+        stopServiceIfAppropriate();
+    }
+
+    /* package */ synchronized void handleFinishImportNotification(
+            int jobId, boolean successful) {
+        if (DEBUG) {
+            Log.d(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
+                    + "Result: %b", jobId, (successful ? "success" : "failure")));
+        }
+        mRunningJobMap.remove(jobId);
+        stopServiceIfAppropriate();
+    }
+
+    /* package */ synchronized void handleFinishExportNotification(
+            int jobId, boolean successful) {
+        if (DEBUG) {
+            Log.d(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
+                    + "Result: %b", jobId, (successful ? "success" : "failure")));
+        }
+        final ProcessorBase job = mRunningJobMap.get(jobId);
+        mRunningJobMap.remove(jobId);
+        if (job == null) {
+            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
+        } else if (!(job instanceof ExportProcessor)) {
+            Log.w(LOG_TAG,
+                    String.format("Removed job (id: %s) isn't ExportProcessor", jobId));
+        } else {
+            final String path = ((ExportProcessor)job).getRequest().destUri.getEncodedPath();
+            if (DEBUG) Log.d(LOG_TAG, "Remove reserved path " + path);
+            mReservedDestination.remove(path);
+        }
+
+        stopServiceIfAppropriate();
+    }
+
+    /**
+     * Cancels all the import/export requests and calls {@link ExecutorService#shutdown()}, which
+     * means this Service becomes no longer ready for import/export requests.
+     *
+     * Mainly called from onDestroy().
+     */
+    private synchronized void cancelAllRequestsAndShutdown() {
+        for (int i = 0; i < mRunningJobMap.size(); i++) {
+            mRunningJobMap.valueAt(i).cancel(true);
+        }
+        mRunningJobMap.clear();
+        mExecutorService.shutdown();
+    }
+
+    /**
+     * Removes import caches stored locally.
+     */
+    private void clearCache() {
+        for (final String fileName : fileList()) {
+            if (fileName.startsWith(CACHE_FILE_PREFIX)) {
+                // We don't want to keep all the caches so we remove cache files old enough.
+                Log.i(LOG_TAG, "Remove a temporary file: " + fileName);
+                deleteFile(fileName);
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/widget/ActivityTouchLinearLayout.java b/src/com/android/contacts/common/widget/ActivityTouchLinearLayout.java
new file mode 100644
index 0000000..d81526e
--- /dev/null
+++ b/src/com/android/contacts/common/widget/ActivityTouchLinearLayout.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2014 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+
+import com.android.contacts.common.interactions.TouchPointManager;
+
+/**
+ * Linear layout for an activity that listens to all touch events on the screen and saves the touch
+ * point.
+ * Typically touch events are handled by child views--this class intercepts those touch events
+ * before passing them on to the child.
+ */
+public class ActivityTouchLinearLayout extends LinearLayout {
+    public ActivityTouchLinearLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent (MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/contacts/common/widget/FloatingActionButtonController.java b/src/com/android/contacts/common/widget/FloatingActionButtonController.java
new file mode 100644
index 0000000..0e94df1
--- /dev/null
+++ b/src/com/android/contacts/common/widget/FloatingActionButtonController.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2014 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.widget;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.View;
+import android.widget.ImageButton;
+
+import com.android.contacts.common.util.ViewUtil;
+import com.android.contacts.common.R;
+import com.android.phone.common.animation.AnimUtils;
+
+/**
+ * Controls the movement and appearance of the FAB (Floating Action Button).
+ */
+public class FloatingActionButtonController {
+    public static final int ALIGN_MIDDLE = 0;
+    public static final int ALIGN_QUARTER_END = 1;
+    public static final int ALIGN_END = 2;
+
+    private static final int FAB_SCALE_IN_DURATION = 186;
+    private static final int FAB_SCALE_IN_FADE_IN_DELAY = 70;
+    private static final int FAB_ICON_FADE_OUT_DURATION = 46;
+
+    private final int mAnimationDuration;
+    private final int mFloatingActionButtonWidth;
+    private final int mFloatingActionButtonMarginRight;
+    private final View mFloatingActionButtonContainer;
+    private final ImageButton mFloatingActionButton;
+    private final Interpolator mFabInterpolator;
+    private int mScreenWidth;
+
+    public FloatingActionButtonController(Activity activity, View container, ImageButton button) {
+        Resources resources = activity.getResources();
+        mFabInterpolator = AnimationUtils.loadInterpolator(activity,
+                android.R.interpolator.fast_out_slow_in);
+        mFloatingActionButtonWidth = resources.getDimensionPixelSize(
+                R.dimen.floating_action_button_width);
+        mFloatingActionButtonMarginRight = resources.getDimensionPixelOffset(
+                R.dimen.floating_action_button_margin_right);
+        mAnimationDuration = resources.getInteger(
+                R.integer.floating_action_button_animation_duration);
+        mFloatingActionButtonContainer = container;
+        mFloatingActionButton = button;
+        ViewUtil.setupFloatingActionButton(mFloatingActionButtonContainer, resources);
+    }
+
+    /**
+     * Passes the screen width into the class. Necessary for translation calculations.
+     * Should be called as soon as parent View width is available.
+     *
+     * @param screenWidth The width of the screen in pixels.
+     */
+    public void setScreenWidth(int screenWidth) {
+        mScreenWidth = screenWidth;
+    }
+
+    /**
+     * Sets FAB as View.VISIBLE or View.GONE.
+     *
+     * @param visible Whether or not to make the container visible.
+     */
+    public void setVisible(boolean visible) {
+        mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
+    }
+
+    public boolean isVisible() {
+        return mFloatingActionButtonContainer.getVisibility() == View.VISIBLE;
+    }
+
+    public void changeIcon(Drawable icon, String description) {
+        if (mFloatingActionButton.getDrawable() != icon
+                || !mFloatingActionButton.getContentDescription().equals(description)) {
+            mFloatingActionButton.setImageDrawable(icon);
+            mFloatingActionButton.setContentDescription(description);
+        }
+    }
+
+    /**
+     * Updates the FAB location (middle to right position) as the PageView scrolls.
+     *
+     * @param positionOffset A fraction used to calculate position of the FAB during page scroll.
+     */
+    public void onPageScrolled(float positionOffset) {
+        // As the page is scrolling, if we're on the first tab, update the FAB position so it
+        // moves along with it.
+        mFloatingActionButtonContainer.setTranslationX(
+                (int) (positionOffset * getTranslationXForAlignment(ALIGN_END)));
+    }
+
+    /**
+     * Aligns the FAB to the described location
+     *
+     * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT.
+     * @param animate Whether or not to animate the transition.
+     */
+    public void align(int align, boolean animate) {
+        align(align, 0 /*offsetX */, 0 /* offsetY */, animate);
+    }
+
+    /**
+     * Aligns the FAB to the described location plus specified additional offsets.
+     *
+     * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT.
+     * @param offsetX Additional offsetX to translate by.
+     * @param offsetY Additional offsetY to translate by.
+     * @param animate Whether or not to animate the transition.
+     */
+    public void align(int align, int offsetX, int offsetY, boolean animate) {
+        if (mScreenWidth == 0) {
+            return;
+        }
+
+        int translationX = getTranslationXForAlignment(align);
+
+        // Skip animation if container is not shown; animation causes container to show again.
+        if (animate && mFloatingActionButtonContainer.isShown()) {
+            mFloatingActionButtonContainer.animate()
+                    .translationX(translationX + offsetX)
+                    .translationY(offsetY)
+                    .setInterpolator(mFabInterpolator)
+                    .setDuration(mAnimationDuration)
+                    .start();
+        } else {
+            mFloatingActionButtonContainer.setTranslationX(translationX + offsetX);
+            mFloatingActionButtonContainer.setTranslationY(offsetY);
+        }
+    }
+
+    /**
+     * Resizes width and height of the floating action bar container.
+     * @param dimension The new dimensions for the width and height.
+     * @param animate Whether to animate this change.
+     */
+    public void resize(int dimension, boolean animate) {
+        if (animate) {
+            AnimUtils.changeDimensions(mFloatingActionButtonContainer, dimension, dimension);
+        } else {
+            mFloatingActionButtonContainer.getLayoutParams().width = dimension;
+            mFloatingActionButtonContainer.getLayoutParams().height = dimension;
+            mFloatingActionButtonContainer.requestLayout();
+        }
+    }
+
+    /**
+     * Scales the floating action button from no height and width to its actual dimensions. This is
+     * an animation for showing the floating action button.
+     * @param delayMs The delay for the effect, in milliseconds.
+     */
+    public void scaleIn(int delayMs) {
+        setVisible(true);
+        AnimUtils.scaleIn(mFloatingActionButtonContainer, FAB_SCALE_IN_DURATION, delayMs);
+        AnimUtils.fadeIn(mFloatingActionButton, FAB_SCALE_IN_DURATION,
+                delayMs + FAB_SCALE_IN_FADE_IN_DELAY, null);
+    }
+
+    /**
+     * Immediately remove the affects of the last call to {@link #scaleOut}.
+     */
+    public void resetIn() {
+        mFloatingActionButton.setAlpha(1f);
+        mFloatingActionButton.setVisibility(View.VISIBLE);
+        mFloatingActionButtonContainer.setScaleX(1);
+        mFloatingActionButtonContainer.setScaleY(1);
+    }
+
+    /**
+     * Scales the floating action button from its actual dimensions to no height and width. This is
+     * an animation for hiding the floating action button.
+     */
+    public void scaleOut() {
+        AnimUtils.scaleOut(mFloatingActionButtonContainer, mAnimationDuration);
+        // Fade out the icon faster than the scale out animation, so that the icon scaling is less
+        // obvious. We don't want it to scale, but the resizing the container is not as performant.
+        AnimUtils.fadeOut(mFloatingActionButton, FAB_ICON_FADE_OUT_DURATION, null);
+    }
+
+    /**
+     * Calculates the X offset of the FAB to the given alignment, adjusted for whether or not the
+     * view is in RTL mode.
+     *
+     * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT.
+     * @return The translationX for the given alignment.
+     */
+    public int getTranslationXForAlignment(int align) {
+        int result = 0;
+        switch (align) {
+            case ALIGN_MIDDLE:
+                // Moves the FAB to exactly center screen.
+                return 0;
+            case ALIGN_QUARTER_END:
+                // Moves the FAB a quarter of the screen width.
+                result = mScreenWidth / 4;
+                break;
+            case ALIGN_END:
+                // Moves the FAB half the screen width. Same as aligning right with a marginRight.
+                result = mScreenWidth / 2
+                        - mFloatingActionButtonWidth / 2
+                        - mFloatingActionButtonMarginRight;
+                break;
+        }
+        if (isLayoutRtl()) {
+            result *= -1;
+        }
+        return result;
+    }
+
+    private boolean isLayoutRtl() {
+        return mFloatingActionButtonContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+    }
+}
diff --git a/src/com/android/contacts/common/widget/LayoutSuppressingImageView.java b/src/com/android/contacts/common/widget/LayoutSuppressingImageView.java
new file mode 100644
index 0000000..abcf786
--- /dev/null
+++ b/src/com/android/contacts/common/widget/LayoutSuppressingImageView.java
@@ -0,0 +1,39 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * Custom {@link ImageView} that improves layouting performance.
+ *
+ * This improves the performance by not passing requestLayout() to its parent, taking advantage
+ * of knowing that image size won't change once set.
+ */
+public class LayoutSuppressingImageView extends ImageView {
+
+    public LayoutSuppressingImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void requestLayout() {
+        forceLayout();
+    }
+}
diff --git a/src/com/android/contacts/common/widget/LayoutSuppressingQuickContactBadge.java b/src/com/android/contacts/common/widget/LayoutSuppressingQuickContactBadge.java
new file mode 100644
index 0000000..1f48f5d
--- /dev/null
+++ b/src/com/android/contacts/common/widget/LayoutSuppressingQuickContactBadge.java
@@ -0,0 +1,39 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.QuickContactBadge;
+
+/**
+ * Custom {@link QuickContactBadge} that improves layouting performance.
+ *
+ * This improves the performance by not passing requestLayout() to its parent, taking advantage
+ * of knowing that image size won't change once set.
+ */
+public class LayoutSuppressingQuickContactBadge extends QuickContactBadge {
+
+    public LayoutSuppressingQuickContactBadge(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void requestLayout() {
+        forceLayout();
+    }
+}
diff --git a/src/com/android/contacts/common/widget/ProportionalLayout.java b/src/com/android/contacts/common/widget/ProportionalLayout.java
new file mode 100644
index 0000000..5a5ac29
--- /dev/null
+++ b/src/com/android/contacts/common/widget/ProportionalLayout.java
@@ -0,0 +1,146 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.contacts.common.R;
+
+/**
+ * Layout that calculates its height based on its width, or vice versa (depending on the set
+ * {@link #setDirection(Direction)}. The factor is specified in {@link #setRatio(float)}.
+ * <p>For {@link Direction#heightToWidth}: width := height * factor</p>
+ * <p>For {@link Direction#widthToHeight}: height := width * factor</p>
+ * <p>Only one child is allowed; if more are required, another ViewGroup can be used as the direct
+ * child of this layout.</p>
+ */
+public class ProportionalLayout extends ViewGroup {
+    /** Specifies whether the width should be calculated based on the height or vice-versa  */
+    public enum Direction {
+        widthToHeight("widthToHeight"),
+        heightToWidth("heightToWidth");
+
+        public final String XmlName;
+
+        private Direction(String xmlName) {
+            XmlName = xmlName;
+        }
+
+        /**
+         * Parses the given direction string and returns the Direction instance. This
+         * should be used when inflating from xml
+         */
+        public static Direction parse(String value) {
+            if (widthToHeight.XmlName.equals(value)) {
+                return Direction.widthToHeight;
+            } else if (heightToWidth.XmlName.equals(value)) {
+                return Direction.heightToWidth;
+            } else {
+                throw new IllegalStateException("direction must be either " +
+                        widthToHeight.XmlName + " or " + heightToWidth.XmlName);
+            }
+        }
+    }
+
+    private Direction mDirection;
+    private float mRatio;
+
+    public ProportionalLayout(Context context) {
+        super(context);
+    }
+
+    public ProportionalLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initFromAttributes(context, attrs);
+    }
+
+    public ProportionalLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        initFromAttributes(context, attrs);
+    }
+
+    private void initFromAttributes(Context context, AttributeSet attrs) {
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProportionalLayout);
+
+        mDirection = Direction.parse(a.getString(R.styleable.ProportionalLayout_direction));
+        mRatio = a.getFloat(R.styleable.ProportionalLayout_ratio, 1.0f);
+
+        a.recycle();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (getChildCount() != 1) {
+            throw new IllegalStateException("ProportionalLayout requires exactly one child");
+        }
+
+        final View child = getChildAt(0);
+
+        // Do a first pass to get the optimal size
+        measureChild(child, widthMeasureSpec, heightMeasureSpec);
+        final int childWidth = child.getMeasuredWidth();
+        final int childHeight = child.getMeasuredHeight();
+
+        final int width;
+        final int height;
+        if (mDirection == Direction.heightToWidth) {
+            width = Math.round(childHeight * mRatio);
+            height = childHeight;
+        } else {
+            width = childWidth;
+            height = Math.round(childWidth * mRatio);
+        }
+
+        // Do a second pass so that all children are informed of the new size
+        measureChild(child,
+                MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+
+        setMeasuredDimension(
+                resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (getChildCount() != 1) {
+            throw new IllegalStateException("ProportionalLayout requires exactly one child");
+        }
+
+        final View child = getChildAt(0);
+        child.layout(0, 0, right-left, bottom-top);
+    }
+
+    public Direction getDirection() {
+        return mDirection;
+    }
+
+    public void setDirection(Direction direction) {
+        mDirection = direction;
+    }
+
+    public float getRatio() {
+        return mRatio;
+    }
+
+    public void setRatio(float ratio) {
+        mRatio = ratio;
+    }
+}
diff --git a/src/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/src/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
new file mode 100644
index 0000000..709ce41
--- /dev/null
+++ b/src/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2014 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.widget;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.PhoneAccountCompat;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Dialog that allows the user to select a phone accounts for a given action. Optionally provides
+ * the choice to set the phone account as default.
+ */
+public class SelectPhoneAccountDialogFragment extends DialogFragment {
+    private static final String ARG_TITLE_RES_ID = "title_res_id";
+    private static final String ARG_CAN_SET_DEFAULT = "can_set_default";
+    private static final String ARG_ACCOUNT_HANDLES = "account_handles";
+    private static final String ARG_IS_DEFAULT_CHECKED = "is_default_checked";
+    private static final String ARG_LISTENER = "listener";
+
+    private int mTitleResId;
+    private boolean mCanSetDefault;
+    private List<PhoneAccountHandle> mAccountHandles;
+    private boolean mIsSelected;
+    private boolean mIsDefaultChecked;
+    private TelecomManager mTelecomManager;
+    private SelectPhoneAccountListener mListener;
+
+    /**
+     * Create new fragment instance with default title and no option to set as default.
+     *
+     * @param accountHandles The {@code PhoneAccountHandle}s available to select from.
+     * @param listener The listener for the results of the account selection.
+     */
+    public static SelectPhoneAccountDialogFragment newInstance(
+            List<PhoneAccountHandle> accountHandles, SelectPhoneAccountListener listener) {
+        return newInstance(R.string.select_account_dialog_title, false,
+                accountHandles, listener);
+    }
+
+    /**
+     * Create new fragment instance.
+     * This method also allows specifying a custom title and "set default" checkbox.
+     *
+     * @param titleResId The resource ID for the string to use in the title of the dialog.
+     * @param canSetDefault {@code true} if the dialog should include an option to set the selection
+     * as the default. False otherwise.
+     * @param accountHandles The {@code PhoneAccountHandle}s available to select from.
+     * @param listener The listener for the results of the account selection.
+     */
+    public static SelectPhoneAccountDialogFragment newInstance(int titleResId,
+            boolean canSetDefault, List<PhoneAccountHandle> accountHandles,
+            SelectPhoneAccountListener listener) {
+        ArrayList<PhoneAccountHandle> accountHandlesCopy = new ArrayList<PhoneAccountHandle>();
+        if (accountHandles != null) {
+            accountHandlesCopy.addAll(accountHandles);
+        }
+        SelectPhoneAccountDialogFragment fragment = new SelectPhoneAccountDialogFragment();
+        final Bundle args = new Bundle();
+        args.putInt(ARG_TITLE_RES_ID, titleResId);
+        args.putBoolean(ARG_CAN_SET_DEFAULT, canSetDefault);
+        args.putParcelableArrayList(ARG_ACCOUNT_HANDLES, accountHandlesCopy);
+        args.putParcelable(ARG_LISTENER, listener);
+        fragment.setArguments(args);
+        fragment.setListener(listener);
+        return fragment;
+    }
+
+    public SelectPhoneAccountDialogFragment() {
+    }
+
+    public void setListener(SelectPhoneAccountListener listener) {
+        mListener = listener;
+    }
+
+    public static class SelectPhoneAccountListener extends ResultReceiver {
+        static final int RESULT_SELECTED = 1;
+        static final int RESULT_DISMISSED = 2;
+
+        static final String EXTRA_SELECTED_ACCOUNT_HANDLE = "extra_selected_account_handle";
+        static final String EXTRA_SET_DEFAULT = "extra_set_default";
+
+        public SelectPhoneAccountListener() {
+            super(new Handler());
+        }
+
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            if (resultCode == RESULT_SELECTED) {
+                onPhoneAccountSelected(
+                        (PhoneAccountHandle) resultData.getParcelable(
+                                EXTRA_SELECTED_ACCOUNT_HANDLE),
+                        resultData.getBoolean(EXTRA_SET_DEFAULT));
+            } else if (resultCode == RESULT_DISMISSED) {
+                onDialogDismissed();
+            }
+        }
+
+        public void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle,
+                boolean setDefault) {}
+
+        public void onDialogDismissed() {}
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putBoolean(ARG_IS_DEFAULT_CHECKED, mIsDefaultChecked);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID);
+        mCanSetDefault = getArguments().getBoolean(ARG_CAN_SET_DEFAULT);
+        mAccountHandles = getArguments().getParcelableArrayList(ARG_ACCOUNT_HANDLES);
+        mListener = getArguments().getParcelable(ARG_LISTENER);
+        if (savedInstanceState != null) {
+            mIsDefaultChecked = savedInstanceState.getBoolean(ARG_IS_DEFAULT_CHECKED);
+        }
+        mIsSelected = false;
+        mTelecomManager =
+                (TelecomManager) getActivity().getSystemService(Context.TELECOM_SERVICE);
+
+        final DialogInterface.OnClickListener selectionListener =
+                new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                mIsSelected = true;
+                PhoneAccountHandle selectedAccountHandle = mAccountHandles.get(which);
+                final Bundle result = new Bundle();
+                result.putParcelable(SelectPhoneAccountListener.EXTRA_SELECTED_ACCOUNT_HANDLE,
+                        selectedAccountHandle);
+                result.putBoolean(SelectPhoneAccountListener.EXTRA_SET_DEFAULT,
+                        mIsDefaultChecked);
+                if (mListener != null) {
+                    mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_SELECTED, result);
+                }
+            }
+        };
+
+        final CompoundButton.OnCheckedChangeListener checkListener =
+                new CompoundButton.OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(CompoundButton check, boolean isChecked) {
+                mIsDefaultChecked = isChecked;
+            }
+        };
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        ListAdapter selectAccountListAdapter = new SelectAccountListAdapter(
+                builder.getContext(),
+                R.layout.select_account_list_item,
+                mAccountHandles);
+
+        AlertDialog dialog = builder.setTitle(mTitleResId)
+                .setAdapter(selectAccountListAdapter, selectionListener)
+                .create();
+
+        if (mCanSetDefault) {
+            // Generate custom checkbox view
+            LinearLayout checkboxLayout = (LinearLayout) getActivity()
+                    .getLayoutInflater()
+                    .inflate(R.layout.default_account_checkbox, null);
+
+            CheckBox cb =
+                    (CheckBox) checkboxLayout.findViewById(R.id.default_account_checkbox_view);
+            cb.setOnCheckedChangeListener(checkListener);
+            cb.setChecked(mIsDefaultChecked);
+
+            dialog.getListView().addFooterView(checkboxLayout);
+        }
+
+        return dialog;
+    }
+
+    private class SelectAccountListAdapter extends ArrayAdapter<PhoneAccountHandle> {
+        private int mResId;
+
+        public SelectAccountListAdapter(
+                Context context, int resource, List<PhoneAccountHandle> accountHandles) {
+            super(context, resource, accountHandles);
+            mResId = resource;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            LayoutInflater inflater = (LayoutInflater)
+                    getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+            View rowView;
+            final ViewHolder holder;
+
+            if (convertView == null) {
+                // Cache views for faster scrolling
+                rowView = inflater.inflate(mResId, null);
+                holder = new ViewHolder();
+                holder.labelTextView = (TextView) rowView.findViewById(R.id.label);
+                holder.numberTextView = (TextView) rowView.findViewById(R.id.number);
+                holder.imageView = (ImageView) rowView.findViewById(R.id.icon);
+                rowView.setTag(holder);
+            }
+            else {
+                rowView = convertView;
+                holder = (ViewHolder) rowView.getTag();
+            }
+
+            PhoneAccountHandle accountHandle = getItem(position);
+            PhoneAccount account = mTelecomManager.getPhoneAccount(accountHandle);
+            if (account == null) {
+                return rowView;
+            }
+            holder.labelTextView.setText(account.getLabel());
+            if (account.getAddress() == null ||
+                    TextUtils.isEmpty(account.getAddress().getSchemeSpecificPart())) {
+                holder.numberTextView.setVisibility(View.GONE);
+            } else {
+                holder.numberTextView.setVisibility(View.VISIBLE);
+                holder.numberTextView.setText(
+                        PhoneNumberUtilsCompat.createTtsSpannable(
+                                account.getAddress().getSchemeSpecificPart()));
+            }
+            holder.imageView.setImageDrawable(PhoneAccountCompat.createIconDrawable(account,
+                    getContext()));
+            return rowView;
+        }
+
+        private class ViewHolder {
+            TextView labelTextView;
+            TextView numberTextView;
+            ImageView imageView;
+        }
+    }
+
+    @Override
+    public void onStop() {
+        if (!mIsSelected && mListener != null) {
+            mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_DISMISSED, null);
+        }
+        super.onStop();
+    }
+}
diff --git a/src/com/android/contacts/interactions/ContactInteractionUtil.java b/src/com/android/contacts/interactions/ContactInteractionUtil.java
index 98d45ee..8ec0547 100644
--- a/src/com/android/contacts/interactions/ContactInteractionUtil.java
+++ b/src/com/android/contacts/interactions/ContactInteractionUtil.java
@@ -26,9 +26,6 @@
 
 import java.util.Calendar;
 
-import com.android.contacts.R;
-
-
 /**
  * Utility methods for interactions and their loaders
  */
@@ -61,8 +58,7 @@
      * compareCalendar.
      * This formats the date based on a few conditions:
      * 1. If the timestamp is today, the time is shown
-     * 2. If the timestamp occurs tomorrow or yesterday, that is displayed
-     * 3. Otherwise {Month Date} format is used
+     * 2. Otherwise show full date and time
      */
     @NeededForTesting
     public static String formatDateStringFromTimestamp(long timestamp, Context context,
@@ -76,19 +72,9 @@
                     interactionCalendar.getTime());
         }
 
-        // Turn compareCalendar to yesterday
-        compareCalendar.add(Calendar.DAY_OF_YEAR, -1);
-        if (compareCalendarDayYear(interactionCalendar, compareCalendar)) {
-            return context.getString(R.string.yesterday);
-        }
-
-        // Turn compareCalendar to tomorrow
-        compareCalendar.add(Calendar.DAY_OF_YEAR, 2);
-        if (compareCalendarDayYear(interactionCalendar, compareCalendar)) {
-            return context.getString(R.string.tomorrow);
-        }
-        return DateUtils.formatDateTime(context, interactionCalendar.getTimeInMillis(),
-                DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR);
+        return DateUtils.formatDateTime(context, timestamp, DateUtils.FORMAT_SHOW_TIME
+                | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY
+                | DateUtils.FORMAT_SHOW_YEAR);
     }
 
     /**
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 75168cd..afaafb7 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -20,6 +20,7 @@
 import android.content.Loader;
 import android.database.Cursor;
 import android.net.Uri;
+import android.provider.ContactsContract.Directory;
 import android.text.TextUtils;
 import android.view.Gravity;
 import android.view.LayoutInflater;
@@ -79,7 +80,9 @@
 
     @Override
     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
-        bindListHeader(data.getCount());
+        if (loader.getId() == Directory.DEFAULT) {
+            bindListHeader(data.getCount());
+        }
         super.onLoadFinished(loader, data);
         if (!isSearchMode() && mCallback != null) {
             mCallback.onLoadFinishedCallback();
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index e918233..c595f23 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -239,7 +239,6 @@
 
     private static final String MIMETYPE_GPLUS_PROFILE =
             "vnd.android.cursor.item/vnd.googleplus.profile";
-    private static final String GPLUS_PROFILE_DATA_5_ADD_TO_CIRCLE = "addtocircle";
     private static final String GPLUS_PROFILE_DATA_5_VIEW_PROFILE = "view";
     private static final String MIMETYPE_HANGOUTS =
             "vnd.android.cursor.item/vnd.googleplus.profile.comm";
@@ -2108,30 +2107,7 @@
                 // Build advanced entry for known 3p types. Otherwise default to ResolveCache icon.
                 switch (mimetype) {
                     case MIMETYPE_GPLUS_PROFILE:
-                        // If a secondDataItem is available, use it to build an entry with
-                        // alternate actions
-                        if (secondDataItem != null) {
-                            icon = res.getDrawable(R.drawable.ic_google_plus_black_24dp);
-                            alternateIcon = res.getDrawable(R.drawable.ic_add_to_circles_black_24);
-                            final GPlusOrHangoutsDataItemModel itemModel =
-                                    new GPlusOrHangoutsDataItemModel(intent, alternateIntent,
-                                            dataItem, secondDataItem, alternateContentDescription,
-                                            header, text, context);
-
-                            populateGPlusOrHangoutsDataItemModel(itemModel);
-                            intent = itemModel.intent;
-                            alternateIntent = itemModel.alternateIntent;
-                            alternateContentDescription = itemModel.alternateContentDescription;
-                            header = itemModel.header;
-                            text = itemModel.text;
-                        } else {
-                            if (GPLUS_PROFILE_DATA_5_ADD_TO_CIRCLE.equals(
-                                    intent.getDataString())) {
-                                icon = res.getDrawable(R.drawable.ic_add_to_circles_black_24);
-                            } else {
-                                icon = res.getDrawable(R.drawable.ic_google_plus_black_24dp);
-                            }
-                        }
+                        icon = res.getDrawable(R.drawable.ic_google_plus_black_24dp);
                         break;
                     case MIMETYPE_HANGOUTS:
                         // If a secondDataItem is available, use it to build an entry with
@@ -2139,12 +2115,12 @@
                         if (secondDataItem != null) {
                             icon = res.getDrawable(R.drawable.ic_hangout_24dp);
                             alternateIcon = res.getDrawable(R.drawable.ic_hangout_video_24dp);
-                            final GPlusOrHangoutsDataItemModel itemModel =
-                                    new GPlusOrHangoutsDataItemModel(intent, alternateIntent,
+                            final HangoutsDataItemModel itemModel =
+                                    new HangoutsDataItemModel(intent, alternateIntent,
                                             dataItem, secondDataItem, alternateContentDescription,
                                             header, text, context);
 
-                            populateGPlusOrHangoutsDataItemModel(itemModel);
+                            populateHangoutsDataItemModel(itemModel);
                             intent = itemModel.intent;
                             alternateIntent = itemModel.alternateIntent;
                             alternateContentDescription = itemModel.alternateContentDescription;
@@ -2216,9 +2192,10 @@
     private List<Entry> dataItemsToEntries(List<DataItem> dataItems,
             MutableString aboutCardTitleOut) {
         // Hangouts and G+ use two data items to create one entry.
-        if (dataItems.get(0).getMimeType().equals(MIMETYPE_GPLUS_PROFILE) ||
-                dataItems.get(0).getMimeType().equals(MIMETYPE_HANGOUTS)) {
-            return gPlusOrHangoutsDataItemsToEntries(dataItems);
+        if (dataItems.get(0).getMimeType().equals(MIMETYPE_GPLUS_PROFILE)) {
+            return gPlusDataItemsToEntries(dataItems);
+        } else if (dataItems.get(0).getMimeType().equals(MIMETYPE_HANGOUTS)) {
+            return hangoutsDataItemsToEntries(dataItems);
         } else {
             final List<Entry> entries = new ArrayList<>();
             for (DataItem dataItem : dataItems) {
@@ -2233,15 +2210,10 @@
     }
 
     /**
-     * G+ and Hangout entries are unique in that a single ExpandingEntryCardView.Entry consists
-     * of two data items. This method attempts to build each entry using the two data items if
-     * they are available. If there are more or less than two data items, a fall back is used
-     * and each data item gets its own entry.
+     * Put the data items into buckets based on the raw contact id
      */
-    private List<Entry> gPlusOrHangoutsDataItemsToEntries(List<DataItem> dataItems) {
-        final List<Entry> entries = new ArrayList<>();
+    private Map<Long, List<DataItem>> dataItemsToBucket(List<DataItem> dataItems) {
         final Map<Long, List<DataItem>> buckets = new HashMap<>();
-        // Put the data items into buckets based on the raw contact id
         for (DataItem dataItem : dataItems) {
             List<DataItem> bucket = buckets.get(dataItem.getRawContactId());
             if (bucket == null) {
@@ -2250,10 +2222,43 @@
             }
             bucket.add(dataItem);
         }
+        return buckets;
+    }
+
+    /**
+     * For G+ entries, a single ExpandingEntryCardView.Entry consists of two data items. This
+     * method use only the View profile to build entry.
+     */
+    private List<Entry> gPlusDataItemsToEntries(List<DataItem> dataItems) {
+        final List<Entry> entries = new ArrayList<>();
+
+        for (List<DataItem> bucket : dataItemsToBucket(dataItems).values()) {
+            for (DataItem dataItem : bucket) {
+                if (GPLUS_PROFILE_DATA_5_VIEW_PROFILE.equals(
+                        dataItem.getContentValues().getAsString(Data.DATA5))) {
+                    final Entry entry = dataItemToEntry(dataItem, /* secondDataItem = */ null,
+                            this, mContactData, /* aboutCardName = */ null);
+                    if (entry != null) {
+                        entries.add(entry);
+                    }
+                }
+            }
+        }
+        return entries;
+    }
+
+    /**
+     * For Hangouts entries, a single ExpandingEntryCardView.Entry consists of two data items. This
+     * method attempts to build each entry using the two data items if they are available. If there
+     * are more or less than two data items, a fall back is used and each data item gets its own
+     * entry.
+     */
+    private List<Entry> hangoutsDataItemsToEntries(List<DataItem> dataItems) {
+        final List<Entry> entries = new ArrayList<>();
 
         // Use the buckets to build entries. If a bucket contains two data items, build the special
         // entry, otherwise fall back to the normal entry.
-        for (List<DataItem> bucket : buckets.values()) {
+        for (List<DataItem> bucket : dataItemsToBucket(dataItems).values()) {
             if (bucket.size() == 2) {
                 // Use the pair to build an entry
                 final Entry entry = dataItemToEntry(bucket.get(0),
@@ -2276,10 +2281,10 @@
     }
 
     /**
-     * Used for statically passing around G+ or Hangouts data items and entry fields to
-     * populateGPlusOrHangoutsDataItemModel.
+     * Used for statically passing around Hangouts data items and entry fields to
+     * populateHangoutsDataItemModel.
      */
-    private static final class GPlusOrHangoutsDataItemModel {
+    private static final class HangoutsDataItemModel {
         public Intent intent;
         public Intent alternateIntent;
         public DataItem dataItem;
@@ -2289,7 +2294,7 @@
         public String text;
         public Context context;
 
-        public GPlusOrHangoutsDataItemModel(Intent intent, Intent alternateIntent, DataItem dataItem,
+        public HangoutsDataItemModel(Intent intent, Intent alternateIntent, DataItem dataItem,
                 DataItem secondDataItem, StringBuilder alternateContentDescription, String header,
                 String text, Context context) {
             this.intent = intent;
@@ -2303,18 +2308,16 @@
         }
     }
 
-    private static void populateGPlusOrHangoutsDataItemModel(
-            GPlusOrHangoutsDataItemModel dataModel) {
+    private static void populateHangoutsDataItemModel(
+            HangoutsDataItemModel dataModel) {
         final Intent secondIntent = new Intent(Intent.ACTION_VIEW);
         secondIntent.setDataAndType(ContentUris.withAppendedId(Data.CONTENT_URI,
                 dataModel.secondDataItem.getId()), dataModel.secondDataItem.getMimeType());
         // There is no guarantee the order the data items come in. Second
         // data item does not necessarily mean it's the alternate.
-        // Hangouts video and Add to circles should be alternate. Swap if needed
+        // Hangouts video should be alternate. Swap if needed
         if (HANGOUTS_DATA_5_VIDEO.equals(
-                dataModel.dataItem.getContentValues().getAsString(Data.DATA5)) ||
-                GPLUS_PROFILE_DATA_5_ADD_TO_CIRCLE.equals(
-                        dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) {
+                dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) {
             dataModel.alternateIntent = dataModel.intent;
             dataModel.alternateContentDescription = new StringBuilder(dataModel.header);
 
@@ -2323,9 +2326,7 @@
                     dataModel.secondDataItem.getDataKind());
             dataModel.text = dataModel.secondDataItem.getDataKind().typeColumn;
         } else if (HANGOUTS_DATA_5_MESSAGE.equals(
-                dataModel.dataItem.getContentValues().getAsString(Data.DATA5)) ||
-                GPLUS_PROFILE_DATA_5_VIEW_PROFILE.equals(
-                        dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) {
+                dataModel.dataItem.getContentValues().getAsString(Data.DATA5))) {
             dataModel.alternateIntent = secondIntent;
             dataModel.alternateContentDescription = new StringBuilder(
                     dataModel.secondDataItem.buildDataStringForDisplay(dataModel.context,
diff --git a/tests/Android.mk b/tests/Android.mk
index 4fd947c..48a00f4 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -7,11 +7,8 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
 
-src_dirs := src \
-    ../../ContactsCommon/TestCommon/src
-
-# Include all test java files.
-LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, res)
 
 LOCAL_PACKAGE_NAME := ContactsTests
 
@@ -20,4 +17,11 @@
 LOCAL_SDK_VERSION := current
 LOCAL_MIN_SDK_VERSION := 21
 
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    mockito-target
+
+LOCAL_AAPT_FLAGS := \
+    --auto-add-overlay \
+    --extra-packages com.android.contacts.common.tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/res-common/drawable/android.jpg b/tests/res-common/drawable/android.jpg
new file mode 100644
index 0000000..95693b2
--- /dev/null
+++ b/tests/res-common/drawable/android.jpg
Binary files differ
diff --git a/tests/res-common/drawable/default_icon.png b/tests/res-common/drawable/default_icon.png
new file mode 100644
index 0000000..cea0eb3
--- /dev/null
+++ b/tests/res-common/drawable/default_icon.png
Binary files differ
diff --git a/tests/res-common/drawable/ic_contact_picture.png b/tests/res-common/drawable/ic_contact_picture.png
new file mode 100644
index 0000000..6876777
--- /dev/null
+++ b/tests/res-common/drawable/ic_contact_picture.png
Binary files differ
diff --git a/tests/res-common/drawable/phone_icon.png b/tests/res-common/drawable/phone_icon.png
new file mode 100644
index 0000000..4e613ec
--- /dev/null
+++ b/tests/res-common/drawable/phone_icon.png
Binary files differ
diff --git a/tests/res-common/values/donottranslate_strings.xml b/tests/res-common/values/donottranslate_strings.xml
new file mode 100644
index 0000000..6c8527f
--- /dev/null
+++ b/tests/res-common/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-common/xml/contacts_fallback.xml b/tests/res-common/xml/contacts_fallback.xml
new file mode 100644
index 0000000..7034d5e
--- /dev/null
+++ b/tests/res-common/xml/contacts_fallback.xml
@@ -0,0 +1,97 @@
+<?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" />
+        <DataKind kind="group_membership" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res-common/xml/contacts_readonly.xml b/tests/res-common/xml/contacts_readonly.xml
new file mode 100644
index 0000000..df8d9c0
--- /dev/null
+++ b/tests/res-common/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-common/xml/iconset.xml b/tests/res-common/xml/iconset.xml
new file mode 100644
index 0000000..d1207e7
--- /dev/null
+++ b/tests/res-common/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-common/xml/missing_contacts_base.xml b/tests/res-common/xml/missing_contacts_base.xml
new file mode 100644
index 0000000..2c9aa6d
--- /dev/null
+++ b/tests/res-common/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-common/xml/missing_contacts_name.xml b/tests/res-common/xml/missing_contacts_name.xml
new file mode 100644
index 0000000..1ac26be
--- /dev/null
+++ b/tests/res-common/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-common/xml/missing_contacts_name_attr1.xml b/tests/res-common/xml/missing_contacts_name_attr1.xml
new file mode 100644
index 0000000..b7b0f19
--- /dev/null
+++ b/tests/res-common/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-common/xml/missing_contacts_name_attr2.xml b/tests/res-common/xml/missing_contacts_name_attr2.xml
new file mode 100644
index 0000000..41be9e8
--- /dev/null
+++ b/tests/res-common/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-common/xml/missing_contacts_name_attr3.xml b/tests/res-common/xml/missing_contacts_name_attr3.xml
new file mode 100644
index 0000000..e639a76
--- /dev/null
+++ b/tests/res-common/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-common/xml/missing_contacts_name_attr4.xml b/tests/res-common/xml/missing_contacts_name_attr4.xml
new file mode 100644
index 0000000..b42cdcd
--- /dev/null
+++ b/tests/res-common/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-common/xml/missing_contacts_name_attr5.xml b/tests/res-common/xml/missing_contacts_name_attr5.xml
new file mode 100644
index 0000000..3778d2f
--- /dev/null
+++ b/tests/res-common/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-common/xml/missing_contacts_name_attr6.xml b/tests/res-common/xml/missing_contacts_name_attr6.xml
new file mode 100644
index 0000000..b3a3411
--- /dev/null
+++ b/tests/res-common/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-common/xml/missing_contacts_name_attr7.xml b/tests/res-common/xml/missing_contacts_name_attr7.xml
new file mode 100644
index 0000000..c87e4f1
--- /dev/null
+++ b/tests/res-common/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-common/xml/missing_contacts_photo.xml b/tests/res-common/xml/missing_contacts_photo.xml
new file mode 100644
index 0000000..87f4fc6
--- /dev/null
+++ b/tests/res-common/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-common/xml/test_basic_contacts.xml b/tests/res-common/xml/test_basic_contacts.xml
new file mode 100644
index 0000000..0047204
--- /dev/null
+++ b/tests/res-common/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-common/xml/test_basic_syncadapter.xml b/tests/res-common/xml/test_basic_syncadapter.xml
new file mode 100644
index 0000000..fecc0eb
--- /dev/null
+++ b/tests/res-common/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/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
index 8f43dbf..1528112 100644
--- a/tests/res/values/donottranslate_strings.xml
+++ b/tests/res/values/donottranslate_strings.xml
@@ -105,4 +105,6 @@
     <string name="attribution_twitter">Twitter</string>
 
     <string name="authenticator_basic_label">Test adapter</string>
+
+    <string name="test_string">TEST STRING</string>
 </resources>
diff --git a/tests/res/xml/contacts_fallback.xml b/tests/res/xml/contacts_fallback.xml
new file mode 100644
index 0000000..7034d5e
--- /dev/null
+++ b/tests/res/xml/contacts_fallback.xml
@@ -0,0 +1,97 @@
+<?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" />
+        <DataKind kind="group_membership" 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
index b9e419d..d1207e7 100644
--- a/tests/res/xml/iconset.xml
+++ b/tests/res/xml/iconset.xml
@@ -1,25 +1,24 @@
 <?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
-  -->
+<!-- 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"
+    <icon android:mimeType="vnd.android.cursor.item/phone" 
         android:icon="@drawable/phone_icon" />
 
-</icon-set>
+</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/ContactsUtilsTests.java b/tests/src/com/android/contacts/common/ContactsUtilsTests.java
new file mode 100644
index 0000000..a209fb2
--- /dev/null
+++ b/tests/src/com/android/contacts/common/ContactsUtilsTests.java
@@ -0,0 +1,188 @@
+/*
+ * 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;
+
+import android.content.ContentValues;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Pair;
+
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.android.contacts.common.model.dataitem.EmailDataItem;
+import com.android.contacts.common.model.dataitem.ImDataItem;
+
+/**
+ * Tests for {@link ContactsUtils}.
+ */
+@SmallTest
+public class ContactsUtilsTests extends AndroidTestCase {
+
+    private static final String TEST_ADDRESS = "user@example.org";
+    private static final String TEST_PROTOCOL = "prot%col";
+
+    public void testIsGraphicNull() throws Exception {
+        assertFalse(ContactsUtils.isGraphic(null));
+    }
+
+    public void testIsGraphicEmpty() throws Exception {
+        assertFalse(ContactsUtils.isGraphic(""));
+    }
+
+    public void testIsGraphicSpaces() throws Exception {
+        assertFalse(ContactsUtils.isGraphic("  "));
+    }
+
+    public void testIsGraphicPunctuation() throws Exception {
+        assertTrue(ContactsUtils.isGraphic("."));
+    }
+
+    public void testAreObjectsEqual() throws Exception {
+        assertTrue("null:null", ContactsUtils.areObjectsEqual(null, null));
+        assertTrue("1:1", ContactsUtils.areObjectsEqual(1, 1));
+
+        assertFalse("null:1", ContactsUtils.areObjectsEqual(null, 1));
+        assertFalse("1:null", ContactsUtils.areObjectsEqual(1, null));
+        assertFalse("1:2", ContactsUtils.areObjectsEqual(1, 2));
+    }
+
+    public void testAreIntentActionEqual() throws Exception {
+        assertTrue("1", ContactsUtils.areIntentActionEqual(null, null));
+        assertTrue("1", ContactsUtils.areIntentActionEqual(new Intent("a"), new Intent("a")));
+
+        assertFalse("11", ContactsUtils.areIntentActionEqual(new Intent("a"), null));
+        assertFalse("12", ContactsUtils.areIntentActionEqual(null, new Intent("a")));
+
+        assertFalse("21", ContactsUtils.areIntentActionEqual(new Intent("a"), new Intent()));
+        assertFalse("22", ContactsUtils.areIntentActionEqual(new Intent(), new Intent("b")));
+        assertFalse("23", ContactsUtils.areIntentActionEqual(new Intent("a"), new Intent("b")));
+    }
+
+    public void testImIntentCustom() throws Exception {
+        // Custom IM types have encoded authority. We send the imto Intent here, because
+        // legacy third party apps might not accept xmpp yet
+        final ContentValues values = new ContentValues();
+        values.put(Im.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        values.put(Im.TYPE, Im.TYPE_HOME);
+        values.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM);
+        values.put(Im.CUSTOM_PROTOCOL, TEST_PROTOCOL);
+        values.put(Im.DATA, TEST_ADDRESS);
+        final ImDataItem im = (ImDataItem) DataItem.createFrom(values);
+
+        final Pair<Intent, Intent> intents = ContactsUtils.buildImIntent(getContext(), im);
+        final Intent imIntent = intents.first;
+
+        assertEquals(Intent.ACTION_SENDTO, imIntent.getAction());
+
+        final Uri data = imIntent.getData();
+        assertEquals("imto", data.getScheme());
+        assertEquals(TEST_PROTOCOL, data.getAuthority());
+        assertEquals(TEST_ADDRESS, data.getPathSegments().get(0));
+
+        assertNull(intents.second);
+    }
+
+    public void testImIntent() throws Exception {
+        // Test GTalk XMPP URI. No chat capabilities provided
+        final ContentValues values = new ContentValues();
+        values.put(Im.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        values.put(Im.TYPE, Im.TYPE_HOME);
+        values.put(Im.PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
+        values.put(Im.DATA, TEST_ADDRESS);
+        final ImDataItem im = (ImDataItem) DataItem.createFrom(values);
+
+        final Pair<Intent, Intent> intents = ContactsUtils.buildImIntent(getContext(), im);
+        final Intent imIntent = intents.first;
+
+        assertEquals(Intent.ACTION_SENDTO, imIntent.getAction());
+        assertEquals("xmpp:" + TEST_ADDRESS + "?message", imIntent.getData().toString());
+
+        assertNull(intents.second);
+    }
+
+    public void testImIntentWithAudio() throws Exception {
+        // Test GTalk XMPP URI. Audio chat capabilities provided
+        final ContentValues values = new ContentValues();
+        values.put(Im.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        values.put(Im.TYPE, Im.TYPE_HOME);
+        values.put(Im.PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
+        values.put(Im.DATA, TEST_ADDRESS);
+        values.put(Im.CHAT_CAPABILITY, Im.CAPABILITY_HAS_VOICE | Im.CAPABILITY_HAS_VIDEO);
+        final ImDataItem im = (ImDataItem) DataItem.createFrom(values);
+
+        final Pair<Intent, Intent> intents = ContactsUtils.buildImIntent(getContext(), im);
+        final Intent imIntent = intents.first;
+
+        assertEquals(Intent.ACTION_SENDTO, imIntent.getAction());
+        assertEquals("xmpp:" + TEST_ADDRESS + "?message", imIntent.getData().toString());
+
+        final Intent secondaryIntent = intents.second;
+        assertEquals(Intent.ACTION_SENDTO, secondaryIntent.getAction());
+        assertEquals("xmpp:" + TEST_ADDRESS + "?call", secondaryIntent.getData().toString());
+    }
+
+    public void testImIntentWithVideo() throws Exception {
+        // Test GTalk XMPP URI. Video chat capabilities provided
+        final ContentValues values = new ContentValues();
+        values.put(Im.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        values.put(Im.TYPE, Im.TYPE_HOME);
+        values.put(Im.PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
+        values.put(Im.DATA, TEST_ADDRESS);
+        values.put(Im.CHAT_CAPABILITY, Im.CAPABILITY_HAS_VOICE | Im.CAPABILITY_HAS_VIDEO |
+                Im.CAPABILITY_HAS_VOICE);
+        final ImDataItem im = (ImDataItem) DataItem.createFrom(values);
+
+        final Pair<Intent, Intent> intents = ContactsUtils.buildImIntent(getContext(), im);
+        final Intent imIntent = intents.first;
+
+        assertEquals(Intent.ACTION_SENDTO, imIntent.getAction());
+        assertEquals("xmpp:" + TEST_ADDRESS + "?message", imIntent.getData().toString());
+
+        final Intent secondaryIntent = intents.second;
+        assertEquals(Intent.ACTION_SENDTO, secondaryIntent.getAction());
+        assertEquals("xmpp:" + TEST_ADDRESS + "?call", secondaryIntent.getData().toString());
+    }
+
+
+    public void testImEmailIntent() throws Exception {
+        // Email addresses are treated as Google Talk entries
+        // This test only tests the VIDEO+CAMERA case. The other cases have been addressed by the
+        // Im tests
+        final ContentValues values = new ContentValues();
+        values.put(Email.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        values.put(Email.TYPE, Email.TYPE_HOME);
+        values.put(Email.DATA, TEST_ADDRESS);
+        values.put(Email.CHAT_CAPABILITY, Im.CAPABILITY_HAS_VOICE | Im.CAPABILITY_HAS_VIDEO |
+                Im.CAPABILITY_HAS_VOICE);
+        final ImDataItem im = ImDataItem.createFromEmail(
+                (EmailDataItem) DataItem.createFrom(values));
+
+        final Pair<Intent, Intent> intents = ContactsUtils.buildImIntent(getContext(), im);
+        final Intent imIntent = intents.first;
+
+        assertEquals(Intent.ACTION_SENDTO, imIntent.getAction());
+        assertEquals("xmpp:" + TEST_ADDRESS + "?message", imIntent.getData().toString());
+
+        final Intent secondaryIntent = intents.second;
+        assertEquals(Intent.ACTION_SENDTO, secondaryIntent.getAction());
+        assertEquals("xmpp:" + TEST_ADDRESS + "?call", secondaryIntent.getData().toString());
+    }
+}
diff --git a/tests/src/com/android/contacts/common/MoreContactUtilsTest.java b/tests/src/com/android/contacts/common/MoreContactUtilsTest.java
new file mode 100644
index 0000000..8d74455
--- /dev/null
+++ b/tests/src/com/android/contacts/common/MoreContactUtilsTest.java
@@ -0,0 +1,176 @@
+package com.android.contacts.common;
+
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for MoreContactsUtils.
+ */
+@SmallTest
+public class MoreContactUtilsTest extends TestCase {
+
+    public void testShouldCollapse() throws Exception {
+        assertCollapses("1", true, null, null, null, null);
+        assertCollapses("2", true, "a", "b", "a", "b");
+
+        assertCollapses("11", false, "a", null, null, null);
+        assertCollapses("12", false, null, "a", null, null);
+        assertCollapses("13", false, null, null, "a", null);
+        assertCollapses("14", false, null, null, null, "a");
+
+        assertCollapses("21", false, "a", "b", null, null);
+        assertCollapses("22", false, "a", "b", "a", null);
+        assertCollapses("23", false, "a", "b", null, "b");
+        assertCollapses("24", false, "a", "b", "a", "x");
+        assertCollapses("25", false, "a", "b", "x", "b");
+
+        assertCollapses("31", false, null, null, "a", "b");
+        assertCollapses("32", false, "a", null, "a", "b");
+        assertCollapses("33", false, null, "b", "a", "b");
+        assertCollapses("34", false, "a", "x", "a", "b");
+        assertCollapses("35", false, "x", "b", "a", "b");
+
+        assertCollapses("41", true, Phone.CONTENT_ITEM_TYPE, null, Phone.CONTENT_ITEM_TYPE, null);
+        assertCollapses("42", true, Phone.CONTENT_ITEM_TYPE, "1", Phone.CONTENT_ITEM_TYPE, "1");
+
+        assertCollapses("51", false, Phone.CONTENT_ITEM_TYPE, "1", Phone.CONTENT_ITEM_TYPE, "2");
+        assertCollapses("52", false, Phone.CONTENT_ITEM_TYPE, "1", Phone.CONTENT_ITEM_TYPE, null);
+        assertCollapses("53", false, Phone.CONTENT_ITEM_TYPE, null, Phone.CONTENT_ITEM_TYPE, "2");
+
+        // Test phone numbers
+        assertCollapses("60", true, Phone.CONTENT_ITEM_TYPE, "1234567", Phone.CONTENT_ITEM_TYPE,
+                "1234567");
+        assertCollapses("61", false, Phone.CONTENT_ITEM_TYPE, "1234567", Phone.CONTENT_ITEM_TYPE,
+                "1234568");
+        assertCollapses("62", true, Phone.CONTENT_ITEM_TYPE, "1234567;0", Phone.CONTENT_ITEM_TYPE,
+                "1234567;0");
+        assertCollapses("63", false, Phone.CONTENT_ITEM_TYPE, "1234567;89321",
+                Phone.CONTENT_ITEM_TYPE, "1234567;89322");
+        assertCollapses("64", true, Phone.CONTENT_ITEM_TYPE, "1234567;89321",
+                Phone.CONTENT_ITEM_TYPE, "1234567;89321");
+        assertCollapses("65", false, Phone.CONTENT_ITEM_TYPE, "1234567;0111111111",
+                Phone.CONTENT_ITEM_TYPE, "1234567;");
+        assertCollapses("66", false, Phone.CONTENT_ITEM_TYPE, "12345675426;91970xxxxx",
+                Phone.CONTENT_ITEM_TYPE, "12345675426");
+        assertCollapses("67", false, Phone.CONTENT_ITEM_TYPE, "12345675426;23456xxxxx",
+                Phone.CONTENT_ITEM_TYPE, "12345675426;234567xxxx");
+        assertCollapses("68", true, Phone.CONTENT_ITEM_TYPE, "1234567;1234567;1234567",
+                Phone.CONTENT_ITEM_TYPE, "1234567;1234567;1234567");
+        assertCollapses("69", false, Phone.CONTENT_ITEM_TYPE, "1234567;1234567;1234567",
+                Phone.CONTENT_ITEM_TYPE, "1234567;1234567");
+
+        // test some numbers with country and area code
+        assertCollapses("70", true, Phone.CONTENT_ITEM_TYPE, "+49 (89) 12345678",
+                Phone.CONTENT_ITEM_TYPE, "+49 (89) 12345678");
+        assertCollapses("71", true, Phone.CONTENT_ITEM_TYPE, "+49 (89) 12345678",
+                Phone.CONTENT_ITEM_TYPE, "+49 (89)12345678");
+        assertCollapses("72", true, Phone.CONTENT_ITEM_TYPE, "+49 (8092) 1234",
+                Phone.CONTENT_ITEM_TYPE, "+49 (8092)1234");
+        assertCollapses("73", false, Phone.CONTENT_ITEM_TYPE, "0049 (8092) 1234",
+                Phone.CONTENT_ITEM_TYPE, "+49/80921234");
+        assertCollapses("74", false, Phone.CONTENT_ITEM_TYPE, "+49 (89) 12345678",
+                Phone.CONTENT_ITEM_TYPE, "+49 (89) 12345679");
+
+        // test special handling of collapsing country code for NANP region only
+        // This is non symmetrical, because we prefer the number with the +1.
+        assertEquals("100", true, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "+1 (415) 555-1212", Phone.CONTENT_ITEM_TYPE, "(415) 555-1212"));
+        assertEquals("101", true, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "+14155551212", Phone.CONTENT_ITEM_TYPE, "4155551212"));
+        assertEquals("102", false, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "(415) 555-1212", Phone.CONTENT_ITEM_TYPE, "+1 (415) 555-1212"));
+        assertEquals("103", false, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "4155551212", Phone.CONTENT_ITEM_TYPE, "+14155551212"));
+        // Require explicit +1 country code declaration to collapse
+        assertEquals("104", false, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "1-415-555-1212", Phone.CONTENT_ITEM_TYPE, "415-555-1212"));
+        assertEquals("105", false, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "14155551212", Phone.CONTENT_ITEM_TYPE, "4155551212"));
+        assertEquals("106", false, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "+1 (415) 555-1212", Phone.CONTENT_ITEM_TYPE, " 1 (415) 555-1212"));
+        assertEquals("107", false, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "+14155551212", Phone.CONTENT_ITEM_TYPE, " 14155551212"));
+        assertEquals("108", false, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "1 (415) 555-1212", Phone.CONTENT_ITEM_TYPE, "+1 (415) 555-1212"));
+        assertEquals("109", false, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "14155551212", Phone.CONTENT_ITEM_TYPE, "+14155551212"));
+
+        // test some numbers with wait symbol and area code
+        assertCollapses("200", true, Phone.CONTENT_ITEM_TYPE, "+49 (8092) 1234;89321",
+                Phone.CONTENT_ITEM_TYPE, "+49/80921234;89321");
+        assertCollapses("201", false, Phone.CONTENT_ITEM_TYPE, "+49 (8092) 1234;89321",
+                Phone.CONTENT_ITEM_TYPE, "+49/80921235;89321");
+        assertCollapses("202", false, Phone.CONTENT_ITEM_TYPE, "+49 (8092) 1234;89322",
+                Phone.CONTENT_ITEM_TYPE, "+49/80921234;89321");
+        assertCollapses("203", true, Phone.CONTENT_ITEM_TYPE, "1234567;+49 (8092) 1234",
+                Phone.CONTENT_ITEM_TYPE, "1234567;+49/80921234");
+
+        assertCollapses("300", true, Phone.CONTENT_ITEM_TYPE, "", Phone.CONTENT_ITEM_TYPE, "");
+
+        assertCollapses("301", false, Phone.CONTENT_ITEM_TYPE, "1", Phone.CONTENT_ITEM_TYPE, "");
+
+        assertCollapses("302", false, Phone.CONTENT_ITEM_TYPE, "", Phone.CONTENT_ITEM_TYPE, "1");
+
+        assertCollapses("303", true, Phone.CONTENT_ITEM_TYPE, "---", Phone.CONTENT_ITEM_TYPE, "---");
+
+        assertCollapses("304", false, Phone.CONTENT_ITEM_TYPE, "1-/().", Phone.CONTENT_ITEM_TYPE,
+                "--$%1");
+
+        // Test numbers using keypad letters. This is non-symmetrical, because we prefer
+        // the version with letters.
+        assertEquals("400", true, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "abcdefghijklmnopqrstuvwxyz", Phone.CONTENT_ITEM_TYPE,
+                "22233344455566677778889999"));
+        assertEquals("401", false, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "22233344455566677778889999", Phone.CONTENT_ITEM_TYPE,
+                "abcdefghijklmnopqrstuvwxyz"));
+
+        assertCollapses("402", false, Phone.CONTENT_ITEM_TYPE, "1;2", Phone.CONTENT_ITEM_TYPE,
+                "12");
+
+        assertCollapses("403", false, Phone.CONTENT_ITEM_TYPE, "1,2", Phone.CONTENT_ITEM_TYPE,
+                "12");
+    }
+
+    public void testShouldCollapse_collapsesSameNumberWithDifferentFormats() {
+        assertEquals("1", true, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "555-1212", Phone.CONTENT_ITEM_TYPE, "5551212"));
+        assertEquals("1", true, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "415-555-1212", Phone.CONTENT_ITEM_TYPE, "(415) 555-1212"));
+        assertEquals("2", true, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "4155551212", Phone.CONTENT_ITEM_TYPE, "(415) 555-1212"));
+        assertEquals("3", true, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "1-415-555-1212", Phone.CONTENT_ITEM_TYPE, "1 (415) 555-1212"));
+        assertEquals("4", true, MoreContactUtils.shouldCollapse(Phone.CONTENT_ITEM_TYPE,
+                "14155551212", Phone.CONTENT_ITEM_TYPE, "1 (415) 555-1212"));
+    }
+
+    private void assertCollapses(String message, boolean expected, CharSequence mimetype1,
+            CharSequence data1, CharSequence mimetype2, CharSequence data2) {
+        assertEquals(message, expected, MoreContactUtils.shouldCollapse(mimetype1, data1, mimetype2,
+                data2));
+        assertEquals(message, expected, MoreContactUtils.shouldCollapse(mimetype2, data2, mimetype1,
+                data1));
+
+        // If data1 and data2 are the same instance, make sure the same test passes with different
+        // instances.
+        if (data1 == data2 && data1 != null) {
+            // Create a different instance
+            final CharSequence data2_newref = new StringBuilder(data2).append("").toString();
+
+            if (data1 == data2_newref) {
+                // In some cases no matter what we do the runtime reuses the same instance, so
+                // we can't do the "different instance" test.
+                return;
+            }
+
+            // we have two different instances, now make sure we get the same result as before
+            assertEquals(message, expected, MoreContactUtils.shouldCollapse(mimetype1, data1,
+                    mimetype2, data2_newref));
+            assertEquals(message, expected, MoreContactUtils.shouldCollapse(mimetype2, data2_newref,
+                    mimetype1, data1));
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/common/RawContactDeltaListTests.java b/tests/src/com/android/contacts/common/RawContactDeltaListTests.java
new file mode 100644
index 0000000..77acb98
--- /dev/null
+++ b/tests/src/com/android/contacts/common/RawContactDeltaListTests.java
@@ -0,0 +1,608 @@
+/*
+ * 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;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.contacts.common.RawContactModifierTests.MockContactsSource;
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.CPOWrapper;
+import com.android.contacts.common.model.RawContact;
+import com.android.contacts.common.model.RawContactDelta;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.RawContactDeltaList;
+import com.android.contacts.common.model.RawContactModifier;
+import com.android.contacts.common.model.account.AccountType;
+import com.google.common.collect.Lists;
+
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Tests for {@link RawContactDeltaList} which focus on "diff" operations that should
+ * create {@link AggregationExceptions} in certain cases.
+ */
+@LargeTest
+public class RawContactDeltaListTests extends AndroidTestCase {
+    public static final String TAG = RawContactDeltaListTests.class.getSimpleName();
+
+    // From android.content.ContentProviderOperation
+    public static final int TYPE_INSERT = 1;
+    public static final int TYPE_UPDATE = 2;
+    public static final int TYPE_DELETE = 3;
+    public static final int TYPE_ASSERT = 4;
+
+    private static final long CONTACT_FIRST = 1;
+    private static final long CONTACT_SECOND = 2;
+
+    public static final long CONTACT_BOB = 10;
+    public static final long CONTACT_MARY = 11;
+
+    public static final long PHONE_RED = 20;
+    public static final long PHONE_GREEN = 21;
+    public static final long PHONE_BLUE = 22;
+
+    public static final long EMAIL_YELLOW = 25;
+
+    public static final long VER_FIRST = 100;
+    public static final long VER_SECOND = 200;
+
+    public static final String TEST_PHONE = "555-1212";
+    public static final String TEST_ACCOUNT = "org.example.test";
+
+    public RawContactDeltaListTests() {
+        super();
+    }
+
+    @Override
+    public void setUp() {
+        mContext = getContext();
+    }
+
+    /**
+     * Build a {@link AccountType} that has various odd constraints for
+     * testing purposes.
+     */
+    protected AccountType getAccountType() {
+        return new MockContactsSource();
+    }
+
+    static ContentValues getValues(ContentProviderOperation operation)
+            throws NoSuchFieldException, IllegalAccessException {
+        final Field field = ContentProviderOperation.class.getDeclaredField("mValues");
+        field.setAccessible(true);
+        return (ContentValues) field.get(operation);
+    }
+
+    static RawContactDelta getUpdate(Context context, long rawContactId) {
+        final RawContact before = RawContactDeltaTests.getRawContact(context, rawContactId,
+                RawContactDeltaTests.TEST_PHONE_ID);
+        return RawContactDelta.fromBefore(before);
+    }
+
+    static RawContactDelta getInsert() {
+        final ContentValues after = new ContentValues();
+        after.put(RawContacts.ACCOUNT_NAME, RawContactDeltaTests.TEST_ACCOUNT_NAME);
+        after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+        final ValuesDelta values = ValuesDelta.fromAfter(after);
+        return new RawContactDelta(values);
+    }
+
+    static RawContactDeltaList buildSet(RawContactDelta... deltas) {
+        final RawContactDeltaList set = new RawContactDeltaList();
+        Collections.addAll(set, deltas);
+        return set;
+    }
+
+    static RawContactDelta buildBeforeEntity(Context context, long rawContactId, long version,
+            ContentValues... entries) {
+        // Build an existing contact read from database
+        final ContentValues contact = new ContentValues();
+        contact.put(RawContacts.VERSION, version);
+        contact.put(RawContacts._ID, rawContactId);
+        final RawContact before = new RawContact(contact);
+        for (ContentValues entry : entries) {
+            before.addDataItemValues(entry);
+        }
+        return RawContactDelta.fromBefore(before);
+    }
+
+    static RawContactDelta buildAfterEntity(ContentValues... entries) {
+        // Build an existing contact read from database
+        final ContentValues contact = new ContentValues();
+        contact.put(RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT);
+        final RawContactDelta after = new RawContactDelta(ValuesDelta.fromAfter(contact));
+        for (ContentValues entry : entries) {
+            after.addEntry(ValuesDelta.fromAfter(entry));
+        }
+        return after;
+    }
+
+    static ContentValues buildPhone(long phoneId) {
+        return buildPhone(phoneId, Long.toString(phoneId));
+    }
+
+    static ContentValues buildPhone(long phoneId, String value) {
+        final ContentValues values = new ContentValues();
+        values.put(Data._ID, phoneId);
+        values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        values.put(Phone.NUMBER, value);
+        values.put(Phone.TYPE, Phone.TYPE_HOME);
+        return values;
+    }
+
+    static ContentValues buildEmail(long emailId) {
+        final ContentValues values = new ContentValues();
+        values.put(Data._ID, emailId);
+        values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        values.put(Email.DATA, Long.toString(emailId));
+        values.put(Email.TYPE, Email.TYPE_HOME);
+        return values;
+    }
+
+    static void insertPhone(RawContactDeltaList set, long rawContactId, ContentValues values) {
+        final RawContactDelta match = set.getByRawContactId(rawContactId);
+        match.addEntry(ValuesDelta.fromAfter(values));
+    }
+
+    static ValuesDelta getPhone(RawContactDeltaList set, long rawContactId, long dataId) {
+        final RawContactDelta match = set.getByRawContactId(rawContactId);
+        return match.getEntry(dataId);
+    }
+
+    static void assertDiffPattern(RawContactDelta delta, CPOWrapper... pattern) {
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        delta.buildAssertWrapper(diff);
+        delta.buildDiffWrapper(diff);
+        assertDiffPattern(diff, pattern);
+    }
+
+    static void assertDiffPattern(RawContactDeltaList set, CPOWrapper... pattern) {
+        assertDiffPattern(set.buildDiffWrapper(), pattern);
+    }
+
+    static void assertDiffPattern(ArrayList<CPOWrapper> diff, CPOWrapper... pattern) {
+        assertEquals("Unexpected operations", pattern.length, diff.size());
+        for (int i = 0; i < pattern.length; i++) {
+            final CPOWrapper expected = pattern[i];
+            final CPOWrapper found = diff.get(i);
+
+            assertEquals("Unexpected uri",
+                    expected.getOperation().getUri(), found.getOperation().getUri());
+
+            final String expectedType = getTypeString(expected);
+            final String foundType = getTypeString(found);
+            assertEquals("Unexpected type", expectedType, foundType);
+
+            if (CompatUtils.isDeleteCompat(expected)) continue;
+
+            try {
+                final ContentValues expectedValues = getValues(expected.getOperation());
+                final ContentValues foundValues = getValues(found.getOperation());
+
+                expectedValues.remove(BaseColumns._ID);
+                foundValues.remove(BaseColumns._ID);
+
+                assertEquals("Unexpected values", expectedValues, foundValues);
+            } catch (NoSuchFieldException e) {
+                fail(e.toString());
+            } catch (IllegalAccessException e) {
+                fail(e.toString());
+            }
+        }
+    }
+
+    static String getTypeString(CPOWrapper cpoWrapper) {
+        if (CompatUtils.isAssertQueryCompat(cpoWrapper)) {
+            return "TYPE_ASSERT";
+        } else if (CompatUtils.isInsertCompat(cpoWrapper)) {
+            return "TYPE_INSERT";
+        } else if (CompatUtils.isUpdateCompat(cpoWrapper)) {
+            return "TYPE_UPDATE";
+        } else if (CompatUtils.isDeleteCompat(cpoWrapper)) {
+            return "TYPE_DELETE";
+        }
+        return "TYPE_UNKNOWN";
+    }
+
+    static CPOWrapper buildAssertVersion(long version) {
+        final ContentValues values = new ContentValues();
+        values.put(RawContacts.VERSION, version);
+        return buildCPOWrapper(RawContacts.CONTENT_URI, TYPE_ASSERT, values);
+    }
+
+    static CPOWrapper buildAggregationModeUpdate(int mode) {
+        final ContentValues values = new ContentValues();
+        values.put(RawContacts.AGGREGATION_MODE, mode);
+        return buildCPOWrapper(RawContacts.CONTENT_URI, TYPE_UPDATE, values);
+    }
+
+    static CPOWrapper buildUpdateAggregationSuspended() {
+        return buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_SUSPENDED);
+    }
+
+    static CPOWrapper buildUpdateAggregationDefault() {
+        return buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT);
+    }
+
+    static CPOWrapper buildUpdateAggregationKeepTogether(long rawContactId) {
+        final ContentValues values = new ContentValues();
+        values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
+        values.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
+        return buildCPOWrapper(AggregationExceptions.CONTENT_URI, TYPE_UPDATE, values);
+    }
+
+    static ContentValues buildDataInsert(ValuesDelta values, long rawContactId) {
+        final ContentValues insertValues = values.getCompleteValues();
+        insertValues.put(Data.RAW_CONTACT_ID, rawContactId);
+        return insertValues;
+    }
+
+    static CPOWrapper buildDelete(Uri uri) {
+        return buildCPOWrapper(uri, TYPE_DELETE, (ContentValues) null);
+    }
+
+    static ContentProviderOperation buildOper(Uri uri, int type, ValuesDelta values) {
+        return buildOper(uri, type, values.getCompleteValues());
+    }
+
+    static ContentProviderOperation buildOper(Uri uri, int type, ContentValues values) {
+        switch (type) {
+            case TYPE_ASSERT:
+                return ContentProviderOperation.newAssertQuery(uri).withValues(values).build();
+            case TYPE_INSERT:
+                return ContentProviderOperation.newInsert(uri).withValues(values).build();
+            case TYPE_UPDATE:
+                return ContentProviderOperation.newUpdate(uri).withValues(values).build();
+            case TYPE_DELETE:
+                return ContentProviderOperation.newDelete(uri).build();
+        }
+        return null;
+    }
+
+    static CPOWrapper buildCPOWrapper(Uri uri, int type, ContentValues values) {
+        if (type == TYPE_ASSERT || type == TYPE_INSERT || type == TYPE_UPDATE
+                || type == TYPE_DELETE) {
+            return new CPOWrapper(buildOper(uri, type, values), type);
+        }
+        return null;
+    }
+
+    static Long getVersion(RawContactDeltaList set, Long rawContactId) {
+        return set.getByRawContactId(rawContactId).getValues().getAsLong(RawContacts.VERSION);
+    }
+
+    /**
+     * Count number of {@link AggregationExceptions} updates contained in the
+     * given list of {@link CPOWrapper}.
+     */
+    static int countExceptionUpdates(ArrayList<CPOWrapper> diff) {
+        int updateCount = 0;
+        for (CPOWrapper cpoWrapper : diff) {
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            if (AggregationExceptions.CONTENT_URI.equals(oper.getUri())
+                    && CompatUtils.isUpdateCompat(cpoWrapper)) {
+                updateCount++;
+            }
+        }
+        return updateCount;
+    }
+
+    public void testInsert() {
+        final RawContactDelta insert = getInsert();
+        final RawContactDeltaList set = buildSet(insert);
+
+        // Inserting single shouldn't create rules
+        final ArrayList<CPOWrapper> diff = set.buildDiffWrapper();
+        final int exceptionCount = countExceptionUpdates(diff);
+        assertEquals("Unexpected exception updates", 0, exceptionCount);
+    }
+
+    public void testUpdateUpdate() {
+        final RawContactDelta updateFirst = getUpdate(mContext, CONTACT_FIRST);
+        final RawContactDelta updateSecond = getUpdate(mContext, CONTACT_SECOND);
+        final RawContactDeltaList set = buildSet(updateFirst, updateSecond);
+
+        // Updating two existing shouldn't create rules
+        final ArrayList<CPOWrapper> diff = set.buildDiffWrapper();
+        final int exceptionCount = countExceptionUpdates(diff);
+        assertEquals("Unexpected exception updates", 0, exceptionCount);
+    }
+
+    public void testUpdateInsert() {
+        final RawContactDelta update = getUpdate(mContext, CONTACT_FIRST);
+        final RawContactDelta insert = getInsert();
+        final RawContactDeltaList set = buildSet(update, insert);
+
+        // New insert should only create one rule
+        final ArrayList<CPOWrapper> diff = set.buildDiffWrapper();
+        final int exceptionCount = countExceptionUpdates(diff);
+        assertEquals("Unexpected exception updates", 1, exceptionCount);
+    }
+
+    public void testInsertUpdateInsert() {
+        final RawContactDelta insertFirst = getInsert();
+        final RawContactDelta update = getUpdate(mContext, CONTACT_FIRST);
+        final RawContactDelta insertSecond = getInsert();
+        final RawContactDeltaList set = buildSet(insertFirst, update, insertSecond);
+
+        // Two inserts should create two rules to bind against single existing
+        final ArrayList<CPOWrapper> diff = set.buildDiffWrapper();
+        final int exceptionCount = countExceptionUpdates(diff);
+        assertEquals("Unexpected exception updates", 2, exceptionCount);
+    }
+
+    public void testInsertInsertInsert() {
+        final RawContactDelta insertFirst = getInsert();
+        final RawContactDelta insertSecond = getInsert();
+        final RawContactDelta insertThird = getInsert();
+        final RawContactDeltaList set = buildSet(insertFirst, insertSecond, insertThird);
+
+        // Three new inserts should create only two binding rules
+        final ArrayList<CPOWrapper> diff = set.buildDiffWrapper();
+        final int exceptionCount = countExceptionUpdates(diff);
+        assertEquals("Unexpected exception updates", 2, exceptionCount);
+    }
+
+    public void testMergeDataRemoteInsert() {
+        final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_FIRST, buildPhone(PHONE_RED)));
+        final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_SECOND, buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
+
+        // Merge in second version, verify they match
+        final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+        assertEquals("Unexpected change when merging", second, merged);
+    }
+
+    public void testMergeDataLocalUpdateRemoteInsert() {
+        final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_FIRST, buildPhone(PHONE_RED)));
+        final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_SECOND, buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
+
+        // Change the local number to trigger update
+        final ValuesDelta phone = getPhone(first, CONTACT_BOB, PHONE_RED);
+        phone.put(Phone.NUMBER, TEST_PHONE);
+
+        assertDiffPattern(first,
+                buildAssertVersion(VER_FIRST),
+                buildUpdateAggregationSuspended(),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
+                buildUpdateAggregationDefault());
+
+        // Merge in the second version, verify diff matches
+        final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildAssertVersion(VER_SECOND),
+                buildUpdateAggregationSuspended(),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
+                buildUpdateAggregationDefault());
+    }
+
+    public void testMergeDataLocalUpdateRemoteDelete() {
+        final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_FIRST, buildPhone(PHONE_RED)));
+        final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_SECOND, buildPhone(PHONE_GREEN)));
+
+        // Change the local number to trigger update
+        final ValuesDelta phone = getPhone(first, CONTACT_BOB, PHONE_RED);
+        phone.put(Phone.NUMBER, TEST_PHONE);
+
+        assertDiffPattern(first,
+                buildAssertVersion(VER_FIRST),
+                buildUpdateAggregationSuspended(),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
+                buildUpdateAggregationDefault());
+
+        // Merge in the second version, verify that our update changed to
+        // insert, since RED was deleted on remote side
+        final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildAssertVersion(VER_SECOND),
+                buildUpdateAggregationSuspended(),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, buildDataInsert(phone, CONTACT_BOB)),
+                buildUpdateAggregationDefault());
+    }
+
+    public void testMergeDataLocalDeleteRemoteUpdate() {
+        final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_FIRST, buildPhone(PHONE_RED)));
+        final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_SECOND, buildPhone(PHONE_RED, TEST_PHONE)));
+
+        // Delete phone locally
+        final ValuesDelta phone = getPhone(first, CONTACT_BOB, PHONE_RED);
+        phone.markDeleted();
+
+        assertDiffPattern(first,
+                buildAssertVersion(VER_FIRST),
+                buildUpdateAggregationSuspended(),
+                buildDelete(Data.CONTENT_URI),
+                buildUpdateAggregationDefault());
+
+        // Merge in the second version, verify that our delete remains
+        final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildAssertVersion(VER_SECOND),
+                buildUpdateAggregationSuspended(),
+                buildDelete(Data.CONTENT_URI),
+                buildUpdateAggregationDefault());
+    }
+
+    public void testMergeDataLocalInsertRemoteInsert() {
+        final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_FIRST, buildPhone(PHONE_RED)));
+        final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_SECOND, buildPhone(PHONE_RED), buildPhone(PHONE_GREEN)));
+
+        // Insert new phone locally
+        final ValuesDelta bluePhone = ValuesDelta.fromAfter(buildPhone(PHONE_BLUE));
+        first.getByRawContactId(CONTACT_BOB).addEntry(bluePhone);
+        assertDiffPattern(first,
+                buildAssertVersion(VER_FIRST),
+                buildUpdateAggregationSuspended(),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, buildDataInsert(bluePhone, CONTACT_BOB)),
+                buildUpdateAggregationDefault());
+
+        // Merge in the second version, verify that our insert remains
+        final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildAssertVersion(VER_SECOND),
+                buildUpdateAggregationSuspended(),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, buildDataInsert(bluePhone, CONTACT_BOB)),
+                buildUpdateAggregationDefault());
+    }
+
+    public void testMergeRawContactLocalInsertRemoteInsert() {
+        final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_FIRST, buildPhone(PHONE_RED)));
+        final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_SECOND, buildPhone(PHONE_RED)), buildBeforeEntity(mContext, CONTACT_MARY,
+                        VER_SECOND, buildPhone(PHONE_RED)));
+
+        // Add new contact locally, should remain insert
+        final ContentValues joePhoneInsert = buildPhone(PHONE_BLUE);
+        final RawContactDelta joeContact = buildAfterEntity(joePhoneInsert);
+        final ContentValues joeContactInsert = joeContact.getValues().getCompleteValues();
+        joeContactInsert.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+        first.add(joeContact);
+        assertDiffPattern(first,
+                buildAssertVersion(VER_FIRST),
+                buildCPOWrapper(RawContacts.CONTENT_URI, TYPE_INSERT, joeContactInsert),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, joePhoneInsert),
+                buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
+                buildUpdateAggregationKeepTogether(CONTACT_BOB));
+
+        // Merge in the second version, verify that our insert remains
+        final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildAssertVersion(VER_SECOND),
+                buildAssertVersion(VER_SECOND),
+                buildCPOWrapper(RawContacts.CONTENT_URI, TYPE_INSERT, joeContactInsert),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, joePhoneInsert),
+                buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
+                buildUpdateAggregationKeepTogether(CONTACT_BOB));
+    }
+
+    public void testMergeRawContactLocalDeleteRemoteDelete() {
+        final RawContactDeltaList first = buildSet(
+                buildBeforeEntity(mContext, CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)),
+                buildBeforeEntity(mContext, CONTACT_MARY, VER_FIRST, buildPhone(PHONE_RED)));
+        final RawContactDeltaList second = buildSet(
+                buildBeforeEntity(mContext, CONTACT_BOB, VER_SECOND, buildPhone(PHONE_RED)));
+
+        // Remove contact locally
+        first.getByRawContactId(CONTACT_MARY).markDeleted();
+        assertDiffPattern(first,
+                buildAssertVersion(VER_FIRST),
+                buildAssertVersion(VER_FIRST),
+                buildDelete(RawContacts.CONTENT_URI));
+
+        // Merge in the second version, verify that our delete isn't needed
+        final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+        assertDiffPattern(merged);
+    }
+
+    public void testMergeRawContactLocalUpdateRemoteDelete() {
+        final RawContactDeltaList first = buildSet(
+                buildBeforeEntity(mContext, CONTACT_BOB, VER_FIRST, buildPhone(PHONE_RED)),
+                buildBeforeEntity(mContext, CONTACT_MARY, VER_FIRST, buildPhone(PHONE_RED)));
+        final RawContactDeltaList second = buildSet(
+                buildBeforeEntity(mContext, CONTACT_BOB, VER_SECOND, buildPhone(PHONE_RED)));
+
+        // Perform local update
+        final ValuesDelta phone = getPhone(first, CONTACT_MARY, PHONE_RED);
+        phone.put(Phone.NUMBER, TEST_PHONE);
+        assertDiffPattern(first,
+                buildAssertVersion(VER_FIRST),
+                buildAssertVersion(VER_FIRST),
+                buildUpdateAggregationSuspended(),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_UPDATE, phone.getAfter()),
+                buildUpdateAggregationDefault());
+
+        final ContentValues phoneInsert = phone.getCompleteValues();
+        final ContentValues contactInsert = first.getByRawContactId(CONTACT_MARY).getValues()
+                .getCompleteValues();
+        contactInsert.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
+
+        // Merge and verify that update turned into insert
+        final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+        assertDiffPattern(merged,
+                buildAssertVersion(VER_SECOND),
+                buildCPOWrapper(RawContacts.CONTENT_URI, TYPE_INSERT, contactInsert),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, phoneInsert),
+                buildAggregationModeUpdate(RawContacts.AGGREGATION_MODE_DEFAULT),
+                buildUpdateAggregationKeepTogether(CONTACT_BOB));
+    }
+
+    public void testMergeUsesNewVersion() {
+        final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_FIRST, buildPhone(PHONE_RED)));
+        final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_SECOND, buildPhone(PHONE_RED)));
+
+        assertEquals((Long)VER_FIRST, getVersion(first, CONTACT_BOB));
+        assertEquals((Long)VER_SECOND, getVersion(second, CONTACT_BOB));
+
+        final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+        assertEquals((Long)VER_SECOND, getVersion(merged, CONTACT_BOB));
+    }
+
+    public void testMergeAfterEnsureAndTrim() {
+        final RawContactDeltaList first = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_FIRST, buildEmail(EMAIL_YELLOW)));
+        final RawContactDeltaList second = buildSet(buildBeforeEntity(mContext, CONTACT_BOB,
+                VER_SECOND, buildEmail(EMAIL_YELLOW)));
+
+        // Ensure we have at least one phone
+        final AccountType source = getAccountType();
+        final RawContactDelta bobContact = first.getByRawContactId(CONTACT_BOB);
+        RawContactModifier.ensureKindExists(bobContact, source, Phone.CONTENT_ITEM_TYPE);
+        final ValuesDelta bobPhone = bobContact.getSuperPrimaryEntry(Phone.CONTENT_ITEM_TYPE, true);
+
+        // Make sure the update would insert a row
+        assertDiffPattern(first,
+                buildAssertVersion(VER_FIRST),
+                buildUpdateAggregationSuspended(),
+                buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT, buildDataInsert(bobPhone, CONTACT_BOB)),
+                buildUpdateAggregationDefault());
+
+        // Trim values and ensure that we don't insert things
+        RawContactModifier.trimEmpty(bobContact, source);
+        assertDiffPattern(first);
+
+        // Now re-parent the change, which should remain no-op
+        final RawContactDeltaList merged = RawContactDeltaList.mergeAfter(second, first);
+        assertDiffPattern(merged);
+    }
+}
diff --git a/tests/src/com/android/contacts/common/RawContactDeltaTests.java b/tests/src/com/android/contacts/common/RawContactDeltaTests.java
new file mode 100644
index 0000000..e4690d9
--- /dev/null
+++ b/tests/src/com/android/contacts/common/RawContactDeltaTests.java
@@ -0,0 +1,383 @@
+/*
+ * 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;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.os.Build;
+import android.os.Parcel;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.BuilderWrapper;
+import com.android.contacts.common.model.CPOWrapper;
+import com.android.contacts.common.model.RawContact;
+import com.android.contacts.common.model.RawContactDelta;
+import com.android.contacts.common.model.ValuesDelta;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for {@link RawContactDelta} and {@link ValuesDelta}. These tests
+ * focus on passing changes across {@link Parcel}, and verifying that they
+ * correctly build expected "diff" operations.
+ */
+@LargeTest
+public class RawContactDeltaTests extends AndroidTestCase {
+    public static final String TAG = "EntityDeltaTests";
+
+    public static final long TEST_CONTACT_ID = 12;
+    public static final long TEST_PHONE_ID = 24;
+
+    public static final String TEST_PHONE_NUMBER_1 = "218-555-1111";
+    public static final String TEST_PHONE_NUMBER_2 = "218-555-2222";
+
+    public static final String TEST_ACCOUNT_NAME = "TEST";
+
+    public RawContactDeltaTests() {
+        super();
+    }
+
+    @Override
+    public void setUp() {
+        mContext = getContext();
+    }
+
+    public static RawContact getRawContact(Context context, long contactId, long phoneId) {
+        // Build an existing contact read from database
+        final ContentValues contact = new ContentValues();
+        contact.put(RawContacts.VERSION, 43);
+        contact.put(RawContacts._ID, contactId);
+
+        final ContentValues phone = new ContentValues();
+        phone.put(Data._ID, phoneId);
+        phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
+        phone.put(Phone.TYPE, Phone.TYPE_HOME);
+
+        final RawContact before = new RawContact(contact);
+        before.addDataItemValues(phone);
+        return before;
+    }
+
+    /**
+     * Test that {@link RawContactDelta#mergeAfter(RawContactDelta)} correctly passes
+     * any changes through the {@link Parcel} object. This enforces that
+     * {@link RawContactDelta} should be identical when serialized against the same
+     * "before" {@link RawContact}.
+     */
+    public void testParcelChangesNone() {
+        final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+        final RawContactDelta source = RawContactDelta.fromBefore(before);
+        final RawContactDelta dest = RawContactDelta.fromBefore(before);
+
+        // Merge modified values and assert they match
+        final RawContactDelta merged = RawContactDelta.mergeAfter(dest, source);
+        assertEquals("Unexpected change when merging", source, merged);
+    }
+
+    public void testParcelChangesInsert() {
+        final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+        final RawContactDelta source = RawContactDelta.fromBefore(before);
+        final RawContactDelta dest = RawContactDelta.fromBefore(before);
+
+        // Add a new row and pass across parcel, should be same
+        final ContentValues phone = new ContentValues();
+        phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+        phone.put(Phone.TYPE, Phone.TYPE_WORK);
+        source.addEntry(ValuesDelta.fromAfter(phone));
+
+        // Merge modified values and assert they match
+        final RawContactDelta merged = RawContactDelta.mergeAfter(dest, source);
+        assertEquals("Unexpected change when merging", source, merged);
+    }
+
+    public void testParcelChangesUpdate() {
+        // Update existing row and pass across parcel, should be same
+        final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+        final RawContactDelta source = RawContactDelta.fromBefore(before);
+        final RawContactDelta dest = RawContactDelta.fromBefore(before);
+
+        final ValuesDelta child = source.getEntry(TEST_PHONE_ID);
+        child.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+
+        // Merge modified values and assert they match
+        final RawContactDelta merged = RawContactDelta.mergeAfter(dest, source);
+        assertEquals("Unexpected change when merging", source, merged);
+    }
+
+    public void testParcelChangesDelete() {
+        // Delete a row and pass across parcel, should be same
+        final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+        final RawContactDelta source = RawContactDelta.fromBefore(before);
+        final RawContactDelta dest = RawContactDelta.fromBefore(before);
+
+        final ValuesDelta child = source.getEntry(TEST_PHONE_ID);
+        child.markDeleted();
+
+        // Merge modified values and assert they match
+        final RawContactDelta merged = RawContactDelta.mergeAfter(dest, source);
+        assertEquals("Unexpected change when merging", source, merged);
+    }
+
+    public void testValuesDiffDelete() {
+        final ContentValues before = new ContentValues();
+        before.put(Data._ID, TEST_PHONE_ID);
+        before.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
+
+        final ValuesDelta values = ValuesDelta.fromBefore(before);
+        values.markDeleted();
+
+        // Should produce a delete action
+        final BuilderWrapper builderWrapper = values.buildDiffWrapper(Data.CONTENT_URI);
+        final boolean isDelete = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+                ? builderWrapper.getBuilder().build().isDelete()
+                : builderWrapper.getType() == CompatUtils.TYPE_DELETE;
+        assertTrue("Didn't produce delete action", isDelete);
+    }
+
+    /**
+     * Test that {@link RawContactDelta#buildDiffWrapper(ArrayList)} is correctly built for
+     * insert, update, and delete cases. This only tests a subset of possible
+     * {@link Data} row changes.
+     */
+    public void testEntityDiffNone() {
+        final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+        final RawContactDelta source = RawContactDelta.fromBefore(before);
+
+        // Assert that writing unchanged produces few operations
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        source.buildDiffWrapper(diff);
+
+        assertTrue("Created changes when none needed", (diff.size() == 0));
+    }
+
+    public void testEntityDiffNoneInsert() {
+        final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+        final RawContactDelta source = RawContactDelta.fromBefore(before);
+
+        // Insert a new phone number
+        final ContentValues phone = new ContentValues();
+        phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+        phone.put(Phone.TYPE, Phone.TYPE_WORK);
+        source.addEntry(ValuesDelta.fromAfter(phone));
+
+        // Assert two operations: insert Data row and enforce version
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        source.buildAssertWrapper(diff);
+        source.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 4, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            assertTrue("Expected version enforcement", CompatUtils.isAssertQueryCompat(cpoWrapper));
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(2);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isInsertCompat(cpoWrapper));
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(3);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
+
+    public void testEntityDiffUpdateInsert() {
+        final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+        final RawContactDelta source = RawContactDelta.fromBefore(before);
+
+        // Update parent contact values
+        source.getValues().put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
+
+        // Insert a new phone number
+        final ContentValues phone = new ContentValues();
+        phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+        phone.put(Phone.TYPE, Phone.TYPE_WORK);
+        source.addEntry(ValuesDelta.fromAfter(phone));
+
+        // Assert three operations: update Contact, insert Data row, enforce version
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        source.buildAssertWrapper(diff);
+        source.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 5, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            assertTrue("Expected version enforcement", CompatUtils.isAssertQueryCompat(cpoWrapper));
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(2);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(3);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isInsertCompat(cpoWrapper));
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(4);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
+
+    public void testEntityDiffNoneUpdate() {
+        final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+        final RawContactDelta source = RawContactDelta.fromBefore(before);
+
+        // Update existing phone number
+        final ValuesDelta child = source.getEntry(TEST_PHONE_ID);
+        child.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+
+        // Assert that version is enforced
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        source.buildAssertWrapper(diff);
+        source.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 4, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            assertTrue("Expected version enforcement", CompatUtils.isAssertQueryCompat(cpoWrapper));
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(2);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(3);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
+
+    public void testEntityDiffDelete() {
+        final RawContact before = getRawContact(mContext, TEST_CONTACT_ID, TEST_PHONE_ID);
+        final RawContactDelta source = RawContactDelta.fromBefore(before);
+
+        // Delete entire entity
+        source.getValues().markDeleted();
+
+        // Assert two operations: delete Contact and enforce version
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        source.buildAssertWrapper(diff);
+        source.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 2, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            assertTrue("Expected version enforcement", CompatUtils.isAssertQueryCompat(cpoWrapper));
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isDeleteCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
+
+    public void testEntityDiffInsert() {
+        // Insert a RawContact
+        final ContentValues after = new ContentValues();
+        after.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
+        after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+        final ValuesDelta values = ValuesDelta.fromAfter(after);
+        final RawContactDelta source = new RawContactDelta(values);
+
+        // Assert two operations: insert Contact and enforce version
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        source.buildAssertWrapper(diff);
+        source.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 2, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isInsertCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
+
+    public void testEntityDiffInsertInsert() {
+        // Insert a RawContact
+        final ContentValues after = new ContentValues();
+        after.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
+        after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+        final ValuesDelta values = ValuesDelta.fromAfter(after);
+        final RawContactDelta source = new RawContactDelta(values);
+
+        // Insert a new phone number
+        final ContentValues phone = new ContentValues();
+        phone.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        phone.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+        phone.put(Phone.TYPE, Phone.TYPE_WORK);
+        source.addEntry(ValuesDelta.fromAfter(phone));
+
+        // Assert two operations: delete Contact and enforce version
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        source.buildAssertWrapper(diff);
+        source.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isInsertCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isInsertCompat(cpoWrapper));
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/common/RawContactModifierTests.java b/tests/src/com/android/contacts/common/RawContactModifierTests.java
new file mode 100644
index 0000000..755838b
--- /dev/null
+++ b/tests/src/com/android/contacts/common/RawContactModifierTests.java
@@ -0,0 +1,1293 @@
+/*
+ * 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;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Intents.Insert;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.CPOWrapper;
+import com.android.contacts.common.model.RawContact;
+import com.android.contacts.common.model.RawContactDelta;
+import com.android.contacts.common.model.ValuesDelta;
+import com.android.contacts.common.model.RawContactDeltaList;
+import com.android.contacts.common.model.RawContactModifier;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountType.EditType;
+import com.android.contacts.common.model.account.ExchangeAccountType;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.test.mocks.ContactsMockContext;
+import com.android.contacts.common.test.mocks.MockAccountTypeManager;
+import com.android.contacts.common.test.mocks.MockContentProvider;
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link RawContactModifier} to verify that {@link AccountType}
+ * constraints are being enforced correctly.
+ */
+@LargeTest
+public class RawContactModifierTests extends AndroidTestCase {
+    public static final String TAG = "EntityModifierTests";
+
+    // From android.content.ContentProviderOperation
+    public static final int TYPE_INSERT = 1;
+
+    public static final long VER_FIRST = 100;
+
+    private static final long TEST_ID = 4;
+    private static final String TEST_PHONE = "218-555-1212";
+    private static final String TEST_NAME = "Adam Young";
+    private static final String TEST_NAME2 = "Breanne Duren";
+    private static final String TEST_IM = "example@example.com";
+    private static final String TEST_POSTAL = "1600 Amphitheatre Parkway";
+
+    private static final String TEST_ACCOUNT_NAME = "unittest@example.com";
+    private static final String TEST_ACCOUNT_TYPE = "com.example.unittest";
+
+    private static final String EXCHANGE_ACCT_TYPE = "com.android.exchange";
+
+    @Override
+    public void setUp() {
+        mContext = getContext();
+    }
+
+    public static class MockContactsSource extends AccountType {
+
+        MockContactsSource() {
+            try {
+                this.accountType = TEST_ACCOUNT_TYPE;
+
+                final DataKind nameKind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+                        R.string.nameLabelsGroup, -1, true);
+                nameKind.typeOverallMax = 1;
+                addKind(nameKind);
+
+                // Phone allows maximum 2 home, 1 work, and unlimited other, with
+                // constraint of 5 numbers maximum.
+                final DataKind phoneKind = new DataKind(
+                        Phone.CONTENT_ITEM_TYPE, -1, 10, true);
+
+                phoneKind.typeOverallMax = 5;
+                phoneKind.typeColumn = Phone.TYPE;
+                phoneKind.typeList = Lists.newArrayList();
+                phoneKind.typeList.add(new EditType(Phone.TYPE_HOME, -1).setSpecificMax(2));
+                phoneKind.typeList.add(new EditType(Phone.TYPE_WORK, -1).setSpecificMax(1));
+                phoneKind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, -1).setSecondary(true));
+                phoneKind.typeList.add(new EditType(Phone.TYPE_OTHER, -1));
+
+                phoneKind.fieldList = Lists.newArrayList();
+                phoneKind.fieldList.add(new EditField(Phone.NUMBER, -1, -1));
+                phoneKind.fieldList.add(new EditField(Phone.LABEL, -1, -1));
+
+                addKind(phoneKind);
+
+                // Email is unlimited
+                final DataKind emailKind = new DataKind(Email.CONTENT_ITEM_TYPE, -1, 10, true);
+                emailKind.typeOverallMax = -1;
+                emailKind.fieldList = Lists.newArrayList();
+                emailKind.fieldList.add(new EditField(Email.DATA, -1, -1));
+                addKind(emailKind);
+
+                // IM is only one
+                final DataKind imKind = new DataKind(Im.CONTENT_ITEM_TYPE, -1, 10, true);
+                imKind.typeOverallMax = 1;
+                imKind.fieldList = Lists.newArrayList();
+                imKind.fieldList.add(new EditField(Im.DATA, -1, -1));
+                addKind(imKind);
+
+                // Organization is only one
+                final DataKind orgKind = new DataKind(Organization.CONTENT_ITEM_TYPE, -1, 10, true);
+                orgKind.typeOverallMax = 1;
+                orgKind.fieldList = Lists.newArrayList();
+                orgKind.fieldList.add(new EditField(Organization.COMPANY, -1, -1));
+                orgKind.fieldList.add(new EditField(Organization.TITLE, -1, -1));
+                addKind(orgKind);
+            } catch (DefinitionException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public boolean isGroupMembershipEditable() {
+            return false;
+        }
+
+        @Override
+        public boolean areContactsWritable() {
+            return true;
+        }
+    }
+
+    /**
+     * Build a {@link AccountType} that has various odd constraints for
+     * testing purposes.
+     */
+    protected AccountType getAccountType() {
+        return new MockContactsSource();
+    }
+
+    /**
+     * Build {@link AccountTypeManager} instance.
+     */
+    protected AccountTypeManager getAccountTypes(AccountType... types) {
+        return new MockAccountTypeManager(types, null);
+    }
+
+    /**
+     * Build an {@link RawContact} with the requested set of phone numbers.
+     */
+    protected RawContactDelta getRawContact(Long existingId, ContentValues... entries) {
+        final ContentValues contact = new ContentValues();
+        if (existingId != null) {
+            contact.put(RawContacts._ID, existingId);
+        }
+        contact.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
+        contact.put(RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE);
+
+        final RawContact before = new RawContact(contact);
+        for (ContentValues values : entries) {
+            before.addDataItemValues(values);
+        }
+        return RawContactDelta.fromBefore(before);
+    }
+
+    /**
+     * Assert this {@link List} contains the given {@link Object}.
+     */
+    protected void assertContains(List<?> list, Object object) {
+        assertTrue("Missing expected value", list.contains(object));
+    }
+
+    /**
+     * Assert this {@link List} does not contain the given {@link Object}.
+     */
+    protected void assertNotContains(List<?> list, Object object) {
+        assertFalse("Contained unexpected value", list.contains(object));
+    }
+
+    /**
+     * Insert various rows to test
+     * {@link RawContactModifier#getValidTypes(RawContactDelta, DataKind, EditType)}
+     */
+    public void testValidTypes() {
+        // Build a source and pull specific types
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+        final EditType typeWork = RawContactModifier.getType(kindPhone, Phone.TYPE_WORK);
+        final EditType typeOther = RawContactModifier.getType(kindPhone, Phone.TYPE_OTHER);
+
+        List<EditType> validTypes;
+
+        // Add first home, first work
+        final RawContactDelta state = getRawContact(TEST_ID);
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+        RawContactModifier.insertChild(state, kindPhone, typeWork);
+
+        // Expecting home, other
+        validTypes = RawContactModifier.getValidTypes(state, kindPhone, null, true, null, true);
+        assertContains(validTypes, typeHome);
+        assertNotContains(validTypes, typeWork);
+        assertContains(validTypes, typeOther);
+
+        // Add second home
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+
+        // Expecting other
+        validTypes = RawContactModifier.getValidTypes(state, kindPhone, null, true, null, true);
+        assertNotContains(validTypes, typeHome);
+        assertNotContains(validTypes, typeWork);
+        assertContains(validTypes, typeOther);
+
+        // Add third and fourth home (invalid, but possible)
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+
+        // Expecting none
+        validTypes = RawContactModifier.getValidTypes(state, kindPhone, null, true, null, true);
+        assertNotContains(validTypes, typeHome);
+        assertNotContains(validTypes, typeWork);
+        assertNotContains(validTypes, typeOther);
+    }
+
+    /**
+     * Test which valid types there are when trying to update the editor type.
+     * {@link RawContactModifier#getValidTypes(RawContactDelta, DataKind, EditType, Boolean)}
+     */
+    public void testValidTypesWhenUpdating() {
+        // Build a source and pull specific types
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+        final EditType typeWork = RawContactModifier.getType(kindPhone, Phone.TYPE_WORK);
+        final EditType typeOther = RawContactModifier.getType(kindPhone, Phone.TYPE_OTHER);
+
+        List<EditType> validTypes;
+
+        // Add first home, first work
+        final RawContactDelta state = getRawContact(TEST_ID);
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+        RawContactModifier.insertChild(state, kindPhone, typeWork);
+
+        // Update editor type for home.
+        validTypes = RawContactModifier.getValidTypes(state, kindPhone, null, true, null, false);
+        assertContains(validTypes, typeHome);
+        assertNotContains(validTypes, typeWork);
+        assertContains(validTypes, typeOther);
+
+        // Add another 3 types. Overall limit is 5.
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+        RawContactModifier.insertChild(state, kindPhone, typeOther);
+        RawContactModifier.insertChild(state, kindPhone, typeOther);
+
+        // "Other" is valid when updating the editor type.
+        validTypes = RawContactModifier.getValidTypes(state, kindPhone, null, true, null, false);
+        assertNotContains(validTypes, typeHome);
+        assertNotContains(validTypes, typeWork);
+        assertContains(validTypes, typeOther);
+    }
+
+    /**
+     * Test {@link RawContactModifier#canInsert(RawContactDelta, DataKind)} by
+     * inserting various rows.
+     */
+    public void testCanInsert() {
+        // Build a source and pull specific types
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+        final EditType typeWork = RawContactModifier.getType(kindPhone, Phone.TYPE_WORK);
+        final EditType typeOther = RawContactModifier.getType(kindPhone, Phone.TYPE_OTHER);
+
+        // Add first home, first work
+        final RawContactDelta state = getRawContact(TEST_ID);
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+        RawContactModifier.insertChild(state, kindPhone, typeWork);
+        assertTrue("Unable to insert", RawContactModifier.canInsert(state, kindPhone));
+
+        // Add two other, which puts us just under "5" overall limit
+        RawContactModifier.insertChild(state, kindPhone, typeOther);
+        RawContactModifier.insertChild(state, kindPhone, typeOther);
+        assertTrue("Unable to insert", RawContactModifier.canInsert(state, kindPhone));
+
+        // Add second home, which should push to snug limit
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+        assertFalse("Able to insert", RawContactModifier.canInsert(state, kindPhone));
+    }
+
+    /**
+     * Test
+     * {@link RawContactModifier#getBestValidType(RawContactDelta, DataKind, boolean, int)}
+     * by asserting expected best options in various states.
+     */
+    public void testBestValidType() {
+        // Build a source and pull specific types
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+        final EditType typeWork = RawContactModifier.getType(kindPhone, Phone.TYPE_WORK);
+        final EditType typeFaxWork = RawContactModifier.getType(kindPhone, Phone.TYPE_FAX_WORK);
+        final EditType typeOther = RawContactModifier.getType(kindPhone, Phone.TYPE_OTHER);
+
+        EditType suggested;
+
+        // Default suggestion should be home
+        final RawContactDelta state = getRawContact(TEST_ID);
+        suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
+        assertEquals("Unexpected suggestion", typeHome, suggested);
+
+        // Add first home, should now suggest work
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+        suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
+        assertEquals("Unexpected suggestion", typeWork, suggested);
+
+        // Add work fax, should still suggest work
+        RawContactModifier.insertChild(state, kindPhone, typeFaxWork);
+        suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
+        assertEquals("Unexpected suggestion", typeWork, suggested);
+
+        // Add other, should still suggest work
+        RawContactModifier.insertChild(state, kindPhone, typeOther);
+        suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
+        assertEquals("Unexpected suggestion", typeWork, suggested);
+
+        // Add work, now should suggest other
+        RawContactModifier.insertChild(state, kindPhone, typeWork);
+        suggested = RawContactModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
+        assertEquals("Unexpected suggestion", typeOther, suggested);
+    }
+
+    public void testIsEmptyEmpty() {
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+
+        // Test entirely empty row
+        final ContentValues after = new ContentValues();
+        final ValuesDelta values = ValuesDelta.fromAfter(after);
+
+        assertTrue("Expected empty", RawContactModifier.isEmpty(values, kindPhone));
+    }
+
+    public void testIsEmptyDirectFields() {
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Test row that has type values, but core fields are empty
+        final RawContactDelta state = getRawContact(TEST_ID);
+        final ValuesDelta values = RawContactModifier.insertChild(state, kindPhone, typeHome);
+
+        assertTrue("Expected empty", RawContactModifier.isEmpty(values, kindPhone));
+
+        // Insert some data to trigger non-empty state
+        values.put(Phone.NUMBER, TEST_PHONE);
+
+        assertFalse("Expected non-empty", RawContactModifier.isEmpty(values, kindPhone));
+    }
+
+    public void testTrimEmptySingle() {
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Test row that has type values, but core fields are empty
+        final RawContactDelta state = getRawContact(TEST_ID);
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+
+        // Build diff, expecting insert for data row and update enforcement
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isInsertCompat(cpoWrapper));
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(2);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+
+        // Trim empty rows and try again, expecting delete of overall contact
+        RawContactModifier.trimEmpty(state, source);
+        diff.clear();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 1, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isDeleteCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
+
+    public void testTrimEmptySpaces() {
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Test row that has type values, but values are spaces
+        final RawContactDelta state = RawContactDeltaListTests.buildBeforeEntity(mContext, TEST_ID,
+                VER_FIRST);
+        final ValuesDelta values = RawContactModifier.insertChild(state, kindPhone, typeHome);
+        values.put(Phone.NUMBER, "   ");
+
+        // Build diff, expecting insert for data row and update enforcement
+        RawContactDeltaListTests.assertDiffPattern(state,
+                RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
+                RawContactDeltaListTests.buildUpdateAggregationSuspended(),
+                RawContactDeltaListTests.buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT,
+                        RawContactDeltaListTests.buildDataInsert(values, TEST_ID)),
+                RawContactDeltaListTests.buildUpdateAggregationDefault());
+
+        // Trim empty rows and try again, expecting delete of overall contact
+        RawContactModifier.trimEmpty(state, source);
+        RawContactDeltaListTests.assertDiffPattern(state,
+                RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
+                RawContactDeltaListTests.buildDelete(RawContacts.CONTENT_URI));
+    }
+
+    public void testTrimLeaveValid() {
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Test row that has type values with valid number
+        final RawContactDelta state = RawContactDeltaListTests.buildBeforeEntity(mContext, TEST_ID,
+                VER_FIRST);
+        final ValuesDelta values = RawContactModifier.insertChild(state, kindPhone, typeHome);
+        values.put(Phone.NUMBER, TEST_PHONE);
+
+        // Build diff, expecting insert for data row and update enforcement
+        RawContactDeltaListTests.assertDiffPattern(state,
+                RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
+                RawContactDeltaListTests.buildUpdateAggregationSuspended(),
+                RawContactDeltaListTests.buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT,
+                        RawContactDeltaListTests.buildDataInsert(values, TEST_ID)),
+                RawContactDeltaListTests.buildUpdateAggregationDefault());
+
+        // Trim empty rows and try again, expecting no differences
+        RawContactModifier.trimEmpty(state, source);
+        RawContactDeltaListTests.assertDiffPattern(state,
+                RawContactDeltaListTests.buildAssertVersion(VER_FIRST),
+                RawContactDeltaListTests.buildUpdateAggregationSuspended(),
+                RawContactDeltaListTests.buildCPOWrapper(Data.CONTENT_URI, TYPE_INSERT,
+                        RawContactDeltaListTests.buildDataInsert(values, TEST_ID)),
+                RawContactDeltaListTests.buildUpdateAggregationDefault());
+    }
+
+    public void testTrimEmptyUntouched() {
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Build "before" that has empty row
+        final RawContactDelta state = getRawContact(TEST_ID);
+        final ContentValues before = new ContentValues();
+        before.put(Data._ID, TEST_ID);
+        before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        state.addEntry(ValuesDelta.fromBefore(before));
+
+        // Build diff, expecting no changes
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+
+        // Try trimming existing empty, which we shouldn't touch
+        RawContactModifier.trimEmpty(state, source);
+        diff.clear();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+    }
+
+    public void testTrimEmptyAfterUpdate() {
+        final AccountType source = getAccountType();
+        final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Build "before" that has row with some phone number
+        final ContentValues before = new ContentValues();
+        before.put(Data._ID, TEST_ID);
+        before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        before.put(kindPhone.typeColumn, typeHome.rawValue);
+        before.put(Phone.NUMBER, TEST_PHONE);
+        final RawContactDelta state = getRawContact(TEST_ID, before);
+
+        // Build diff, expecting no changes
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+
+        // Now update row by changing number to empty string, expecting single update
+        final ValuesDelta child = state.getEntry(TEST_ID);
+        child.put(Phone.NUMBER, "");
+        diff.clear();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(2);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+
+        // Now run trim, which should turn that update into delete
+        RawContactModifier.trimEmpty(state, source);
+        diff.clear();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 1, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isDeleteCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
+
+    public void testTrimInsertEmpty() {
+        final AccountType accountType = getAccountType();
+        final AccountTypeManager accountTypes = getAccountTypes(accountType);
+        final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Try creating a contact without any child entries
+        final RawContactDelta state = getRawContact(null);
+        final RawContactDeltaList set = new RawContactDeltaList();
+        set.add(state);
+
+        // Build diff, expecting single insert
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 2, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isInsertCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+
+        // Trim empty rows and try again, expecting no insert
+        RawContactModifier.trimEmpty(set, accountTypes);
+        diff.clear();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+    }
+
+    public void testTrimInsertInsert() {
+        final AccountType accountType = getAccountType();
+        final AccountTypeManager accountTypes = getAccountTypes(accountType);
+        final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Try creating a contact with single empty entry
+        final RawContactDelta state = getRawContact(null);
+        RawContactModifier.insertChild(state, kindPhone, typeHome);
+        final RawContactDeltaList set = new RawContactDeltaList();
+        set.add(state);
+
+        // Build diff, expecting two insert operations
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isInsertCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isInsertCompat(cpoWrapper));
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+
+        // Trim empty rows and try again, expecting silence
+        RawContactModifier.trimEmpty(set, accountTypes);
+        diff.clear();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+    }
+
+    public void testTrimUpdateRemain() {
+        final AccountType accountType = getAccountType();
+        final AccountTypeManager accountTypes = getAccountTypes(accountType);
+        final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Build "before" with two phone numbers
+        final ContentValues first = new ContentValues();
+        first.put(Data._ID, TEST_ID);
+        first.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        first.put(kindPhone.typeColumn, typeHome.rawValue);
+        first.put(Phone.NUMBER, TEST_PHONE);
+
+        final ContentValues second = new ContentValues();
+        second.put(Data._ID, TEST_ID);
+        second.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        second.put(kindPhone.typeColumn, typeHome.rawValue);
+        second.put(Phone.NUMBER, TEST_PHONE);
+
+        final RawContactDelta state = getRawContact(TEST_ID, first, second);
+        final RawContactDeltaList set = new RawContactDeltaList();
+        set.add(state);
+
+        // Build diff, expecting no changes
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+
+        // Now update row by changing number to empty string, expecting single update
+        final ValuesDelta child = state.getEntry(TEST_ID);
+        child.put(Phone.NUMBER, "");
+        diff.clear();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(2);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+
+        // Now run trim, which should turn that update into delete
+        RawContactModifier.trimEmpty(set, accountTypes);
+        diff.clear();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isDeleteCompat(cpoWrapper));
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(2);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
+
+    public void testTrimUpdateUpdate() {
+        final AccountType accountType = getAccountType();
+        final AccountTypeManager accountTypes = getAccountTypes(accountType);
+        final DataKind kindPhone = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        final EditType typeHome = RawContactModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+        // Build "before" with two phone numbers
+        final ContentValues first = new ContentValues();
+        first.put(Data._ID, TEST_ID);
+        first.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        first.put(kindPhone.typeColumn, typeHome.rawValue);
+        first.put(Phone.NUMBER, TEST_PHONE);
+
+        final RawContactDelta state = getRawContact(TEST_ID, first);
+        final RawContactDeltaList set = new RawContactDeltaList();
+        set.add(state);
+
+        // Build diff, expecting no changes
+        final ArrayList<CPOWrapper> diff = Lists.newArrayList();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 0, diff.size());
+
+        // Now update row by changing number to empty string, expecting single update
+        final ValuesDelta child = state.getEntry(TEST_ID);
+        child.put(Phone.NUMBER, "");
+        diff.clear();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 3, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(1);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+        }
+        {
+            final CPOWrapper cpoWrapper = diff.get(2);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Expected aggregation mode change", CompatUtils.isUpdateCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+
+        // Now run trim, which should turn into deleting the whole contact
+        RawContactModifier.trimEmpty(set, accountTypes);
+        diff.clear();
+        state.buildDiffWrapper(diff);
+        assertEquals("Unexpected operations", 1, diff.size());
+        {
+            final CPOWrapper cpoWrapper = diff.get(0);
+            final ContentProviderOperation oper = cpoWrapper.getOperation();
+            assertTrue("Incorrect type", CompatUtils.isDeleteCompat(cpoWrapper));
+            assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+        }
+    }
+
+    public void testParseExtrasExistingName() {
+        final AccountType accountType = getAccountType();
+
+        // Build "before" name
+        final ContentValues first = new ContentValues();
+        first.put(Data._ID, TEST_ID);
+        first.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        first.put(StructuredName.GIVEN_NAME, TEST_NAME);
+
+        // Parse extras, making sure we keep single name
+        final RawContactDelta state = getRawContact(TEST_ID, first);
+        final Bundle extras = new Bundle();
+        extras.putString(Insert.NAME, TEST_NAME2);
+        RawContactModifier.parseExtras(mContext, accountType, state, extras);
+
+        final int nameCount = state.getMimeEntriesCount(StructuredName.CONTENT_ITEM_TYPE, true);
+        assertEquals("Unexpected names", 1, nameCount);
+    }
+
+    public void testParseExtrasIgnoreLimit() {
+        final AccountType accountType = getAccountType();
+
+        // Build "before" IM
+        final ContentValues first = new ContentValues();
+        first.put(Data._ID, TEST_ID);
+        first.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        first.put(Im.DATA, TEST_IM);
+
+        final RawContactDelta state = getRawContact(TEST_ID, first);
+        final int beforeCount = state.getMimeEntries(Im.CONTENT_ITEM_TYPE).size();
+
+        // We should ignore data that doesn't fit account type rules, since account type
+        // only allows single Im
+        final Bundle extras = new Bundle();
+        extras.putInt(Insert.IM_PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
+        extras.putString(Insert.IM_HANDLE, TEST_IM);
+        RawContactModifier.parseExtras(mContext, accountType, state, extras);
+
+        final int afterCount = state.getMimeEntries(Im.CONTENT_ITEM_TYPE).size();
+        assertEquals("Broke account type rules", beforeCount, afterCount);
+    }
+
+    public void testParseExtrasIgnoreUnhandled() {
+        final AccountType accountType = getAccountType();
+        final RawContactDelta state = getRawContact(TEST_ID);
+
+        // We should silently ignore types unsupported by account type
+        final Bundle extras = new Bundle();
+        extras.putString(Insert.POSTAL, TEST_POSTAL);
+        RawContactModifier.parseExtras(mContext, accountType, state, extras);
+
+        assertNull("Broke accoun type rules",
+                state.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE));
+    }
+
+    public void testParseExtrasJobTitle() {
+        final AccountType accountType = getAccountType();
+        final RawContactDelta state = getRawContact(TEST_ID);
+
+        // Make sure that we create partial Organizations
+        final Bundle extras = new Bundle();
+        extras.putString(Insert.JOB_TITLE, TEST_NAME);
+        RawContactModifier.parseExtras(mContext, accountType, state, extras);
+
+        final int count = state.getMimeEntries(Organization.CONTENT_ITEM_TYPE).size();
+        assertEquals("Expected to create organization", 1, count);
+    }
+
+    public void testMigrateWithDisplayNameFromGoogleToExchange1() {
+        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+        DataKind kind = newAccountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+
+        ContactsMockContext context = new ContactsMockContext(getContext());
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        mockNameValues.put(StructuredName.PREFIX, "prefix");
+        mockNameValues.put(StructuredName.GIVEN_NAME, "given");
+        mockNameValues.put(StructuredName.MIDDLE_NAME, "middle");
+        mockNameValues.put(StructuredName.FAMILY_NAME, "family");
+        mockNameValues.put(StructuredName.SUFFIX, "suffix");
+        mockNameValues.put(StructuredName.PHONETIC_FAMILY_NAME, "PHONETIC_FAMILY");
+        mockNameValues.put(StructuredName.PHONETIC_MIDDLE_NAME, "PHONETIC_MIDDLE");
+        mockNameValues.put(StructuredName.PHONETIC_GIVEN_NAME, "PHONETIC_GIVEN");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migrateStructuredName(context, oldState, newState, kind);
+        List<ValuesDelta> list = newState.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
+        assertEquals(1, list.size());
+
+        ContentValues output = list.get(0).getAfter();
+        assertEquals("prefix", output.getAsString(StructuredName.PREFIX));
+        assertEquals("given", output.getAsString(StructuredName.GIVEN_NAME));
+        assertEquals("middle", output.getAsString(StructuredName.MIDDLE_NAME));
+        assertEquals("family", output.getAsString(StructuredName.FAMILY_NAME));
+        assertEquals("suffix", output.getAsString(StructuredName.SUFFIX));
+        // Phonetic middle name isn't supported by Exchange.
+        assertEquals("PHONETIC_FAMILY", output.getAsString(StructuredName.PHONETIC_FAMILY_NAME));
+        assertEquals("PHONETIC_GIVEN", output.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
+    }
+
+    public void testMigrateWithDisplayNameFromGoogleToExchange2() {
+        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+        DataKind kind = newAccountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+
+        ContactsMockContext context = new ContactsMockContext(getContext());
+        MockContentProvider provider = context.getContactsProvider();
+
+        String inputDisplayName = "prefix given middle family suffix";
+        // The method will ask the provider to split/join StructuredName.
+        Uri uriForBuildDisplayName =
+                ContactsContract.AUTHORITY_URI
+                        .buildUpon()
+                        .appendPath("complete_name")
+                        .appendQueryParameter(StructuredName.DISPLAY_NAME, inputDisplayName)
+                        .build();
+        provider.expectQuery(uriForBuildDisplayName)
+                .returnRow("prefix", "given", "middle", "family", "suffix")
+                .withProjection(StructuredName.PREFIX, StructuredName.GIVEN_NAME,
+                        StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME,
+                        StructuredName.SUFFIX);
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        mockNameValues.put(StructuredName.DISPLAY_NAME, inputDisplayName);
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migrateStructuredName(context, oldState, newState, kind);
+        List<ValuesDelta> list = newState.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
+        assertEquals(1, list.size());
+
+        ContentValues outputValues = list.get(0).getAfter();
+        assertEquals("prefix", outputValues.getAsString(StructuredName.PREFIX));
+        assertEquals("given", outputValues.getAsString(StructuredName.GIVEN_NAME));
+        assertEquals("middle", outputValues.getAsString(StructuredName.MIDDLE_NAME));
+        assertEquals("family", outputValues.getAsString(StructuredName.FAMILY_NAME));
+        assertEquals("suffix", outputValues.getAsString(StructuredName.SUFFIX));
+    }
+
+    public void testMigrateWithStructuredNameFromExchangeToGoogle() {
+        AccountType oldAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+        AccountType newAccountType = new GoogleAccountType(getContext(), "");
+        DataKind kind = newAccountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+
+        ContactsMockContext context = new ContactsMockContext(getContext());
+        MockContentProvider provider = context.getContactsProvider();
+
+        // The method will ask the provider to split/join StructuredName.
+        Uri uriForBuildDisplayName =
+                ContactsContract.AUTHORITY_URI
+                        .buildUpon()
+                        .appendPath("complete_name")
+                        .appendQueryParameter(StructuredName.PREFIX, "prefix")
+                        .appendQueryParameter(StructuredName.GIVEN_NAME, "given")
+                        .appendQueryParameter(StructuredName.MIDDLE_NAME, "middle")
+                        .appendQueryParameter(StructuredName.FAMILY_NAME, "family")
+                        .appendQueryParameter(StructuredName.SUFFIX, "suffix")
+                        .build();
+        provider.expectQuery(uriForBuildDisplayName)
+                .returnRow("prefix given middle family suffix")
+                .withProjection(StructuredName.DISPLAY_NAME);
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        mockNameValues.put(StructuredName.PREFIX, "prefix");
+        mockNameValues.put(StructuredName.GIVEN_NAME, "given");
+        mockNameValues.put(StructuredName.MIDDLE_NAME, "middle");
+        mockNameValues.put(StructuredName.FAMILY_NAME, "family");
+        mockNameValues.put(StructuredName.SUFFIX, "suffix");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migrateStructuredName(context, oldState, newState, kind);
+
+        List<ValuesDelta> list = newState.getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
+        assertNotNull(list);
+        assertEquals(1, list.size());
+        ContentValues outputValues = list.get(0).getAfter();
+        assertEquals("prefix given middle family suffix",
+                outputValues.getAsString(StructuredName.DISPLAY_NAME));
+    }
+
+    public void testMigratePostalFromGoogleToExchange() {
+        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+        DataKind kind = newAccountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
+        mockNameValues.put(StructuredPostal.FORMATTED_ADDRESS, "formatted_address");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migratePostal(oldState, newState, kind);
+
+        List<ValuesDelta> list = newState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE);
+        assertNotNull(list);
+        assertEquals(1, list.size());
+        ContentValues outputValues = list.get(0).getAfter();
+        // FORMATTED_ADDRESS isn't supported by Exchange.
+        assertNull(outputValues.getAsString(StructuredPostal.FORMATTED_ADDRESS));
+        assertEquals("formatted_address", outputValues.getAsString(StructuredPostal.STREET));
+    }
+
+    public void testMigratePostalFromExchangeToGoogle() {
+        AccountType oldAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+        AccountType newAccountType = new GoogleAccountType(getContext(), "");
+        DataKind kind = newAccountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
+        mockNameValues.put(StructuredPostal.COUNTRY, "country");
+        mockNameValues.put(StructuredPostal.POSTCODE, "postcode");
+        mockNameValues.put(StructuredPostal.REGION, "region");
+        mockNameValues.put(StructuredPostal.CITY, "city");
+        mockNameValues.put(StructuredPostal.STREET, "street");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migratePostal(oldState, newState, kind);
+
+        List<ValuesDelta> list = newState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE);
+        assertNotNull(list);
+        assertEquals(1, list.size());
+        ContentValues outputValues = list.get(0).getAfter();
+
+        // Check FORMATTED_ADDRESS contains all info.
+        String formattedAddress = outputValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
+        assertNotNull(formattedAddress);
+        assertTrue(formattedAddress.contains("country"));
+        assertTrue(formattedAddress.contains("postcode"));
+        assertTrue(formattedAddress.contains("region"));
+        assertTrue(formattedAddress.contains("postcode"));
+        assertTrue(formattedAddress.contains("city"));
+        assertTrue(formattedAddress.contains("street"));
+    }
+
+    public void testMigrateEventFromGoogleToExchange1() {
+        testMigrateEventCommon(new GoogleAccountType(getContext(), ""),
+                new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE));
+    }
+
+    public void testMigrateEventFromExchangeToGoogle() {
+        testMigrateEventCommon(new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE),
+                new GoogleAccountType(getContext(), ""));
+    }
+
+    private void testMigrateEventCommon(AccountType oldAccountType, AccountType newAccountType) {
+        DataKind kind = newAccountType.getKindForMimetype(Event.CONTENT_ITEM_TYPE);
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Event.START_DATE, "1972-02-08");
+        mockNameValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migrateEvent(oldState, newState, kind, 1990);
+
+        List<ValuesDelta> list = newState.getMimeEntries(Event.CONTENT_ITEM_TYPE);
+        assertNotNull(list);
+        assertEquals(1, list.size());  // Anniversary should be dropped.
+        ContentValues outputValues = list.get(0).getAfter();
+
+        assertEquals("1972-02-08", outputValues.getAsString(Event.START_DATE));
+        assertEquals(Event.TYPE_BIRTHDAY, outputValues.getAsInteger(Event.TYPE).intValue());
+    }
+
+    public void testMigrateEventFromGoogleToExchange2() {
+        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+        DataKind kind = newAccountType.getKindForMimetype(Event.CONTENT_ITEM_TYPE);
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
+        // No year format is not supported by Exchange.
+        mockNameValues.put(Event.START_DATE, "--06-01");
+        mockNameValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+        mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Event.START_DATE, "1980-08-02");
+        // Anniversary is not supported by Exchange
+        mockNameValues.put(Event.TYPE, Event.TYPE_ANNIVERSARY);
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migrateEvent(oldState, newState, kind, 1990);
+
+        List<ValuesDelta> list = newState.getMimeEntries(Event.CONTENT_ITEM_TYPE);
+        assertNotNull(list);
+        assertEquals(1, list.size());  // Anniversary should be dropped.
+        ContentValues outputValues = list.get(0).getAfter();
+
+        // Default year should be used.
+        assertEquals("1990-06-01", outputValues.getAsString(Event.START_DATE));
+        assertEquals(Event.TYPE_BIRTHDAY, outputValues.getAsInteger(Event.TYPE).intValue());
+    }
+
+    public void testMigrateEmailFromGoogleToExchange() {
+        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+        DataKind kind = newAccountType.getKindForMimetype(Email.CONTENT_ITEM_TYPE);
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Email.TYPE, Email.TYPE_CUSTOM);
+        mockNameValues.put(Email.LABEL, "custom_type");
+        mockNameValues.put(Email.ADDRESS, "address1");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+        mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Email.TYPE, Email.TYPE_HOME);
+        mockNameValues.put(Email.ADDRESS, "address2");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+        mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Email.TYPE, Email.TYPE_WORK);
+        mockNameValues.put(Email.ADDRESS, "address3");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+        // Exchange can have up to 3 email entries. This 4th entry should be dropped.
+        mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Email.TYPE, Email.TYPE_OTHER);
+        mockNameValues.put(Email.ADDRESS, "address4");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migrateGenericWithTypeColumn(oldState, newState, kind);
+
+        List<ValuesDelta> list = newState.getMimeEntries(Email.CONTENT_ITEM_TYPE);
+        assertNotNull(list);
+        assertEquals(3, list.size());
+
+        ContentValues outputValues = list.get(0).getAfter();
+        assertEquals(Email.TYPE_CUSTOM, outputValues.getAsInteger(Email.TYPE).intValue());
+        assertEquals("custom_type", outputValues.getAsString(Email.LABEL));
+        assertEquals("address1", outputValues.getAsString(Email.ADDRESS));
+
+        outputValues = list.get(1).getAfter();
+        assertEquals(Email.TYPE_HOME, outputValues.getAsInteger(Email.TYPE).intValue());
+        assertEquals("address2", outputValues.getAsString(Email.ADDRESS));
+
+        outputValues = list.get(2).getAfter();
+        assertEquals(Email.TYPE_WORK, outputValues.getAsInteger(Email.TYPE).intValue());
+        assertEquals("address3", outputValues.getAsString(Email.ADDRESS));
+    }
+
+    public void testMigrateImFromGoogleToExchange() {
+        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+        DataKind kind = newAccountType.getKindForMimetype(Im.CONTENT_ITEM_TYPE);
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        // Exchange doesn't support TYPE_HOME
+        mockNameValues.put(Im.TYPE, Im.TYPE_HOME);
+        mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_JABBER);
+        mockNameValues.put(Im.DATA, "im1");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        // Exchange doesn't support TYPE_WORK
+        mockNameValues.put(Im.TYPE, Im.TYPE_WORK);
+        mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_YAHOO);
+        mockNameValues.put(Im.DATA, "im2");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Im.TYPE, Im.TYPE_OTHER);
+        mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM);
+        mockNameValues.put(Im.CUSTOM_PROTOCOL, "custom_protocol");
+        mockNameValues.put(Im.DATA, "im3");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        // Exchange can have up to 3 IM entries. This 4th entry should be dropped.
+        mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Im.TYPE, Im.TYPE_OTHER);
+        mockNameValues.put(Im.PROTOCOL, Im.PROTOCOL_GOOGLE_TALK);
+        mockNameValues.put(Im.DATA, "im4");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migrateGenericWithTypeColumn(oldState, newState, kind);
+
+        List<ValuesDelta> list = newState.getMimeEntries(Im.CONTENT_ITEM_TYPE);
+        assertNotNull(list);
+        assertEquals(3, list.size());
+
+        assertNotNull(kind.defaultValues.getAsInteger(Im.TYPE));
+
+        int defaultType = kind.defaultValues.getAsInteger(Im.TYPE);
+
+        ContentValues outputValues = list.get(0).getAfter();
+        // HOME should become default type.
+        assertEquals(defaultType, outputValues.getAsInteger(Im.TYPE).intValue());
+        assertEquals(Im.PROTOCOL_JABBER, outputValues.getAsInteger(Im.PROTOCOL).intValue());
+        assertEquals("im1", outputValues.getAsString(Im.DATA));
+
+        outputValues = list.get(1).getAfter();
+        assertEquals(defaultType, outputValues.getAsInteger(Im.TYPE).intValue());
+        assertEquals(Im.PROTOCOL_YAHOO, outputValues.getAsInteger(Im.PROTOCOL).intValue());
+        assertEquals("im2", outputValues.getAsString(Im.DATA));
+
+        outputValues = list.get(2).getAfter();
+        assertEquals(defaultType, outputValues.getAsInteger(Im.TYPE).intValue());
+        assertEquals(Im.PROTOCOL_CUSTOM, outputValues.getAsInteger(Im.PROTOCOL).intValue());
+        assertEquals("custom_protocol", outputValues.getAsString(Im.CUSTOM_PROTOCOL));
+        assertEquals("im3", outputValues.getAsString(Im.DATA));
+    }
+
+    public void testMigratePhoneFromGoogleToExchange() {
+        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+        DataKind kind = newAccountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+
+        // Create 5 numbers.
+        // - "1" -- HOME
+        // - "2" -- WORK
+        // - "3" -- CUSTOM
+        // - "4" -- WORK
+        // - "5" -- WORK_MOBILE
+        // Then we convert it to Exchange account type.
+        // - "1" -- HOME
+        // - "2" -- WORK
+        // - "3" -- Because CUSTOM is not supported, it'll be changed to the default, MOBILE
+        // - "4" -- WORK
+        // - "5" -- WORK_MOBILE not suppoted again, so will be MOBILE.
+        // But then, Exchange doesn't support multiple MOBILE numbers, so "5" will be removed.
+        // i.e. the result will be:
+        // - "1" -- HOME
+        // - "2" -- WORK
+        // - "3" -- MOBILE
+        // - "4" -- WORK
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Phone.TYPE, Phone.TYPE_HOME);
+        mockNameValues.put(Phone.NUMBER, "1");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+        mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Phone.TYPE, Phone.TYPE_WORK);
+        mockNameValues.put(Phone.NUMBER, "2");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+        mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        // Exchange doesn't support this type. Default to MOBILE
+        mockNameValues.put(Phone.TYPE, Phone.TYPE_CUSTOM);
+        mockNameValues.put(Phone.LABEL, "custom_type");
+        mockNameValues.put(Phone.NUMBER, "3");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+        mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Phone.TYPE, Phone.TYPE_WORK);
+        mockNameValues.put(Phone.NUMBER, "4");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+        mockNameValues = new ContentValues();
+
+        mockNameValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Phone.TYPE, Phone.TYPE_WORK_MOBILE);
+        mockNameValues.put(Phone.NUMBER, "5");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migrateGenericWithTypeColumn(oldState, newState, kind);
+
+        List<ValuesDelta> list = newState.getMimeEntries(Phone.CONTENT_ITEM_TYPE);
+        assertNotNull(list);
+        assertEquals(4, list.size());
+
+        int defaultType = Phone.TYPE_MOBILE;
+
+        ContentValues outputValues = list.get(0).getAfter();
+        assertEquals(Phone.TYPE_HOME, outputValues.getAsInteger(Phone.TYPE).intValue());
+        assertEquals("1", outputValues.getAsString(Phone.NUMBER));
+        outputValues = list.get(1).getAfter();
+        assertEquals(Phone.TYPE_WORK, outputValues.getAsInteger(Phone.TYPE).intValue());
+        assertEquals("2", outputValues.getAsString(Phone.NUMBER));
+        outputValues = list.get(2).getAfter();
+        assertEquals(defaultType, outputValues.getAsInteger(Phone.TYPE).intValue());
+        assertNull(outputValues.getAsInteger(Phone.LABEL));
+        assertEquals("3", outputValues.getAsString(Phone.NUMBER));
+        outputValues = list.get(3).getAfter();
+        assertEquals(Phone.TYPE_WORK, outputValues.getAsInteger(Phone.TYPE).intValue());
+        assertEquals("4", outputValues.getAsString(Phone.NUMBER));
+    }
+
+    public void testMigrateOrganizationFromGoogleToExchange() {
+        AccountType oldAccountType = new GoogleAccountType(getContext(), "");
+        AccountType newAccountType = new ExchangeAccountType(getContext(), "", EXCHANGE_ACCT_TYPE);
+        DataKind kind = newAccountType.getKindForMimetype(Organization.CONTENT_ITEM_TYPE);
+
+        RawContactDelta oldState = new RawContactDelta();
+        ContentValues mockNameValues = new ContentValues();
+        mockNameValues.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+        mockNameValues.put(Organization.COMPANY, "company1");
+        mockNameValues.put(Organization.DEPARTMENT, "department1");
+        oldState.addEntry(ValuesDelta.fromAfter(mockNameValues));
+
+        RawContactDelta newState = new RawContactDelta();
+        RawContactModifier.migrateGenericWithoutTypeColumn(oldState, newState, kind);
+
+        List<ValuesDelta> list = newState.getMimeEntries(Organization.CONTENT_ITEM_TYPE);
+        assertNotNull(list);
+        assertEquals(1, list.size());
+
+        ContentValues outputValues = list.get(0).getAfter();
+        assertEquals("company1", outputValues.getAsString(Organization.COMPANY));
+        assertEquals("department1", outputValues.getAsString(Organization.DEPARTMENT));
+    }
+}
diff --git a/tests/src/com/android/contacts/common/compat/CompatUtilsTest.java b/tests/src/com/android/contacts/common/compat/CompatUtilsTest.java
new file mode 100644
index 0000000..3386a00
--- /dev/null
+++ b/tests/src/com/android/contacts/common/compat/CompatUtilsTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.test.AndroidTestCase;
+
+public class CompatUtilsTest extends AndroidTestCase {
+
+    public void testIsClassAvailable_NullClassName() {
+        assertFalse(CompatUtils.isClassAvailable(null));
+    }
+
+    public void testIsClassAvailable_EmptyClassName() {
+        assertFalse(CompatUtils.isClassAvailable(""));
+    }
+
+    public void testIsClassAvailable_NonexistentClass() {
+        assertFalse(CompatUtils.isClassAvailable("com.android.contacts.common.NonexistentClass"));
+    }
+
+    public void testIsClassAvailable() {
+        assertTrue(CompatUtils.isClassAvailable(BaseClass.class.getName()));
+    }
+
+    public void testIsMethodAvailable_NullClassName() {
+        assertFalse(CompatUtils.isMethodAvailable(null, "methodName"));
+    }
+
+    public void testIsMethodAvailable_EmptyClassName() {
+        assertFalse(CompatUtils.isMethodAvailable("", "methodName"));
+    }
+
+    public void testIsMethodAvailable_NullMethodName() {
+        assertFalse(CompatUtils.isMethodAvailable("className", null));
+    }
+
+    public void testIsMethodAvailable_EmptyMethodName() {
+        assertFalse(CompatUtils.isMethodAvailable("className", ""));
+    }
+
+    public void testIsMethodAvailable_NonexistentClass() {
+        assertFalse(CompatUtils.isMethodAvailable("com.android.contacts.common.NonexistentClass",
+                ""));
+    }
+
+    public void testIsMethodAvailable_NonexistentMethod() {
+        assertFalse(CompatUtils.isMethodAvailable(BaseClass.class.getName(), "derivedMethod"));
+    }
+
+    public void testIsMethodAvailable() {
+        assertTrue(CompatUtils.isMethodAvailable(BaseClass.class.getName(), "baseMethod"));
+    }
+
+    public void testIsMethodAvailable_InheritedMethod() {
+        assertTrue(CompatUtils.isMethodAvailable(DerivedClass.class.getName(), "baseMethod"));
+    }
+
+    public void testIsMethodAvailable_OverloadedMethod() {
+        assertTrue(CompatUtils.isMethodAvailable(DerivedClass.class.getName(), "overloadedMethod"));
+        assertTrue(CompatUtils.isMethodAvailable(DerivedClass.class.getName(), "overloadedMethod",
+                Integer.TYPE));
+    }
+
+    public void testIsMethodAvailable_NonexistentOverload() {
+        assertFalse(CompatUtils.isMethodAvailable(DerivedClass.class.getName(), "overloadedMethod",
+                Boolean.TYPE));
+    }
+
+    public void testInvokeMethod_NullMethodName() {
+        assertNull(CompatUtils.invokeMethod(new BaseClass(), null, null, null));
+    }
+
+    public void testInvokeMethod_EmptyMethodName() {
+        assertNull(CompatUtils.invokeMethod(new BaseClass(), "", null, null));
+    }
+
+    public void testInvokeMethod_NullClassInstance() {
+        assertNull(CompatUtils.invokeMethod(null, "", null, null));
+    }
+
+    public void testInvokeMethod_NonexistentMethod() {
+        assertNull(CompatUtils.invokeMethod(new BaseClass(), "derivedMethod", null, null));
+    }
+
+    public void testInvokeMethod_MethodWithNoParameters() {
+        assertEquals(1, CompatUtils.invokeMethod(new DerivedClass(), "overloadedMethod", null, null));
+    }
+
+    public void testInvokeMethod_MethodWithNoParameters_WithParameters() {
+        assertNull(CompatUtils.invokeMethod(new DerivedClass(), "derivedMethod",
+                new Class<?>[] {Integer.TYPE}, new Object[] {1}));
+    }
+
+    public void testInvokeMethod_MethodWithParameters_WithEmptyParameterList() {
+        assertNull(CompatUtils.invokeMethod(new DerivedClass(), "overloadedMethod",
+                new Class<?>[] {Integer.TYPE}, new Object[] {}));
+    }
+
+    public void testInvokeMethod_InvokeSimpleMethod() {
+        assertEquals(2, CompatUtils.invokeMethod(new DerivedClass(), "overloadedMethod",
+                new Class<?>[] {Integer.TYPE}, new Object[] {2}));
+    }
+
+    private class BaseClass {
+        public void baseMethod() {}
+    }
+
+    private class DerivedClass extends BaseClass {
+        public int derivedMethod() {
+            // This method needs to return something to differentiate a successful invocation from
+            // an unsuccessful one.
+            return 0;
+        }
+
+        public int overloadedMethod() {
+            return 1;
+        }
+
+        public int overloadedMethod(int i) {
+            return i;
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/common/database/NoNullCursorAsyncQueryHandlerTest.java b/tests/src/com/android/contacts/common/database/NoNullCursorAsyncQueryHandlerTest.java
new file mode 100644
index 0000000..a2b635d
--- /dev/null
+++ b/tests/src/com/android/contacts/common/database/NoNullCursorAsyncQueryHandlerTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.database;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.test.InstrumentationTestCase;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit test for {@link NoNullCursorAsyncQueryHandler}
+ */
+public class NoNullCursorAsyncQueryHandlerTest extends InstrumentationTestCase {
+
+    private MockContentResolver mMockContentResolver;
+
+    private static final String AUTHORITY = "com.android.contacts.common.unittest";
+    private static final Uri URI = Uri.parse("content://" + AUTHORITY);
+    private static final String[] PROJECTION = new String[]{"column1", "column2"};
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mMockContentResolver = new MockContentResolver();
+        final MockContentProvider mMockContentProvider = new MockContentProvider() {
+            @Override
+            public Cursor query(Uri uri, String[] projection, String selection,
+                    String[] selectionArgs,
+                    String sortOrder) {
+                return null;
+            }
+        };
+
+        mMockContentResolver.addProvider(AUTHORITY, mMockContentProvider);
+    }
+
+    public void testCursorIsNotNull() throws Throwable {
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ObjectHolder<Cursor> cursorHolder = ObjectHolder.newInstance();
+        final ObjectHolder<Boolean> ranHolder = ObjectHolder.newInstance(false);
+
+        runTestOnUiThread(new Runnable() {
+
+            @Override
+            public void run() {
+
+                NoNullCursorAsyncQueryHandler handler = new NoNullCursorAsyncQueryHandler(
+                        mMockContentResolver) {
+                    @Override
+                    protected void onNotNullableQueryComplete(int token, Object cookie,
+                            Cursor cursor) {
+                        cursorHolder.obj = cursor;
+                        ranHolder.obj = true;
+                        latch.countDown();
+                    }
+                };
+                handler.startQuery(1, null, URI, PROJECTION, null, null, null);
+            }
+        });
+
+        latch.await(5, TimeUnit.SECONDS);
+        assertFalse(cursorHolder.obj == null);
+        assertTrue(ranHolder.obj);
+    }
+
+    public void testCursorContainsCorrectCookies() throws Throwable {
+        final ObjectHolder<Boolean> ranHolder = ObjectHolder.newInstance(false);
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ObjectHolder<Object> cookieHolder = ObjectHolder.newInstance();
+        final String cookie = "TEST COOKIE";
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                final NoNullCursorAsyncQueryHandler handler = new NoNullCursorAsyncQueryHandler(
+                        mMockContentResolver) {
+                    @Override
+                    protected void onNotNullableQueryComplete(int token, Object cookie,
+                            Cursor cursor) {
+                        ranHolder.obj = true;
+                        cookieHolder.obj = cookie;
+                        latch.countDown();
+                    }
+                };
+                handler.startQuery(1, cookie, URI, PROJECTION, null, null, null);
+            }
+        });
+
+        latch.await(5, TimeUnit.SECONDS);
+        assertSame(cookie, cookieHolder.obj);
+        assertTrue(ranHolder.obj);
+    }
+
+    public void testCursorContainsCorrectColumns() throws Throwable {
+        final ObjectHolder<Boolean> ranHolder = ObjectHolder.newInstance(false);
+        final CountDownLatch latch = new CountDownLatch(1);
+        final ObjectHolder<Cursor> cursorHolder = ObjectHolder.newInstance();
+        final String cookie = "TEST COOKIE";
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                final NoNullCursorAsyncQueryHandler handler = new NoNullCursorAsyncQueryHandler(
+                        mMockContentResolver) {
+                    @Override
+                    protected void onNotNullableQueryComplete(int token, Object cookie,
+                            Cursor cursor) {
+                        ranHolder.obj = true;
+                        cursorHolder.obj = cursor;
+                        latch.countDown();
+                    }
+                };
+                handler.startQuery(1, cookie, URI, PROJECTION, null, null, null);
+            }
+        });
+
+        latch.await(5, TimeUnit.SECONDS);
+        assertSame(PROJECTION, cursorHolder.obj.getColumnNames());
+        assertTrue(ranHolder.obj);
+    }
+
+    private static class ObjectHolder<T> {
+        public T obj;
+
+        public static <E> ObjectHolder<E> newInstance() {
+            return new ObjectHolder<E>();
+        }
+
+        public static <E> ObjectHolder<E> newInstance(E value) {
+            ObjectHolder<E> holder = new ObjectHolder<E>();
+            holder.obj = value;
+            return holder;
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/common/format/FormatUtilsTests.java b/tests/src/com/android/contacts/common/format/FormatUtilsTests.java
new file mode 100644
index 0000000..8f4f772
--- /dev/null
+++ b/tests/src/com/android/contacts/common/format/FormatUtilsTests.java
@@ -0,0 +1,114 @@
+/*
+ * 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.format;
+
+import android.database.CharArrayBuffer;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+/**
+ * Test cases for format utility methods.
+ */
+@SmallTest
+public class FormatUtilsTests extends AndroidTestCase {
+
+    public void testOverlapPoint() throws Exception {
+        assertEquals(2, FormatUtils.overlapPoint("abcde", "cdefg"));
+        assertEquals(-1, FormatUtils.overlapPoint("John Doe", "John Doe"));
+        assertEquals(5, FormatUtils.overlapPoint("John Doe", "Doe, John"));
+        assertEquals(-1, FormatUtils.overlapPoint("Mr. John Doe", "Mr. Doe, John"));
+        assertEquals(13, FormatUtils.overlapPoint("John Herbert Doe", "Doe, John Herbert"));
+    }
+
+    public void testCopyToCharArrayBuffer() {
+        CharArrayBuffer charArrayBuffer = new CharArrayBuffer(20);
+        checkCopyToCharArrayBuffer(charArrayBuffer, null, 0);
+        checkCopyToCharArrayBuffer(charArrayBuffer, "", 0);
+        checkCopyToCharArrayBuffer(charArrayBuffer, "test", 4);
+        // Check that it works after copying something into it.
+        checkCopyToCharArrayBuffer(charArrayBuffer, "", 0);
+        checkCopyToCharArrayBuffer(charArrayBuffer, "test", 4);
+        checkCopyToCharArrayBuffer(charArrayBuffer, null, 0);
+        // This requires a resize of the actual buffer.
+        checkCopyToCharArrayBuffer(charArrayBuffer, "test test test test test", 24);
+    }
+
+    public void testCharArrayBufferToString() {
+        checkCharArrayBufferToString("");
+        checkCharArrayBufferToString("test");
+        checkCharArrayBufferToString("test test test test test");
+    }
+
+    /** Checks that copying a string into a {@link CharArrayBuffer} and back works correctly. */
+    private void checkCharArrayBufferToString(String text) {
+        CharArrayBuffer buffer = new CharArrayBuffer(20);
+        FormatUtils.copyToCharArrayBuffer(text, buffer);
+        assertEquals(text, FormatUtils.charArrayBufferToString(buffer));
+    }
+
+    /**
+     * Checks that copying into the char array buffer copies the values correctly.
+     */
+    private void checkCopyToCharArrayBuffer(CharArrayBuffer buffer, String value, int length) {
+        FormatUtils.copyToCharArrayBuffer(value, buffer);
+        assertEquals(length, buffer.sizeCopied);
+        for (int index = 0; index < length; ++index) {
+            assertEquals(value.charAt(index), buffer.data[index]);
+        }
+    }
+
+    public void testIndexOfWordPrefix_NullPrefix() {
+        assertEquals(-1, FormatUtils.indexOfWordPrefix("test", null));
+    }
+
+    public void testIndexOfWordPrefix_NullText() {
+        assertEquals(-1, FormatUtils.indexOfWordPrefix(null, "TE"));
+    }
+
+    public void testIndexOfWordPrefix_MatchingPrefix() {
+        checkIndexOfWordPrefix("test", "TE", 0);
+        checkIndexOfWordPrefix("Test", "TE", 0);
+        checkIndexOfWordPrefix("TEst", "TE", 0);
+        checkIndexOfWordPrefix("TEST", "TE", 0);
+        checkIndexOfWordPrefix("a test", "TE", 2);
+        checkIndexOfWordPrefix("test test", "TE", 0);
+        checkIndexOfWordPrefix("a test test", "TE", 2);
+    }
+
+    public void testIndexOfWordPrefix_NotMatchingPrefix() {
+        checkIndexOfWordPrefix("test", "TA", -1);
+        checkIndexOfWordPrefix("test type theme", "TA", -1);
+        checkIndexOfWordPrefix("atest retest pretest", "TEST", -1);
+        checkIndexOfWordPrefix("tes", "TEST", -1);
+    }
+
+    public void testIndexOfWordPrefix_LowerCase() {
+        // The prefix match only works if the prefix is un upper case.
+        checkIndexOfWordPrefix("test", "te", -1);
+    }
+
+    /**
+     * Checks that getting the index of a word prefix in the given text returns the expected index.
+     *
+     * @param text the text in which to look for the word
+     * @param wordPrefix the word prefix to look for
+     * @param expectedIndex the expected value to be returned by the function
+     */
+    private void checkIndexOfWordPrefix(String text, String wordPrefix, int expectedIndex) {
+        assertEquals(expectedIndex, FormatUtils.indexOfWordPrefix(text, wordPrefix));
+    }
+}
diff --git a/tests/src/com/android/contacts/common/format/TextHighlighterTest.java b/tests/src/com/android/contacts/common/format/TextHighlighterTest.java
new file mode 100644
index 0000000..7bf28c5
--- /dev/null
+++ b/tests/src/com/android/contacts/common/format/TextHighlighterTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.format;
+
+import android.graphics.Typeface;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.SpannableString;
+
+import com.android.contacts.common.format.SpannedTestUtils;
+import com.android.contacts.common.format.TextHighlighter;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link TextHighlighter}.
+ */
+@SmallTest
+public class TextHighlighterTest extends TestCase {
+    private static final int TEST_PREFIX_HIGHLIGHT_COLOR = 0xFF0000;
+
+    /** The object under test. */
+    private TextHighlighter mTextHighlighter;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mTextHighlighter = new TextHighlighter(Typeface.BOLD);
+    }
+
+    public void testApply_EmptyPrefix() {
+        CharSequence seq = mTextHighlighter.applyPrefixHighlight("", "");
+        SpannedTestUtils.assertNotSpanned(seq, "");
+
+        seq = mTextHighlighter.applyPrefixHighlight("test", "");
+        SpannedTestUtils.assertNotSpanned(seq, "test");
+    }
+
+    public void testSetText_MatchingPrefix() {
+        final String prefix = "TE";
+
+        CharSequence seq = mTextHighlighter.applyPrefixHighlight("test", prefix);
+        SpannedTestUtils.assertPrefixSpan(seq, 0, 1);
+
+        seq = mTextHighlighter.applyPrefixHighlight("Test", prefix);
+        SpannedTestUtils.assertPrefixSpan(seq, 0, 1);
+
+        seq = mTextHighlighter.applyPrefixHighlight("TEst", prefix);
+        SpannedTestUtils.assertPrefixSpan(seq, 0, 1);
+
+        seq = mTextHighlighter.applyPrefixHighlight("a test", prefix);
+        SpannedTestUtils.assertPrefixSpan(seq, 2, 3);
+    }
+
+    public void testSetText_NotMatchingPrefix() {
+        final CharSequence seq = mTextHighlighter.applyPrefixHighlight("test", "TA");
+        SpannedTestUtils.assertNotSpanned(seq, "test");
+    }
+
+    public void testSetText_FirstMatch() {
+        final CharSequence seq = mTextHighlighter.applyPrefixHighlight(
+                "a test's tests are not tests", "TE");
+        SpannedTestUtils.assertPrefixSpan(seq, 2, 3);
+    }
+
+    public void testSetText_NoMatchingMiddleOfWord() {
+        final String prefix = "TE";
+        CharSequence seq = mTextHighlighter.applyPrefixHighlight("atest", prefix);
+        SpannedTestUtils.assertNotSpanned(seq, "atest");
+
+        seq = mTextHighlighter.applyPrefixHighlight("atest otest", prefix);
+        SpannedTestUtils.assertNotSpanned(seq, "atest otest");
+
+        seq = mTextHighlighter.applyPrefixHighlight("atest test", prefix);
+        SpannedTestUtils.assertPrefixSpan(seq, 6, 7);
+    }
+
+    public void testSetMask_Highlight() {
+        final SpannableString testString1 = new SpannableString("alongtest");
+        mTextHighlighter.applyMaskingHighlight(testString1, 2, 4);
+        assertEquals(2, SpannedTestUtils.getNextTransition(testString1, 0));
+        assertEquals(4, SpannedTestUtils.getNextTransition(testString1, 2));
+
+        mTextHighlighter.applyMaskingHighlight(testString1, 3, 6);
+        assertEquals(2, SpannedTestUtils.getNextTransition(testString1, 0));
+        assertEquals(4, SpannedTestUtils.getNextTransition(testString1, 3));
+
+        mTextHighlighter.applyMaskingHighlight(testString1, 4, 5);
+        assertEquals(3, SpannedTestUtils.getNextTransition(testString1, 2));
+
+        mTextHighlighter.applyMaskingHighlight(testString1, 7, 8);
+        assertEquals(6, SpannedTestUtils.getNextTransition(testString1, 5));
+        assertEquals(7, SpannedTestUtils.getNextTransition(testString1, 6));
+        assertEquals(8, SpannedTestUtils.getNextTransition(testString1, 7));
+    }
+}
diff --git a/tests/src/com/android/contacts/common/list/ContactListItemViewTest.java b/tests/src/com/android/contacts/common/list/ContactListItemViewTest.java
new file mode 100644
index 0000000..c9b2e6d
--- /dev/null
+++ b/tests/src/com/android/contacts/common/list/ContactListItemViewTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.ContactsContract;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.widget.TextView;
+
+import com.android.contacts.common.format.SpannedTestUtils;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/**
+ * Unit tests for {@link com.android.contacts.common.list.ContactListItemView}.
+ *
+ * It uses an {@link ActivityInstrumentationTestCase2} for {@link PeopleActivity} because we need
+ * to have the style properly setup.
+ */
+@LargeTest
+public class ContactListItemViewTest extends AndroidTestCase {
+
+    //private IntegrationTestUtils mUtils;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        // This test requires that the screen be turned on.
+        //mUtils = new IntegrationTestUtils(getInstrumentation());
+        //mUtils.acquireScreenWakeLock(getInstrumentation().getTargetContext());
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        //mUtils.releaseScreenWakeLock();
+        super.tearDown();
+    }
+
+    public void testShowDisplayName_Simple() {
+        Cursor cursor = createCursor("John Doe", "Doe John");
+        ContactListItemView view = createView();
+
+        view.showDisplayName(cursor, 0, ContactsPreferences.DISPLAY_ORDER_PRIMARY);
+
+        assertEquals(view.getNameTextView().getText().toString(), "John Doe");
+    }
+
+    public void testShowDisplayName_Unknown() {
+        Cursor cursor = createCursor("", "");
+        ContactListItemView view = createView();
+
+        view.setUnknownNameText("unknown");
+        view.showDisplayName(cursor, 0, ContactsPreferences.DISPLAY_ORDER_PRIMARY);
+
+        assertEquals(view.getNameTextView().getText().toString(), "unknown");
+    }
+
+    public void testShowDisplayName_WithPrefix() {
+        Cursor cursor = createCursor("John Doe", "Doe John");
+        ContactListItemView view = createView();
+
+        view.setHighlightedPrefix("DOE");
+        view.showDisplayName(cursor, 0, ContactsPreferences.DISPLAY_ORDER_PRIMARY);
+
+        CharSequence seq = view.getNameTextView().getText();
+        assertEquals("John Doe", seq.toString());
+        SpannedTestUtils.assertPrefixSpan(seq, 5, 7);
+        // Talback should be without span tags.
+        assertEquals("John Doe", view.getNameTextView().getContentDescription());
+        assertFalse("John Doe".equals(seq));
+    }
+
+    public void testShowDisplayName_WithPrefixReversed() {
+        Cursor cursor = createCursor("John Doe", "Doe John");
+        ContactListItemView view = createView();
+
+        view.setHighlightedPrefix("DOE");
+        view.showDisplayName(cursor, 0, ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE);
+
+        CharSequence seq = view.getNameTextView().getText();
+        assertEquals("John Doe", seq.toString());
+        SpannedTestUtils.assertPrefixSpan(seq, 5, 7);
+    }
+
+    public void testSetSnippet_Prefix() {
+        ContactListItemView view = createView();
+        view.setHighlightedPrefix("TEST");
+        view.setSnippet("This is a test");
+
+        CharSequence seq = view.getSnippetView().getText();
+
+        assertEquals("This is a test", seq.toString());
+        SpannedTestUtils.assertPrefixSpan(seq, 10, 13);
+    }
+
+    /** Creates the view to be tested. */
+    private ContactListItemView createView() {
+        ContactListItemView view = new ContactListItemView(getContext());
+        // Set the name view to use a Spannable to represent its content.
+        view.getNameTextView().setText("", TextView.BufferType.SPANNABLE);
+        return view;
+    }
+
+    /**
+     * Creates a cursor containing a pair of values.
+     *
+     * @param name the name to insert in the first column of the cursor
+     * @param alternateName the alternate name to insert in the second column of the cursor
+     * @return the newly created cursor
+     */
+    private Cursor createCursor(String name, String alternateName) {
+        MatrixCursor cursor = new MatrixCursor(new String[]{"Name", "AlternateName"});
+        cursor.moveToFirst();
+        cursor.addRow(new Object[]{name, alternateName});
+        return cursor;
+    }
+}
diff --git a/tests/src/com/android/contacts/common/model/AccountTypeManagerTest.java b/tests/src/com/android/contacts/common/model/AccountTypeManagerTest.java
new file mode 100644
index 0000000..b8ebdd2
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/AccountTypeManagerTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.content.Context;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test case for {@link com.android.contacts.common.model.AccountTypeManager}.
+ *
+ * adb shell am instrument -w -e class com.android.contacts.model.AccountTypeManagerTest \
+       com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+@SmallTest
+public class AccountTypeManagerTest extends AndroidTestCase {
+    public void testFindAllInvitableAccountTypes() {
+        final Context c = getContext();
+
+        // Define account types.
+        final AccountType typeA = new MockAccountType("type1", null, null);
+        final AccountType typeB = new MockAccountType("type1", "minus", null);
+        final AccountType typeC = new MockAccountType("type2", null, "c");
+        final AccountType typeD = new MockAccountType("type2", "minus", "d");
+
+        // Define users
+        final AccountWithDataSet accountA1 = createAccountWithDataSet("a1", typeA);
+        final AccountWithDataSet accountC1 = createAccountWithDataSet("c1", typeC);
+        final AccountWithDataSet accountC2 = createAccountWithDataSet("c2", typeC);
+        final AccountWithDataSet accountD1 = createAccountWithDataSet("d1", typeD);
+
+        // empty - empty
+        Map<AccountTypeWithDataSet, AccountType> types =
+                AccountTypeManagerImpl.findAllInvitableAccountTypes(c,
+                        buildAccounts(), buildAccountTypes());
+        assertEquals(0, types.size());
+        try {
+            types.clear();
+            fail("Returned Map should be unmodifiable.");
+        } catch (UnsupportedOperationException ok) {
+        }
+
+        // No invite support, no accounts
+        verifyAccountTypes(
+                buildAccounts(),
+                buildAccountTypes(typeA, typeB)
+                /* empty */
+                );
+
+        // No invite support, with accounts
+        verifyAccountTypes(
+                buildAccounts(accountA1),
+                buildAccountTypes(typeA)
+                /* empty */
+                );
+
+        // With invite support, no accounts
+        verifyAccountTypes(
+                buildAccounts(),
+                buildAccountTypes(typeC)
+                /* empty */
+                );
+
+        // With invite support, 1 account
+        verifyAccountTypes(
+                buildAccounts(accountC1),
+                buildAccountTypes(typeC),
+                typeC
+                );
+
+        // With invite support, 2 account
+        verifyAccountTypes(
+                buildAccounts(accountC1, accountC2),
+                buildAccountTypes(typeC),
+                typeC
+                );
+
+        // Combinations...
+        verifyAccountTypes(
+                buildAccounts(accountA1),
+                buildAccountTypes(typeA, typeC)
+                /* empty */
+                );
+
+        verifyAccountTypes(
+                buildAccounts(accountC1, accountA1),
+                buildAccountTypes(typeA, typeC),
+                typeC
+                );
+
+        verifyAccountTypes(
+                buildAccounts(accountC1, accountA1),
+                buildAccountTypes(typeD, typeA, typeC),
+                typeC
+                );
+
+        verifyAccountTypes(
+                buildAccounts(accountC1, accountA1, accountD1),
+                buildAccountTypes(typeD, typeA, typeC, typeB),
+                typeC, typeD
+                );
+    }
+
+    private static AccountWithDataSet createAccountWithDataSet(String name, AccountType type) {
+        return new AccountWithDataSet(name, type.accountType, type.dataSet);
+    }
+
+    /**
+     * Array of {@link AccountType} -> {@link Map}
+     */
+    private static Map<AccountTypeWithDataSet, AccountType> buildAccountTypes(AccountType... types) {
+        final HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
+        for (AccountType type : types) {
+            result.put(type.getAccountTypeAndDataSet(), type);
+        }
+        return result;
+    }
+
+    /**
+     * Array of {@link AccountWithDataSet} -> {@link Collection}
+     */
+    private static Collection<AccountWithDataSet> buildAccounts(AccountWithDataSet... accounts) {
+        final List<AccountWithDataSet> result = Lists.newArrayList();
+        for (AccountWithDataSet account : accounts) {
+            result.add(account);
+        }
+        return result;
+    }
+
+    /**
+     * Executes {@link AccountTypeManagerImpl#findInvitableAccountTypes} and verifies the
+     * result.
+     */
+    private void verifyAccountTypes(
+            Collection<AccountWithDataSet> accounts,
+            Map<AccountTypeWithDataSet, AccountType> types,
+            AccountType... expectedInvitableTypes
+            ) {
+        Map<AccountTypeWithDataSet, AccountType> result =
+                AccountTypeManagerImpl.findAllInvitableAccountTypes(getContext(), accounts, types);
+        for (AccountType type : expectedInvitableTypes) {
+            assertTrue("Result doesn't contain type=" + type.getAccountTypeAndDataSet(),
+                    result.containsKey(type.getAccountTypeAndDataSet()));
+        }
+        final int numExcessTypes = result.size() - expectedInvitableTypes.length;
+        assertEquals("Result contains " + numExcessTypes + " excess type(s)", 0, numExcessTypes);
+    }
+
+    private static class MockAccountType extends AccountType {
+        private final String mInviteContactActivityClassName;
+
+        public MockAccountType(String type, String dataSet, String inviteContactActivityClassName) {
+            accountType = type;
+            this.dataSet = dataSet;
+            mInviteContactActivityClassName = inviteContactActivityClassName;
+        }
+
+        @Override
+        public String getInviteContactActivityClassName() {
+            return mInviteContactActivityClassName;
+        }
+
+        @Override
+        public boolean isGroupMembershipEditable() {
+            return false;
+        }
+
+        @Override
+        public boolean areContactsWritable() {
+            return false;
+        }
+    }
+}
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/ContactLoaderTest.java b/tests/src/com/android/contacts/common/model/ContactLoaderTest.java
new file mode 100644
index 0000000..9878a12
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/ContactLoaderTest.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentUris;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.test.LoaderTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.BaseAccountType;
+import com.android.contacts.common.testing.InjectedServices;
+import com.android.contacts.common.test.mocks.ContactsMockContext;
+import com.android.contacts.common.test.mocks.MockContentProvider;
+import com.android.contacts.common.test.mocks.MockContentProvider.Query;
+import com.android.contacts.common.test.mocks.MockAccountTypeManager;
+import com.android.contacts.common.util.Constants;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Runs ContactLoader tests for the the contact-detail and editor view.
+ */
+@LargeTest
+public class ContactLoaderTest extends LoaderTestCase {
+    private static final long CONTACT_ID = 1;
+    private static final long RAW_CONTACT_ID = 11;
+    private static final long DATA_ID = 21;
+    private static final String LOOKUP_KEY = "aa%12%@!";
+
+    private ContactsMockContext mMockContext;
+    private MockContentProvider mContactsProvider;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mMockContext = new ContactsMockContext(getContext());
+        mContactsProvider = mMockContext.getContactsProvider();
+
+        InjectedServices services = new InjectedServices();
+        AccountType accountType = new BaseAccountType() {
+            @Override
+            public boolean areContactsWritable() {
+                return false;
+            }
+        };
+        accountType.accountType = "mockAccountType";
+
+        AccountWithDataSet account =
+                new AccountWithDataSet("mockAccountName", "mockAccountType", null);
+
+        AccountTypeManager.setInstanceForTest(
+                new MockAccountTypeManager(
+                        new AccountType[]{accountType}, new AccountWithDataSet[]{account}));
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mMockContext = null;
+        mContactsProvider = null;
+        super.tearDown();
+    }
+
+    private Contact assertLoadContact(Uri uri) {
+        final ContactLoader loader = new ContactLoader(mMockContext, uri, true);
+        return getLoaderResultSynchronously(loader);
+    }
+
+    public void testNullUri() {
+        Contact result = assertLoadContact(null);
+        assertTrue(result.isError());
+    }
+
+    public void testEmptyUri() {
+        Contact result = assertLoadContact(Uri.EMPTY);
+        assertTrue(result.isError());
+    }
+
+    public void testInvalidUri() {
+        Contact result = assertLoadContact(Uri.parse("content://wtf"));
+        assertTrue(result.isError());
+    }
+
+    public void testLoadContactWithContactIdUri() {
+        // Use content Uris that only contain the ID
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
+        final Uri entityUri = Uri.withAppendedPath(baseUri, Contacts.Entity.CONTENT_DIRECTORY);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
+                CONTACT_ID);
+
+        ContactQueries queries = new ContactQueries();
+        mContactsProvider.expectTypeQuery(baseUri, Contacts.CONTENT_ITEM_TYPE);
+        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
+
+        Contact contact = assertLoadContact(baseUri);
+
+        assertEquals(CONTACT_ID, contact.getId());
+        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(LOOKUP_KEY, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getRawContacts().size());
+        assertEquals(1, contact.getStatuses().size());
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithOldStyleUri() {
+        // Use content Uris that only contain the ID but use the format used in Donut
+        final Uri legacyUri = ContentUris.withAppendedId(
+                Uri.parse("content://contacts"), RAW_CONTACT_ID);
+        final Uri rawContactUri = ContentUris.withAppendedId(
+                RawContacts.CONTENT_URI, RAW_CONTACT_ID);
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
+                CONTACT_ID);
+        final Uri entityUri = Uri.withAppendedPath(lookupUri, Contacts.Entity.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        queries.fetchContactIdAndLookupFromRawContactUri(rawContactUri, CONTACT_ID, LOOKUP_KEY);
+        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
+
+        Contact contact = assertLoadContact(legacyUri);
+
+        assertEquals(CONTACT_ID, contact.getId());
+        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(LOOKUP_KEY, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getRawContacts().size());
+        assertEquals(1, contact.getStatuses().size());
+        if (CompatUtils.isMarshmallowCompatible()) {
+            assertEquals(
+                    1, contact.getRawContacts().get(0).getDataItems().get(0).getCarrierPresence());
+        }
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithRawContactIdUri() {
+        // Use content Uris that only contain the ID but use the format used in Donut
+        final Uri rawContactUri = ContentUris.withAppendedId(
+                RawContacts.CONTENT_URI, RAW_CONTACT_ID);
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
+                CONTACT_ID);
+        final Uri entityUri = Uri.withAppendedPath(lookupUri, Contacts.Entity.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        mContactsProvider.expectTypeQuery(rawContactUri, RawContacts.CONTENT_ITEM_TYPE);
+        queries.fetchContactIdAndLookupFromRawContactUri(rawContactUri, CONTACT_ID, LOOKUP_KEY);
+        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
+
+        Contact contact = assertLoadContact(rawContactUri);
+
+        assertEquals(CONTACT_ID, contact.getId());
+        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(LOOKUP_KEY, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getRawContacts().size());
+        assertEquals(1, contact.getStatuses().size());
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithContactLookupUri() {
+        // Use lookup-style Uris that do not contain the Contact-ID
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
+        final Uri lookupNoIdUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY);
+        final Uri lookupUri = ContentUris.withAppendedId(lookupNoIdUri, CONTACT_ID);
+        final Uri entityUri = Uri.withAppendedPath(
+                lookupNoIdUri, Contacts.Entity.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        mContactsProvider.expectTypeQuery(lookupNoIdUri, Contacts.CONTENT_ITEM_TYPE);
+        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
+
+        Contact contact = assertLoadContact(lookupNoIdUri);
+
+        assertEquals(CONTACT_ID, contact.getId());
+        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(LOOKUP_KEY, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getRawContacts().size());
+        assertEquals(1, contact.getStatuses().size());
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithContactLookupAndIdUri() {
+        // Use lookup-style Uris that also contain the Contact-ID
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
+                CONTACT_ID);
+        final Uri entityUri = Uri.withAppendedPath(lookupUri, Contacts.Entity.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        mContactsProvider.expectTypeQuery(lookupUri, Contacts.CONTENT_ITEM_TYPE);
+        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
+
+        Contact contact = assertLoadContact(lookupUri);
+
+        assertEquals(CONTACT_ID, contact.getId());
+        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(LOOKUP_KEY, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getRawContacts().size());
+        assertEquals(1, contact.getStatuses().size());
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactWithContactLookupWithIncorrectIdUri() {
+        // Use lookup-style Uris that contain incorrect Contact-ID
+        // (we want to ensure that still the correct contact is chosen)
+        final long wrongContactId = 2;
+        final long wrongRawContactId = 12;
+
+        final String wrongLookupKey = "ab%12%@!";
+        final Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, CONTACT_ID);
+        final Uri wrongBaseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, wrongContactId);
+        final Uri lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
+                CONTACT_ID);
+        final Uri lookupWithWrongIdUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, LOOKUP_KEY),
+                wrongContactId);
+        final Uri entityUri = Uri.withAppendedPath(lookupWithWrongIdUri,
+                Contacts.Entity.CONTENT_DIRECTORY);
+
+        ContactQueries queries = new ContactQueries();
+        mContactsProvider.expectTypeQuery(lookupWithWrongIdUri, Contacts.CONTENT_ITEM_TYPE);
+        queries.fetchAllData(entityUri, CONTACT_ID, RAW_CONTACT_ID, DATA_ID, LOOKUP_KEY);
+
+        Contact contact = assertLoadContact(lookupWithWrongIdUri);
+
+        assertEquals(CONTACT_ID, contact.getId());
+        assertEquals(RAW_CONTACT_ID, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals(LOOKUP_KEY, contact.getLookupKey());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getRawContacts().size());
+        assertEquals(1, contact.getStatuses().size());
+
+        mContactsProvider.verify();
+    }
+
+    public void testLoadContactReturnDirectoryContactWithoutDisplayName() throws JSONException {
+        // Use lookup-style Uri that contains encoded json object which encapsulates the
+        // directory contact. The test json object is:
+        // {
+        //   display_name_source": 40,
+        //   "vnd.android.cursor.item\/contact":{"email":{"data1":"test@google.com" }}
+        // }
+        JSONObject itemJson = new JSONObject();
+        itemJson.put("email", new JSONObject().put("data1", "test@google.com"));
+        JSONObject json = new JSONObject();
+        json.put(Contacts.NAME_RAW_CONTACT_ID, CONTACT_ID);
+        json.put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
+        json.put(Contacts.CONTENT_ITEM_TYPE, itemJson);
+
+        final Uri lookupUri = Contacts.CONTENT_LOOKUP_URI.buildUpon()
+                .encodedFragment(json.toString())
+                .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, "1")
+                .appendPath(Constants.LOOKUP_URI_ENCODED).build();
+
+        mContactsProvider.expectTypeQuery(lookupUri, Contacts.CONTENT_ITEM_TYPE);
+        Contact contact = assertLoadContact(lookupUri);
+
+        assertEquals(-1, contact.getId());
+        assertEquals(-1, contact.getNameRawContactId());
+        assertEquals(DisplayNameSources.STRUCTURED_NAME, contact.getDisplayNameSource());
+        assertEquals("", contact.getDisplayName());
+        assertEquals(lookupUri, contact.getLookupUri());
+        assertEquals(1, contact.getRawContacts().size());
+        mContactsProvider.verify();
+    }
+
+    class ContactQueries {
+        public void fetchAllData(
+                Uri baseUri, long contactId, long rawContactId, long dataId, String encodedLookup) {
+            final String[] COLUMNS_INTERNAL = new String[] {
+                    Contacts.NAME_RAW_CONTACT_ID, Contacts.DISPLAY_NAME_SOURCE,
+                    Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME,
+                    Contacts.DISPLAY_NAME_ALTERNATIVE, Contacts.PHONETIC_NAME,
+                    Contacts.PHOTO_ID, Contacts.STARRED, Contacts.CONTACT_PRESENCE,
+                    Contacts.CONTACT_STATUS, Contacts.CONTACT_STATUS_TIMESTAMP,
+                    Contacts.CONTACT_STATUS_RES_PACKAGE, Contacts.CONTACT_STATUS_LABEL,
+
+                    Contacts.Entity.CONTACT_ID,
+                    Contacts.Entity.RAW_CONTACT_ID,
+
+                    RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE,
+                    RawContacts.DATA_SET,
+                    RawContacts.DIRTY, RawContacts.VERSION, RawContacts.SOURCE_ID,
+                    RawContacts.SYNC1, RawContacts.SYNC2, RawContacts.SYNC3, RawContacts.SYNC4,
+                    RawContacts.DELETED,
+
+                    Contacts.Entity.DATA_ID,
+
+                    Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
+                    Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10,
+                    Data.DATA11, Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
+                    Data.SYNC1, Data.SYNC2, Data.SYNC3, Data.SYNC4,
+                    Data.DATA_VERSION, Data.IS_PRIMARY,
+                    Data.IS_SUPER_PRIMARY, Data.MIMETYPE,
+
+                    GroupMembership.GROUP_SOURCE_ID,
+
+                    Data.PRESENCE, Data.CHAT_CAPABILITY,
+                    Data.STATUS, Data.STATUS_RES_PACKAGE, Data.STATUS_ICON,
+                    Data.STATUS_LABEL, Data.STATUS_TIMESTAMP,
+
+                    Contacts.PHOTO_URI,
+
+                    Contacts.SEND_TO_VOICEMAIL,
+                    Contacts.CUSTOM_RINGTONE,
+                    Contacts.IS_USER_PROFILE,
+
+                    Data.TIMES_USED,
+                    Data.LAST_TIME_USED
+            };
+
+            List<String> projectionList = Lists.newArrayList(COLUMNS_INTERNAL);
+            if (CompatUtils.isMarshmallowCompatible()) {
+                projectionList.add(Data.CARRIER_PRESENCE);
+            }
+            final String[] COLUMNS = projectionList.toArray(new String[projectionList.size()]);
+
+            final Object[] ROWS_INTERNAL = new Object[] {
+                    rawContactId, 40,
+                    "aa%12%@!", "John Doe", "Doe, John", "jdo",
+                    0, 0, StatusUpdates.AVAILABLE,
+                    "Having lunch", 0,
+                    "mockPkg1", 10,
+
+                    contactId,
+                    rawContactId,
+
+                    "mockAccountName", "mockAccountType", null,
+                    0, 1, 0,
+                    "sync1", "sync2", "sync3", "sync4",
+                    0,
+
+                    dataId,
+
+                    "dat1", "dat2", "dat3", "dat4", "dat5",
+                    "dat6", "dat7", "dat8", "dat9", "dat10",
+                    "dat11", "dat12", "dat13", "dat14", "dat15",
+                    "syn1", "syn2", "syn3", "syn4",
+
+                    0, 0,
+                    0, StructuredName.CONTENT_ITEM_TYPE,
+
+                    "groupId",
+
+                    StatusUpdates.INVISIBLE, null,
+                    "Having dinner", "mockPkg3", 0,
+                    20, 0,
+
+                    "content:some.photo.uri",
+
+                    0,
+                    null,
+                    0,
+
+                    0,
+                    0
+            };
+
+            List<Object> rowsList = Lists.newArrayList(ROWS_INTERNAL);
+            if (CompatUtils.isMarshmallowCompatible()) {
+                rowsList.add(Data.CARRIER_PRESENCE_VT_CAPABLE);
+            }
+            final Object[] ROWS = rowsList.toArray(new Object[rowsList.size()]);
+
+            mContactsProvider.expectQuery(baseUri)
+                    .withProjection(COLUMNS)
+                    .withSortOrder(Contacts.Entity.RAW_CONTACT_ID)
+                    .returnRow(ROWS);
+        }
+
+        void fetchLookupAndId(final Uri sourceUri, final long expectedContactId,
+                final String expectedEncodedLookup) {
+            mContactsProvider.expectQuery(sourceUri)
+                    .withProjection(Contacts.LOOKUP_KEY, Contacts._ID)
+                    .returnRow(expectedEncodedLookup, expectedContactId);
+        }
+
+        void fetchContactIdAndLookupFromRawContactUri(final Uri rawContactUri,
+                final long expectedContactId, final String expectedEncodedLookup) {
+            // TODO: use a lighter query by joining rawcontacts with contacts in provider
+            // (See ContactContracts.java)
+            final Uri dataUri = Uri.withAppendedPath(rawContactUri,
+                    RawContacts.Data.CONTENT_DIRECTORY);
+            mContactsProvider.expectQuery(dataUri)
+                    .withProjection(RawContacts.CONTACT_ID, Contacts.LOOKUP_KEY)
+                    .returnRow(expectedContactId, expectedEncodedLookup);
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/common/model/RawContactTest.java b/tests/src/com/android/contacts/common/model/RawContactTest.java
new file mode 100644
index 0000000..b8e03c3
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/RawContactTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.model;
+
+import android.content.ContentValues;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.contacts.common.model.RawContact;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit test for {@link RawContact}.
+ */
+public class RawContactTest extends TestCase {
+
+    private RawContact buildRawContact() {
+        final ContentValues values = new ContentValues();
+        values.put("key1", "value1");
+        values.put("key2", "value2");
+
+        final ContentValues dataItem = new ContentValues();
+        dataItem.put("key3", "value3");
+        dataItem.put("key4", "value4");
+
+        final RawContact contact = new RawContact(values);
+        contact.addDataItemValues(dataItem);
+
+        return contact;
+    }
+
+    private RawContact buildRawContact2() {
+        final ContentValues values = new ContentValues();
+        values.put("key11", "value11");
+        values.put("key22", "value22");
+
+        final ContentValues dataItem = new ContentValues();
+        dataItem.put("key33", "value33");
+        dataItem.put("key44", "value44");
+
+        final RawContact contact = new RawContact(values);
+        contact.addDataItemValues(dataItem);
+
+        return contact;
+    }
+
+    public void testNotEquals() {
+        final RawContact one = buildRawContact();
+        final RawContact two = buildRawContact2();
+        assertFalse(one.equals(two));
+    }
+
+    public void testEquals() {
+        assertEquals(buildRawContact(), buildRawContact());
+    }
+
+    public void testParcelable() {
+        assertParcelableEquals(buildRawContact());
+    }
+
+    private RawContact.NamedDataItem buildNamedDataItem() {
+        final ContentValues values = new ContentValues();
+        values.put("key1", "value1");
+        values.put("key2", "value2");
+        final Uri uri = Uri.fromParts("content:", "ssp", "fragment");
+
+        return new RawContact.NamedDataItem(uri, values);
+    }
+
+    private RawContact.NamedDataItem buildNamedDataItem2() {
+        final ContentValues values = new ContentValues();
+        values.put("key11", "value11");
+        values.put("key22", "value22");
+        final Uri uri = Uri.fromParts("content:", "blah", "blah");
+
+        return new RawContact.NamedDataItem(uri, values);
+    }
+
+    public void testNamedDataItemEquals() {
+        assertEquals(buildNamedDataItem(), buildNamedDataItem());
+    }
+
+    public void testNamedDataItemNotEquals() {
+        assertFalse(buildNamedDataItem().equals(buildNamedDataItem2()));
+    }
+
+    public void testNamedDataItemParcelable() {
+        assertParcelableEquals(buildNamedDataItem());
+    }
+
+    private void assertParcelableEquals(Parcelable parcelable) {
+        final Parcel parcel = Parcel.obtain();
+        try {
+            parcel.writeParcelable(parcelable, 0);
+            parcel.setDataPosition(0);
+
+            Parcelable out = parcel.readParcelable(parcelable.getClass().getClassLoader());
+            assertEquals(parcelable, out);
+        } finally {
+            parcel.recycle();
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/common/model/ValuesDeltaTests.java b/tests/src/com/android/contacts/common/model/ValuesDeltaTests.java
new file mode 100644
index 0000000..77bf456
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/ValuesDeltaTests.java
@@ -0,0 +1,90 @@
+/*
+ * 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.model;
+
+import android.content.ContentProviderOperation.Builder;
+import android.content.ContentValues;
+import android.os.Build;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Data;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.model.BuilderWrapper;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for  {@link ValuesDelta}. These tests
+ * focus on passing changes across {@link android.os.Parcel}, and verifying that they
+ * correctly build expected "diff" operations.
+ */
+@SmallTest
+public class ValuesDeltaTests extends TestCase {
+
+    public static final long TEST_PHONE_ID = 24;
+
+    public static final String TEST_PHONE_NUMBER_1 = "218-555-1111";
+    public static final String TEST_PHONE_NUMBER_2 = "218-555-2222";
+
+    public void testValuesDiffInsert() {
+        final ContentValues after = new ContentValues();
+        after.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+
+        final ValuesDelta values = ValuesDelta.fromAfter(after);
+
+        // Should produce an insert action
+        final BuilderWrapper builderWrapper = values.buildDiffWrapper(Data.CONTENT_URI);
+        final boolean isInsert = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+                ? builderWrapper.getBuilder().build().isInsert()
+                : builderWrapper.getType() == CompatUtils.TYPE_INSERT;
+        assertTrue("Didn't produce insert action", isInsert);
+    }
+
+    /**
+     * Test that {@link ValuesDelta#buildDiff(android.net.Uri)} is correctly
+     * built for insert, update, and delete cases. Note this only tests behavior
+     * for individual {@link Data} rows.
+     */
+    public void testValuesDiffNone() {
+        final ContentValues before = new ContentValues();
+        before.put(Data._ID, TEST_PHONE_ID);
+        before.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
+
+        final ValuesDelta values = ValuesDelta.fromBefore(before);
+
+        // None action shouldn't produce a builder
+        final Builder builder = values.buildDiff(Data.CONTENT_URI);
+        assertNull("None action produced a builder", builder);
+    }
+
+    public void testValuesDiffUpdate() {
+        final ContentValues before = new ContentValues();
+        before.put(Data._ID, TEST_PHONE_ID);
+        before.put(Phone.NUMBER, TEST_PHONE_NUMBER_1);
+
+        final ValuesDelta values = ValuesDelta.fromBefore(before);
+        values.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
+
+        // Should produce an update action
+        final BuilderWrapper builderWrapper = values.buildDiffWrapper(Data.CONTENT_URI);
+        final boolean isUpdate = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+                ? builderWrapper.getBuilder().build().isUpdate()
+                : builderWrapper.getType() == CompatUtils.TYPE_UPDATE;
+        assertTrue("Didn't produce update action", isUpdate);
+    }
+}
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..e204722
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/account/AccountTypeTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.contacts.common.tests.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 InstrumentationTestCase {
+    public void testGetResourceText() {
+        // In this test we use the test package itself as an external package.
+        final String packageName = getInstrumentation().getContext().getPackageName();
+
+        final Context c = getInstrumentation().getTargetContext();
+        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(getInstrumentation().getContext().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 = getInstrumentation().getContext().getPackageName();
+        final Context c = getInstrumentation().getTargetContext();
+
+        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(getInstrumentation().getContext().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(
+                getInstrumentation().getTargetContext()).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..50a5110
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/account/ExternalAccountTypeTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.util.Log;
+
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.tests.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 InstrumentationTestCase {
+
+    @Suppress
+    public void testResolveExternalResId() {
+        final Context c = getInstrumentation().getTargetContext();
+        // In this test we use the test package itself as an external package.
+        final String packageName = getInstrumentation().getContext().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 will be initialized, but empty.
+     */
+    public void testNoPackage() {
+        final ExternalAccountType type = new ExternalAccountType(getInstrumentation().getTargetContext(),
+                "!!!no such package name!!!", false);
+        assertTrue(type.isInitialized());
+    }
+
+    /**
+     * Initialize with the test package itself and see if EditSchema is correctly parsed.
+     */
+    @Suppress
+    public void testEditSchema() {
+        final ExternalAccountType type = new ExternalAccountType(getInstrumentation().getTargetContext(),
+                getInstrumentation().getContext().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(getInstrumentation().getTargetContext(),
+                getInstrumentation().getContext().getPackageName(), false,
+                getInstrumentation().getContext().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(
+                getInstrumentation().getTargetContext(), 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(getInstrumentation().getTargetContext(),
+                getInstrumentation().getContext().getPackageName(), false,
+                getInstrumentation().getContext().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(getInstrumentation().getTargetContext(),
+                getInstrumentation().getContext().getPackageName(), false,
+                getInstrumentation().getContext().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());
+
+        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());
+
+        kind = type.getKindForMimetype("vnd.android.cursor.item/xyz");
+        assertNotNull(kind);
+        assertEquals("data5", ((BaseAccountType.SimpleInflater) kind.actionHeader)
+                .getColumnNameForTest());
+        assertEquals("data6", ((BaseAccountType.SimpleInflater) kind.actionBody)
+                .getColumnNameForTest());
+    }
+
+    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/model/dataitem/DataItemTests.java b/tests/src/com/android/contacts/common/model/dataitem/DataItemTests.java
new file mode 100644
index 0000000..ec1a8da
--- /dev/null
+++ b/tests/src/com/android/contacts/common/model/dataitem/DataItemTests.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2014 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.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.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.provider.ContactsContract.Contacts.Data;
+import android.provider.ContactsContract.Contacts.Entity;
+import android.test.AndroidTestCase;
+
+import com.android.contacts.common.Collapser;
+import com.android.contacts.common.model.account.AccountType.EditType;
+import com.android.contacts.common.model.account.BaseAccountType;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.android.contacts.common.model.dataitem.DataKind;
+
+import java.lang.Math;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test case for {@link DataItem}.
+ */
+public class DataItemTests extends AndroidTestCase {
+
+    private ContentValues mValues1;
+    private ContentValues mValues2;
+    private ContentValues mValues3;
+    private ContentValues mValues4;
+    private GoogleAccountType mGoogleAccountType;
+
+    @Override
+    protected void setUp() {
+        mValues1 = new ContentValues();
+        mValues2 = new ContentValues();
+        mValues3 = new ContentValues();
+        mValues4 = new ContentValues();
+
+        mValues1.put(Data._ID, 1);
+        mValues2.put(Data._ID, 2);
+        mValues3.put(Data._ID, 3);
+        mValues4.put(Data._ID, 4);
+
+        mGoogleAccountType = new GoogleAccountType(getContext(), "packageName");
+    }
+
+    private List<DataItem> createDataItemsAndCollapse(DataKind kind, ContentValues... values) {
+        final List<DataItem> dataList = new ArrayList<>(values.length);
+        for (ContentValues value : values) {
+            final DataItem data = DataItem.createFrom(value);
+            data.setDataKind(kind);
+            dataList.add(data);
+        }
+        Collapser.collapseList(dataList, getContext());
+        return dataList;
+    }
+
+    public void testDataItemCollapsing_genericDataItemFields() {
+        mValues1.put(Data.IS_SUPER_PRIMARY, 1);
+        mValues2.put(Data.IS_PRIMARY, 0);
+
+        mValues1.put(Entity.TIMES_USED, 5);
+        mValues2.put(Entity.TIMES_USED, 4);
+
+        mValues1.put(Entity.LAST_TIME_USED, 555);
+        mValues2.put(Entity.LAST_TIME_USED, 999);
+
+        final DataKind kind = new DataKind("test.mimetype", 0, 0, false);
+        kind.actionBody = new BaseAccountType.SimpleInflater(0);
+        kind.typeList = new ArrayList<>();
+        kind.typeList.add(new EditType(1, -1));
+        kind.typeList.add(new EditType(2, -1));
+        kind.typeColumn = Data.DATA2;
+
+        mValues1.put(kind.typeColumn, 2);
+        mValues2.put(kind.typeColumn, 1);
+
+        final List<DataItem> dataList = createDataItemsAndCollapse(kind, mValues1, mValues2);
+
+        assertEquals(1, dataList.size());
+        assertEquals(true, dataList.get(0).isSuperPrimary());
+        assertEquals(true, dataList.get(0).isPrimary());
+        assertEquals(9, (int) dataList.get(0).getTimesUsed());
+        assertEquals(999L, (long) dataList.get(0).getLastTimeUsed());
+        assertEquals(1, dataList.get(0).getKindTypeColumn(kind));
+    }
+
+    public void testDataItemCollapsing_email() {
+        final String email1 = "email1@google.com";
+        final String email2 = "email2@google.com";
+
+        mValues1.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+
+        mValues1.put(Email.ADDRESS, email1);
+        mValues2.put(Email.ADDRESS, email1);
+        mValues3.put(Email.ADDRESS, email2);
+
+        mValues1.put(Email.TYPE, Email.TYPE_MOBILE);
+        mValues2.put(Email.TYPE, Email.TYPE_HOME);
+        mValues3.put(Email.TYPE, Email.TYPE_WORK);
+
+        final DataKind kind = mGoogleAccountType.getKindForMimetype(Email.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3);
+
+        assertEquals(2, dataList.size());
+        assertEquals(email1, ((EmailDataItem) dataList.get(0)).getAddress());
+        assertEquals(email2, ((EmailDataItem) dataList.get(1)).getAddress());
+        assertEquals(Math.min(Email.TYPE_MOBILE, Email.TYPE_HOME),
+                ((EmailDataItem) dataList.get(0)).getKindTypeColumn(kind));
+    }
+
+    public void testDataItemCollapsing_event() {
+        final String date1 = "2014-01-01";
+        final String date2 = "2014-02-02";
+        final String customLabel1 = "custom label1";
+        final String customLabel2 = "custom label2";
+
+        mValues1.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
+        mValues4.put(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
+
+        mValues1.put(Event.START_DATE, date1);
+        mValues2.put(Event.START_DATE, date1);
+        mValues3.put(Event.START_DATE, date1);
+        mValues4.put(Event.START_DATE, date2);
+
+        mValues1.put(Event.TYPE, Event.TYPE_CUSTOM);
+        mValues2.put(Event.TYPE, Event.TYPE_CUSTOM);
+        mValues3.put(Event.TYPE, Event.TYPE_CUSTOM);
+        mValues4.put(Event.TYPE, Event.TYPE_ANNIVERSARY);
+
+        mValues1.put(Event.LABEL, customLabel1);
+        mValues2.put(Event.LABEL, customLabel1);
+        mValues3.put(Event.LABEL, customLabel2);
+
+        final DataKind kind = mGoogleAccountType.getKindForMimetype(Event.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3, mValues4);
+
+        assertEquals(3, dataList.size());
+        assertEquals(customLabel1, ((EventDataItem) dataList.get(0)).getLabel());
+        assertEquals(customLabel2, ((EventDataItem) dataList.get(1)).getLabel());
+        assertEquals(date2, ((EventDataItem) dataList.get(2)).getStartDate());
+    }
+
+    public void testDataItemCollapsing_im() {
+        final String address1 = "address 1";
+        final String address2 = "address 2";
+        final String customProtocol1 = "custom 1";
+        final String customProtocol2 = "custom 2";
+
+        mValues1.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        mValues4.put(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+
+        mValues1.put(Im.DATA, address1);
+        mValues2.put(Im.DATA, address1);
+        mValues3.put(Im.DATA, address1);
+        mValues4.put(Im.DATA, address2);
+
+        mValues1.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM);
+        mValues2.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM);
+        mValues3.put(Im.PROTOCOL, Im.PROTOCOL_CUSTOM);
+        mValues4.put(Im.PROTOCOL, Im.PROTOCOL_AIM);
+
+        mValues1.put(Im.CUSTOM_PROTOCOL, customProtocol1);
+        mValues2.put(Im.CUSTOM_PROTOCOL, customProtocol1);
+        mValues3.put(Im.CUSTOM_PROTOCOL, customProtocol2);
+
+        final DataKind kind = mGoogleAccountType.getKindForMimetype(Im.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3, mValues4);
+
+        assertEquals(3, dataList.size());
+        assertEquals(address1, ((ImDataItem) dataList.get(0)).getData());
+        assertEquals(address1, ((ImDataItem) dataList.get(1)).getData());
+        assertEquals(address2, ((ImDataItem) dataList.get(2)).getData());
+
+        assertEquals(customProtocol1, ((ImDataItem) dataList.get(0)).getCustomProtocol());
+        assertEquals(customProtocol2, ((ImDataItem) dataList.get(1)).getCustomProtocol());
+        assertEquals(Im.PROTOCOL_AIM, (int) ((ImDataItem) dataList.get(2)).getProtocol());
+    }
+
+    public void testDataItemCollapsing_nickname() {
+        final String nickname1 = "nickname 1";
+        final String nickname2 = "nickname 2";
+
+        mValues1.put(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
+
+        mValues1.put(Nickname.NAME, nickname1);
+        mValues2.put(Nickname.NAME, nickname1);
+        mValues3.put(Nickname.NAME, nickname2);
+
+        final DataKind kind = mGoogleAccountType.getKindForMimetype(Nickname.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3);
+
+        assertEquals(2, dataList.size());
+        assertEquals(nickname1, ((NicknameDataItem) dataList.get(0)).getName());
+        assertEquals(nickname2, ((NicknameDataItem) dataList.get(1)).getName());
+    }
+
+    public void testDataItemCollapsing_note() {
+        final String note1 = "note 1";
+        final String note2 = "note 2";
+
+        mValues1.put(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
+
+        mValues1.put(Note.NOTE, note1);
+        mValues2.put(Note.NOTE, note1);
+        mValues3.put(Note.NOTE, note2);
+
+        DataKind kind = mGoogleAccountType.getKindForMimetype(Note.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3);
+
+        assertEquals(2, dataList.size());
+        assertEquals(note1, ((NoteDataItem) dataList.get(0)).getNote());
+        assertEquals(note2, ((NoteDataItem) dataList.get(1)).getNote());
+    }
+
+    public void testDataItemCollapsing_organization() {
+        final String company1 = "company1";
+        final String company2 = "company2";
+        final String title1 = "title1";
+        final String title2 = "title2";
+
+        mValues1.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+        mValues4.put(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
+
+        mValues1.put(Organization.COMPANY, company1);
+        mValues2.put(Organization.COMPANY, company1);
+        mValues3.put(Organization.COMPANY, company1);
+        mValues4.put(Organization.COMPANY, company2);
+
+        mValues1.put(Organization.TITLE, title1);
+        mValues2.put(Organization.TITLE, title1);
+        mValues3.put(Organization.TITLE, title2);
+        mValues4.put(Organization.TITLE, title1);
+
+        final DataKind kind =
+                mGoogleAccountType.getKindForMimetype(Organization.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3, mValues4);
+
+        assertEquals(3, dataList.size());
+        assertEquals(company1, ((OrganizationDataItem) dataList.get(0)).getCompany());
+        assertEquals(company1, ((OrganizationDataItem) dataList.get(1)).getCompany());
+        assertEquals(company2, ((OrganizationDataItem) dataList.get(2)).getCompany());
+
+        assertEquals(title1, ((OrganizationDataItem) dataList.get(0)).getTitle());
+        assertEquals(title2, ((OrganizationDataItem) dataList.get(1)).getTitle());
+    }
+
+    public void testDataItemCollapsing_phone() {
+        final String phone1 = "111-111-1111";
+        final String phone1a = "1111111111";
+        final String phone2 = "222-222-2222";
+
+        mValues1.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+
+        mValues1.put(Phone.NUMBER, phone1);
+        mValues2.put(Phone.NUMBER, phone1a);
+        mValues3.put(Phone.NUMBER, phone2);
+
+        mValues1.put(Phone.TYPE, Phone.TYPE_MOBILE);
+        mValues2.put(Phone.TYPE, Phone.TYPE_HOME);
+        mValues3.put(Phone.TYPE, Phone.TYPE_WORK);
+
+        final DataKind kind = mGoogleAccountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3);
+        assertEquals(2, dataList.size());
+        assertEquals(phone1, ((PhoneDataItem) dataList.get(0)).getNumber());
+        assertEquals(phone2, ((PhoneDataItem) dataList.get(1)).getNumber());
+        assertEquals(Phone.TYPE_MOBILE,
+                ((PhoneDataItem) dataList.get(0)).getKindTypeColumn(kind));
+    }
+
+    public void testDataItemCollapsing_relation() {
+        final String name1 = "name1";
+        final String name2 = "name2";
+        final String customRelation1 = "custom relation 1";
+        final String customRelation2 = "custom relation 2";
+
+        mValues1.put(Data.MIMETYPE, Relation.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, Relation.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, Relation.CONTENT_ITEM_TYPE);
+        mValues4.put(Data.MIMETYPE, Relation.CONTENT_ITEM_TYPE);
+
+        mValues1.put(Relation.NAME, name1);
+        mValues2.put(Relation.NAME, name1);
+        mValues3.put(Relation.NAME, name1);
+        mValues4.put(Relation.NAME, name2);
+
+        mValues1.put(Relation.TYPE, Relation.TYPE_CUSTOM);
+        mValues2.put(Relation.TYPE, Relation.TYPE_CUSTOM);
+        mValues3.put(Relation.TYPE, Relation.TYPE_CUSTOM);
+        mValues4.put(Relation.TYPE, Relation.TYPE_BROTHER);
+
+        mValues1.put(Relation.LABEL, customRelation1);
+        mValues2.put(Relation.LABEL, customRelation1);
+        mValues3.put(Relation.LABEL, customRelation2);
+
+        final DataKind kind = mGoogleAccountType.getKindForMimetype(Relation.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3, mValues4);
+
+        assertEquals(3, dataList.size());
+        assertEquals(name1, ((RelationDataItem) dataList.get(0)).getName());
+        assertEquals(name2, ((RelationDataItem) dataList.get(2)).getName());
+
+        assertEquals(customRelation1, ((RelationDataItem) dataList.get(0)).getLabel());
+        assertEquals(customRelation2, ((RelationDataItem) dataList.get(1)).getLabel());
+    }
+
+    public void testDataItemCollapsing_sip() {
+        final String sip1 = "sip 1";
+        final String sip2 = "sip 2";
+
+        mValues1.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+
+        mValues1.put(SipAddress.SIP_ADDRESS, sip1);
+        mValues2.put(SipAddress.SIP_ADDRESS, sip1);
+        mValues3.put(SipAddress.SIP_ADDRESS, sip2);
+
+        mValues1.put(SipAddress.TYPE, SipAddress.TYPE_WORK);
+        mValues2.put(SipAddress.TYPE, SipAddress.TYPE_HOME);
+        mValues3.put(SipAddress.TYPE, SipAddress.TYPE_WORK);
+
+        final DataKind kind = mGoogleAccountType.getKindForMimetype(SipAddress.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3);
+
+        assertEquals(2, dataList.size());
+        assertEquals(sip1, ((SipAddressDataItem) dataList.get(0)).getSipAddress());
+        assertEquals(sip2, ((SipAddressDataItem) dataList.get(1)).getSipAddress());
+    }
+
+    public void testDataItemCollapsing_structuredName() {
+        final String displayName1 = "Display Name 1";
+        final String displayName2 = "Display Name 2";
+
+        mValues1.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+
+        mValues1.put(StructuredName.DISPLAY_NAME, displayName1);
+        mValues2.put(StructuredName.DISPLAY_NAME, displayName1);
+        mValues3.put(StructuredName.DISPLAY_NAME, displayName2);
+
+        final DataKind kind =
+                mGoogleAccountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3);
+
+        assertEquals(2, dataList.size());
+        assertEquals(displayName1, ((StructuredNameDataItem) dataList.get(0)).getDisplayName());
+        assertEquals(displayName2, ((StructuredNameDataItem) dataList.get(1)).getDisplayName());
+    }
+
+    public void testDataItemCollapsing_structuredPostal() {
+        final String formattedAddress1 = "Formatted Address 1";
+        final String formattedAddress2 = "Formatted Address 2";
+
+        mValues1.put(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
+
+        mValues1.put(StructuredPostal.FORMATTED_ADDRESS, formattedAddress1);
+        mValues2.put(StructuredPostal.FORMATTED_ADDRESS, formattedAddress1);
+        mValues3.put(StructuredPostal.FORMATTED_ADDRESS, formattedAddress2);
+
+        final DataKind kind =
+                mGoogleAccountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3);
+
+        assertEquals(2, dataList.size());
+        assertEquals(formattedAddress1,
+                ((StructuredPostalDataItem) dataList.get(0)).getFormattedAddress());
+        assertEquals(formattedAddress2,
+                ((StructuredPostalDataItem) dataList.get(1)).getFormattedAddress());
+    }
+
+    public void testDataItemCollapsing_website() {
+        final String url1 = "www.url1.com";
+        final String url2 = "www.url2.com";
+
+        mValues1.put(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
+        mValues2.put(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
+        mValues3.put(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
+
+        mValues1.put(Website.URL, url1);
+        mValues2.put(Website.URL, url1);
+        mValues3.put(Website.URL, url2);
+
+        final DataKind kind = mGoogleAccountType.getKindForMimetype(Website.CONTENT_ITEM_TYPE);
+
+        final List<DataItem> dataList =
+                createDataItemsAndCollapse(kind, mValues1, mValues2, mValues3);
+
+        assertEquals(2, dataList.size());
+        assertEquals(url1, ((WebsiteDataItem) dataList.get(0)).getUrl());
+        assertEquals(url2, ((WebsiteDataItem) dataList.get(1)).getUrl());
+    }
+}
diff --git a/tests/src/com/android/contacts/common/preference/ContactsPreferencesTest.java b/tests/src/com/android/contacts/common/preference/ContactsPreferencesTest.java
new file mode 100644
index 0000000..26e811d
--- /dev/null
+++ b/tests/src/com/android/contacts/common/preference/ContactsPreferencesTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2015 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.preference;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.test.AndroidTestCase;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import junit.framework.Assert;
+
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@MediumTest
+public class ContactsPreferencesTest extends InstrumentationTestCase {
+
+    private static final String ACCOUNT_KEY = "ACCOUNT_KEY";
+
+    @Mock private Context mContext;
+    @Mock private Resources mResources;
+    @Mock private SharedPreferences mSharedPreferences;
+
+    private ContactsPreferences mContactsPreferences;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        System.setProperty("dexmaker.dexcache",
+                getInstrumentation().getTargetContext().getCacheDir().getPath());
+        MockitoAnnotations.initMocks(this);
+
+        Mockito.when(mContext.getResources()).thenReturn(mResources);
+        Mockito.when(mResources.getString(Mockito.anyInt()))
+                .thenReturn(ACCOUNT_KEY); // contact_editor_default_account_key
+
+        Mockito.when(mContext.getSharedPreferences(Mockito.anyString(), Mockito.anyInt()))
+                .thenReturn(mSharedPreferences);
+        Mockito.when(mSharedPreferences.contains(ContactsPreferences.SORT_ORDER_KEY))
+                .thenReturn(true);
+        Mockito.when(mSharedPreferences.contains(ContactsPreferences.DISPLAY_ORDER_KEY))
+                .thenReturn(true);
+
+        mContactsPreferences = new ContactsPreferences(mContext);
+    }
+
+    public void testGetSortOrderDefault() {
+        Mockito.when(mResources.getBoolean(Mockito.anyInt())).thenReturn(
+                false, // R.bool.config_sort_order_user_changeable
+                true // R.bool.config_default_sort_order_primary
+        );
+        Assert.assertEquals(ContactsPreferences.SORT_ORDER_PRIMARY,
+                mContactsPreferences.getSortOrder());
+    }
+
+    public void testGetSortOrder() {
+        Mockito.when(mResources.getBoolean(Mockito.anyInt())).thenReturn(
+                true // R.bool.config_sort_order_user_changeable
+        );
+        Mockito.when(mSharedPreferences.getInt(Mockito.eq(ContactsPreferences.SORT_ORDER_KEY),
+                Mockito.anyInt())).thenReturn(ContactsPreferences.SORT_ORDER_PRIMARY);
+        Assert.assertEquals(ContactsPreferences.SORT_ORDER_PRIMARY,
+                mContactsPreferences.getSortOrder());
+    }
+
+    public void testGetDisplayOrderDefault() {
+        Mockito.when(mResources.getBoolean(Mockito.anyInt())).thenReturn(
+                false, // R.bool.config_display_order_user_changeable
+                true // R.bool.config_default_display_order_primary
+        );
+        Assert.assertEquals(ContactsPreferences.DISPLAY_ORDER_PRIMARY,
+                mContactsPreferences.getDisplayOrder());
+    }
+
+    public void testGetDisplayOrder() {
+        Mockito.when(mResources.getBoolean(Mockito.anyInt())).thenReturn(
+                true // R.bool.config_display_order_user_changeable
+        );
+        Mockito.when(mSharedPreferences.getInt(Mockito.eq(ContactsPreferences.DISPLAY_ORDER_KEY),
+                Mockito.anyInt())).thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY);
+        Assert.assertEquals(ContactsPreferences.DISPLAY_ORDER_PRIMARY,
+                mContactsPreferences.getDisplayOrder());
+    }
+
+    public void testRefreshSortOrder() throws InterruptedException {
+        Mockito.when(mResources.getBoolean(Mockito.anyInt())).thenReturn(
+                true // R.bool.config_sort_order_user_changeable
+        );
+        Mockito.when(mSharedPreferences.getInt(Mockito.eq(ContactsPreferences.SORT_ORDER_KEY),
+                Mockito.anyInt())).thenReturn(ContactsPreferences.SORT_ORDER_PRIMARY,
+                ContactsPreferences.SORT_ORDER_ALTERNATIVE);
+
+        Assert.assertEquals(ContactsPreferences.SORT_ORDER_PRIMARY,
+                mContactsPreferences.getSortOrder());
+        mContactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY);
+
+        Assert.assertEquals(ContactsPreferences.SORT_ORDER_ALTERNATIVE,
+                mContactsPreferences.getSortOrder());
+    }
+
+    public void testRefreshDisplayOrder() throws InterruptedException {
+        Mockito.when(mResources.getBoolean(Mockito.anyInt())).thenReturn(
+                true // R.bool.config_display_order_user_changeable
+        );
+        Mockito.when(mSharedPreferences.getInt(Mockito.eq(ContactsPreferences.DISPLAY_ORDER_KEY),
+                Mockito.anyInt())).thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY,
+                ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE);
+
+        Assert.assertEquals(ContactsPreferences.DISPLAY_ORDER_PRIMARY,
+                mContactsPreferences.getDisplayOrder());
+        mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
+
+        Assert.assertEquals(ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE,
+                mContactsPreferences.getDisplayOrder());
+    }
+
+    public void testRefreshDefaultAccount() throws InterruptedException {
+        Mockito.when(mResources.getBoolean(Mockito.anyInt())).thenReturn(
+                true // R.bool.config_default_account_user_changeable
+        );
+
+        Mockito.when(mSharedPreferences.getString(Mockito.eq(ACCOUNT_KEY), Mockito.anyString()))
+                .thenReturn(new AccountWithDataSet("name1", "type1", "dataset1").stringify(),
+                        new AccountWithDataSet("name2", "type2", "dataset2").stringify());
+
+        Assert.assertEquals("name1", mContactsPreferences.getDefaultAccount());
+        mContactsPreferences.refreshValue(ACCOUNT_KEY);
+
+        Assert.assertEquals("name2", mContactsPreferences.getDefaultAccount());
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/FragmentTestActivity.java b/tests/src/com/android/contacts/common/test/FragmentTestActivity.java
new file mode 100644
index 0000000..5ae2d95
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/FragmentTestActivity.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.test;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+/**
+ * An activity that is used for testing fragments.  A unit test starts this
+ * activity, adds a fragment and then tests the fragment.
+ */
+public class FragmentTestActivity extends Activity {
+
+    public final static int LAYOUT_ID = 1;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Normally fragment/activity onStart() methods will not be called when screen is locked.
+        // Use the following flags to ensure that activities can be show for testing.
+        final Window window = getWindow();
+        window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
+                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+
+        final FrameLayout layout = new FrameLayout(this);
+        layout.setId(LAYOUT_ID);
+        setContentView(layout);
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/IntegrationTestUtils.java b/tests/src/com/android/contacts/common/test/IntegrationTestUtils.java
new file mode 100644
index 0000000..5457128
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/IntegrationTestUtils.java
@@ -0,0 +1,196 @@
+/*
+ * 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 static android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP;
+import static android.os.PowerManager.FULL_WAKE_LOCK;
+import static android.os.PowerManager.ON_AFTER_RELEASE;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.PowerManager;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.google.common.base.Preconditions;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Some utility methods for making integration testing smoother. */
+@ThreadSafe
+public class IntegrationTestUtils {
+    private static final String TAG = "IntegrationTestUtils";
+
+    private final Instrumentation mInstrumentation;
+    private final Object mLock = new Object();
+    @GuardedBy("mLock") private PowerManager.WakeLock mWakeLock;
+
+    public IntegrationTestUtils(Instrumentation instrumentation) {
+        mInstrumentation = instrumentation;
+    }
+
+    /**
+     * Find a view by a given resource id, from the given activity, and click it, iff it is
+     * enabled according to {@link View#isEnabled()}.
+     */
+    public void clickButton(final Activity activity, final int buttonResourceId) throws Throwable {
+        runOnUiThreadAndGetTheResult(new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                View view = activity.findViewById(buttonResourceId);
+                Assert.assertNotNull(view);
+                if (view.isEnabled()) {
+                    view.performClick();
+                }
+                return null;
+            }
+        });
+    }
+
+    /** Returns the result of running {@link TextView#getText()} on the ui thread. */
+    public CharSequence getText(final TextView view) throws Throwable {
+        return runOnUiThreadAndGetTheResult(new Callable<CharSequence>() {
+            @Override
+            public CharSequence call() {
+                return view.getText();
+            }
+        });
+    }
+
+    // TODO: Move this class and the appropriate documentation into a test library, having checked
+    // first to see if exactly this code already exists or not.
+    /**
+     * Execute a callable on the ui thread, returning its result synchronously.
+     * <p>
+     * Waits for an idle sync on the main thread (see {@link Instrumentation#waitForIdle(Runnable)})
+     * before executing this callable.
+     */
+    public <T> T runOnUiThreadAndGetTheResult(Callable<T> callable) throws Throwable {
+        FutureTask<T> future = new FutureTask<T>(callable);
+        mInstrumentation.waitForIdle(future);
+        try {
+            return future.get();
+        } catch (ExecutionException e) {
+            // Unwrap the cause of the exception and re-throw it.
+            throw e.getCause();
+        }
+    }
+
+    /**
+     * Wake up the screen, useful in tests that want or need the screen to be on.
+     * <p>
+     * This is usually called from setUp() for tests that require it.  After calling this method,
+     * {@link #releaseScreenWakeLock()} must be called, this is usually done from tearDown().
+     */
+    public void acquireScreenWakeLock(Context context) {
+        synchronized (mLock) {
+            Preconditions.checkState(mWakeLock == null, "mWakeLock was already held");
+            mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE))
+                    .newWakeLock(
+                            PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE | PowerManager.FULL_WAKE_LOCK, TAG);
+            mWakeLock.acquire();
+        }
+    }
+
+    /** Release the wake lock previously acquired with {@link #acquireScreenWakeLock(Context)}. */
+    public void releaseScreenWakeLock() {
+        synchronized (mLock) {
+            // We don't use Preconditions to force you to have acquired before release.
+            // This is because we don't want unnecessary exceptions in tearDown() since they'll
+            // typically mask the actual exception that happened during the test.
+            // The other reason is that this method is most likely to be called from tearDown(),
+            // which is invoked within a finally block, so it's not infrequently the case that
+            // the setUp() method fails before getting the lock, at which point we don't want
+            // to fail in tearDown().
+            if (mWakeLock != null) {
+                mWakeLock.release();
+                mWakeLock = null;
+            }
+        }
+    }
+
+    /**
+     * Gets all {@link TextView} objects whose {@link TextView#getText()} contains the given text as
+     * a substring.
+     */
+    public List<TextView> getTextViewsWithString(final Activity activity, final String text)
+            throws Throwable {
+        return getTextViewsWithString(getRootView(activity), text);
+    }
+
+    /**
+     * Gets all {@link TextView} objects whose {@link TextView#getText()} contains the given text as
+     * a substring for the given root view.
+     */
+    public List<TextView> getTextViewsWithString(final View rootView, final String text)
+            throws Throwable {
+        return runOnUiThreadAndGetTheResult(new Callable<List<TextView>>() {
+            @Override
+            public List<TextView> call() throws Exception {
+                List<TextView> matchingViews = new ArrayList<TextView>();
+                for (TextView textView : getAllViews(TextView.class, rootView)) {
+                    if (textView.getText().toString().contains(text)) {
+                        matchingViews.add(textView);
+                    }
+                }
+                return matchingViews;
+            }
+        });
+    }
+
+    /** Find the root view for a given activity. */
+    public static View getRootView(Activity activity) {
+        return activity.findViewById(android.R.id.content).getRootView();
+    }
+
+    /**
+     * Gets a list of all views of a given type, rooted at the given parent.
+     * <p>
+     * This method will recurse down through all {@link ViewGroup} instances looking for
+     * {@link View} instances of the supplied class type. Specifically it will use the
+     * {@link Class#isAssignableFrom(Class)} method as the test for which views to add to the list,
+     * so if you provide {@code View.class} as your type, you will get every view. The parent itself
+     * will be included also, should it be of the right type.
+     * <p>
+     * This call manipulates the ui, and as such should only be called from the application's main
+     * thread.
+     */
+    private static <T extends View> List<T> getAllViews(final Class<T> clazz, final View parent) {
+        List<T> results = new ArrayList<T>();
+        if (parent.getClass().equals(clazz)) {
+            results.add(clazz.cast(parent));
+        }
+        if (parent instanceof ViewGroup) {
+            ViewGroup viewGroup = (ViewGroup) parent;
+            for (int i = 0; i < viewGroup.getChildCount(); ++i) {
+                results.addAll(getAllViews(clazz, viewGroup.getChildAt(i)));
+            }
+        }
+        return results;
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/LaunchPerformanceBase.java b/tests/src/com/android/contacts/common/test/LaunchPerformanceBase.java
new file mode 100644
index 0000000..a2ebde3
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/LaunchPerformanceBase.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.test;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Bundle;
+
+
+/**
+ * Base class for all launch performance Instrumentation classes.
+ */
+public class LaunchPerformanceBase extends Instrumentation {
+
+    public static final String LOG_TAG = "Launch Performance";
+
+    protected Bundle mResults;
+    protected Intent mIntent;
+
+    public LaunchPerformanceBase() {
+        mResults = new Bundle();
+        mIntent = new Intent(Intent.ACTION_MAIN);
+        mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        setAutomaticPerformanceSnapshots();
+    }
+
+    /**
+     * Launches intent, and waits for idle before returning.
+     *
+     * @hide
+     */
+    protected void LaunchApp() {
+        startActivitySync(mIntent);
+        waitForIdleSync();
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/ContactsMockContext.java b/tests/src/com/android/contacts/common/test/mocks/ContactsMockContext.java
new file mode 100644
index 0000000..c72fe3d
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/ContactsMockContext.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.test.mocks;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
+
+/**
+ * A mock context for contacts unit tests. Forwards everything to
+ * a supplied context, except content resolver operations, which are sent
+ * to mock content providers.
+ */
+public class ContactsMockContext extends ContextWrapper {
+    private ContactsMockPackageManager mPackageManager;
+    private MockContentResolver mContentResolver;
+    private MockContentProvider mContactsProvider;
+    private MockContentProvider mSettingsProvider;
+    private Intent mIntentForStartActivity;
+
+    public ContactsMockContext(Context base) {
+        this(base, ContactsContract.AUTHORITY);
+    }
+
+    public ContactsMockContext(Context base, String authority) {
+        super(base);
+        mPackageManager = new ContactsMockPackageManager();
+        mContentResolver = new MockContentResolver();
+        mContactsProvider = new MockContentProvider();
+        mContentResolver.addProvider(authority, mContactsProvider);
+        mSettingsProvider = new MockContentProvider();
+        mContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider);
+    }
+
+    @Override
+    public ContentResolver getContentResolver() {
+        return mContentResolver;
+    }
+
+    public MockContentProvider getContactsProvider() {
+        return mContactsProvider;
+    }
+
+    public MockContentProvider getSettingsProvider() {
+        return mSettingsProvider;
+    }
+
+    @Override
+    public PackageManager getPackageManager() {
+        return mPackageManager;
+    }
+
+    @Override
+    public Context getApplicationContext() {
+        return this;
+    }
+
+    /**
+     * Instead of actually sending Intent, this method just remembers what Intent was supplied last.
+     * You can check the content via {@link #getIntentForStartActivity()} for verification.
+     */
+    @Override
+    public void startActivity(Intent intent) {
+        mIntentForStartActivity = intent;
+    }
+
+    public Intent getIntentForStartActivity() {
+        return mIntentForStartActivity;
+    }
+
+    public void verify() {
+        mContactsProvider.verify();
+        mSettingsProvider.verify();
+    }
+
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/ContactsMockPackageManager.java b/tests/src/com/android/contacts/common/test/mocks/ContactsMockPackageManager.java
new file mode 100644
index 0000000..a1557ff
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/ContactsMockPackageManager.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.test.mocks;
+
+import android.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.test.mock.MockPackageManager;
+
+/**
+ */
+public class ContactsMockPackageManager extends MockPackageManager {
+    public ContactsMockPackageManager() {
+    }
+
+    @Override
+    public Drawable getActivityLogo(ComponentName activityName) throws NameNotFoundException {
+        return new ColorDrawable();
+    }
+
+    @Override
+    public Drawable getActivityIcon(ComponentName activityName) {
+        return new ColorDrawable();
+    }
+
+    @Override
+    public Drawable getDrawable(String packageName, int resid, ApplicationInfo appInfo) {
+        // TODO: make programmable
+        return new ColorDrawable();
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java
new file mode 100644
index 0000000..b46c49d
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.test.mocks;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.BaseAccountType;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A mock {@link AccountTypeManager} class.
+ */
+public class MockAccountTypeManager extends AccountTypeManager {
+
+    public AccountType[] mTypes;
+    public AccountWithDataSet[] mAccounts;
+
+    public MockAccountTypeManager(AccountType[] types, AccountWithDataSet[] accounts) {
+        this.mTypes = types;
+        this.mAccounts = accounts;
+    }
+
+    @Override
+    public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
+        // Add fallback accountType to mimic the behavior of AccountTypeManagerImpl
+        AccountType mFallbackAccountType = new BaseAccountType() {
+            @Override
+            public boolean areContactsWritable() {
+                return false;
+            }
+        };
+        mFallbackAccountType.accountType = "fallback";
+        for (AccountType type : mTypes) {
+            if (Objects.equal(accountTypeWithDataSet.accountType, type.accountType)
+                    && Objects.equal(accountTypeWithDataSet.dataSet, type.dataSet)) {
+                return type;
+            }
+        }
+        return mFallbackAccountType;
+    }
+
+    @Override
+    public List<AccountWithDataSet> getAccounts(boolean writableOnly) {
+        return Arrays.asList(mAccounts);
+    }
+
+    @Override
+    public void sortAccounts(AccountWithDataSet account) {}
+
+    @Override
+    public List<AccountWithDataSet> getGroupWritableAccounts() {
+        return Arrays.asList(mAccounts);
+    }
+
+    @Override
+    public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
+        return Maps.newHashMap(); // Always returns empty
+    }
+
+    @Override
+    public List<AccountType> getAccountTypes(boolean writableOnly) {
+        final List<AccountType> ret = Lists.newArrayList();
+        synchronized (this) {
+            for (AccountType type : mTypes) {
+                if (!writableOnly || type.areContactsWritable()) {
+                    ret.add(type);
+                }
+            }
+        }
+        return ret;
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/MockContactPhotoManager.java b/tests/src/com/android/contacts/common/test/mocks/MockContactPhotoManager.java
new file mode 100644
index 0000000..db8f06f
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/MockContactPhotoManager.java
@@ -0,0 +1,72 @@
+/*
+ * 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.mocks;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.contacts.common.ContactPhotoManager;
+
+/**
+ * A photo preloader that always uses the "no contact" picture and never executes any real
+ * db queries
+ */
+public class MockContactPhotoManager extends ContactPhotoManager {
+    @Override
+    public void loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular,
+            DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider) {
+        defaultProvider.applyDefaultImage(view, -1, darkTheme, null);
+    }
+
+    @Override
+    public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
+            boolean isCircular, DefaultImageRequest defaultImageRequest,
+            DefaultImageProvider defaultProvider) {
+        defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, null);
+    }
+
+    @Override
+    public void removePhoto(ImageView view) {
+        view.setImageDrawable(null);
+    }
+
+    @Override
+    public void cancelPendingRequests(View fragmentRootView) {
+    }
+
+    @Override
+    public void pause() {
+    }
+
+    @Override
+    public void resume() {
+    }
+
+    @Override
+    public void refreshCache() {
+    }
+
+    @Override
+    public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
+    }
+
+    @Override
+    public void preloadPhotosInBackground() {
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/MockContentProvider.java b/tests/src/com/android/contacts/common/test/mocks/MockContentProvider.java
new file mode 100644
index 0000000..335e8d2
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/MockContentProvider.java
@@ -0,0 +1,659 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.test.mocks;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A programmable mock content provider.
+ */
+public class MockContentProvider extends android.test.mock.MockContentProvider {
+    private static final String TAG = "MockContentProvider";
+
+    public static class Query {
+
+        private final Uri mUri;
+        private String[] mProjection;
+        private String[] mDefaultProjection;
+        private String mSelection;
+        private String[] mSelectionArgs;
+        private String mSortOrder;
+        private List<Object> mRows = new ArrayList<>();
+        private boolean mAnyProjection;
+        private boolean mAnySelection;
+        private boolean mAnySortOrder;
+        private boolean mAnyNumberOfTimes;
+
+        private boolean mExecuted;
+
+        public Query(Uri uri) {
+            mUri = uri;
+        }
+
+        @Override
+        public String toString() {
+            return queryToString(mUri, mProjection, mSelection, mSelectionArgs, mSortOrder);
+        }
+
+        public Query withProjection(String... projection) {
+            mProjection = projection;
+            return this;
+        }
+
+        public Query withDefaultProjection(String... projection) {
+            mDefaultProjection = projection;
+            return this;
+        }
+
+        public Query withAnyProjection() {
+            mAnyProjection = true;
+            return this;
+        }
+
+        public Query withSelection(String selection, String... selectionArgs) {
+            mSelection = selection;
+            mSelectionArgs = selectionArgs;
+            return this;
+        }
+
+        public Query withAnySelection() {
+            mAnySelection = true;
+            return this;
+        }
+
+        public Query withSortOrder(String sortOrder) {
+            mSortOrder = sortOrder;
+            return this;
+        }
+
+        public Query withAnySortOrder() {
+            mAnySortOrder = true;
+            return this;
+        }
+
+        public Query returnRow(ContentValues values) {
+            mRows.add(values);
+            return this;
+        }
+
+        public Query returnRow(Object... row) {
+            mRows.add(row);
+            return this;
+        }
+
+        public Query returnEmptyCursor() {
+            mRows.clear();
+            return this;
+        }
+
+        public Query anyNumberOfTimes() {
+            mAnyNumberOfTimes = true;
+            return this;
+        }
+
+        public boolean equals(Uri uri, String[] projection, String selection,
+                String[] selectionArgs, String sortOrder) {
+            if (!uri.equals(mUri)) {
+                return false;
+            }
+
+            if (!mAnyProjection && !Arrays.equals(projection, mProjection)) {
+                return false;
+            }
+
+            if (!mAnySelection && !Objects.equals(selection, mSelection)) {
+                return false;
+            }
+
+            if (!mAnySelection && !Arrays.equals(selectionArgs, mSelectionArgs)) {
+                return false;
+            }
+
+            if (!mAnySortOrder && !Objects.equals(sortOrder, mSortOrder)) {
+                return false;
+            }
+
+            return true;
+        }
+
+        public Cursor getResult(String[] projection) {
+            String[] columnNames;
+            if (mAnyProjection) {
+                columnNames = projection;
+            } else {
+                columnNames = mProjection != null ? mProjection : mDefaultProjection;
+            }
+
+            MatrixCursor cursor = new MatrixCursor(columnNames);
+            for (Object row : mRows) {
+                if (row instanceof Object[]) {
+                    cursor.addRow((Object[]) row);
+                } else {
+                    ContentValues values = (ContentValues) row;
+                    Object[] columns = new Object[projection.length];
+                    for (int i = 0; i < projection.length; i++) {
+                        columns[i] = values.get(projection[i]);
+                    }
+                    cursor.addRow(columns);
+                }
+            }
+            return cursor;
+        }
+    }
+
+    public static class TypeQuery {
+        private final Uri mUri;
+        private final String mType;
+
+        public TypeQuery(Uri uri, String type) {
+            mUri = uri;
+            mType = type;
+        }
+
+        public Uri getUri() {
+            return mUri;
+        }
+
+        public String getType() {
+            return mType;
+        }
+
+        @Override
+        public String toString() {
+            return mUri + " --> " + mType;
+        }
+
+        public boolean equals(Uri uri) {
+            return getUri().equals(uri);
+        }
+    }
+
+    public static class Insert {
+        private final Uri mUri;
+        private final ContentValues mContentValues;
+        private final Uri mResultUri;
+        private boolean mAnyNumberOfTimes;
+        private boolean mIsExecuted;
+
+        /**
+         * Creates a new Insert to expect.
+         *
+         * @param uri the uri of the insertion request.
+         * @param contentValues the ContentValues to insert.
+         * @param resultUri the {@link Uri} for the newly inserted item.
+         * @throws NullPointerException if any parameter is {@code null}.
+         */
+        public Insert(Uri uri, ContentValues contentValues, Uri resultUri) {
+            mUri = Preconditions.checkNotNull(uri);
+            mContentValues = Preconditions.checkNotNull(contentValues);
+            mResultUri = Preconditions.checkNotNull(resultUri);
+        }
+
+        /**
+         * Causes this insert expectation to be useable for mutliple calls to insert, rather than
+         * just one.
+         *
+         * @return this
+         */
+        public Insert anyNumberOfTimes() {
+            mAnyNumberOfTimes = true;
+            return this;
+        }
+
+        private boolean equals(Uri uri, ContentValues contentValues) {
+            return mUri.equals(uri) && mContentValues.equals(contentValues);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            Insert insert = (Insert) o;
+            return mAnyNumberOfTimes == insert.mAnyNumberOfTimes &&
+                    mIsExecuted == insert.mIsExecuted &&
+                    Objects.equals(mUri, insert.mUri) &&
+                    Objects.equals(mContentValues, insert.mContentValues) &&
+                    Objects.equals(mResultUri, insert.mResultUri);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mUri, mContentValues, mResultUri, mAnyNumberOfTimes, mIsExecuted);
+        }
+
+        @Override
+        public String toString() {
+            return "Insert{" +
+                    "mUri=" + mUri +
+                    ", mContentValues=" + mContentValues +
+                    ", mResultUri=" + mResultUri +
+                    ", mAnyNumberOfTimes=" + mAnyNumberOfTimes +
+                    ", mIsExecuted=" + mIsExecuted +
+                    '}';
+        }
+    }
+
+    public static class Delete {
+        private final Uri mUri;
+
+        private boolean mAnyNumberOfTimes;
+        private boolean mAnySelection;
+        @Nullable private String mSelection;
+        @Nullable private String[] mSelectionArgs;
+        private boolean mIsExecuted;
+        private int mRowsAffected;
+
+        /**
+         * Creates a new Delete to expect.
+         * @param uri the uri of the delete request.
+         * @throws NullPointerException if uri is {@code null}.
+         */
+        public Delete(Uri uri) {
+            mUri = Preconditions.checkNotNull(uri);
+        }
+
+        /**
+         * Sets the given information as expected selection arguments.
+         *
+         * @param selection The selection to expect.
+         * @param selectionArgs The selection args to expect.
+         * @return this.
+         */
+        public Delete withSelection(String selection, @Nullable String[] selectionArgs) {
+            mSelection = Preconditions.checkNotNull(selection);
+            mSelectionArgs = selectionArgs;
+            mAnySelection = false;
+            return this;
+        }
+
+        /**
+         * Sets this delete to expect any selection arguments.
+         *
+         * @return this.
+         */
+        public Delete withAnySelection() {
+            mAnySelection = true;
+            return this;
+        }
+
+        /**
+         * Sets this delete to return the given number of rows affected.
+         *
+         * @param rowsAffected The value to return when this expected delete is executed.
+         * @return this.
+         */
+        public Delete returnRowsAffected(int rowsAffected) {
+            mRowsAffected = rowsAffected;
+            return this;
+        }
+
+        /**
+         * Causes this delete expectation to be useable for multiple calls to delete, rather than
+         * just one.
+         *
+         * @return this.
+         */
+        public Delete anyNumberOfTimes() {
+            mAnyNumberOfTimes = true;
+            return this;
+        }
+
+        private boolean equals(Uri uri, String selection, String[] selectionArgs) {
+            return mUri.equals(uri) && Objects.equals(mSelection, selection)
+                    && Arrays.equals(mSelectionArgs, selectionArgs);
+        }
+    }
+
+    public static class Update {
+        private final Uri mUri;
+        private final ContentValues mContentValues;
+        @Nullable private String mSelection;
+        @Nullable private String[] mSelectionArgs;
+        private boolean mAnyNumberOfTimes;
+        private boolean mIsExecuted;
+        private int mRowsAffected;
+
+        /**
+         * Creates a new Update to expect.
+         *
+         * @param uri the uri of the update request.
+         * @param contentValues the ContentValues to update.
+         *
+         * @throws NullPointerException if any parameter is {@code null}.
+         */
+        public Update(Uri uri,
+                      ContentValues contentValues,
+                      @Nullable String selection,
+                      @Nullable String[] selectionArgs) {
+            mUri = Preconditions.checkNotNull(uri);
+            mContentValues = Preconditions.checkNotNull(contentValues);
+            mSelection = selection;
+            mSelectionArgs = selectionArgs;
+        }
+
+        /**
+         * Causes this update expectation to be useable for mutliple calls to update, rather than
+         * just one.
+         *
+         * @return this
+         */
+        public Update anyNumberOfTimes() {
+            mAnyNumberOfTimes = true;
+            return this;
+        }
+
+        /**
+         * Sets this update to return the given number of rows affected.
+         *
+         * @param rowsAffected The value to return when this expected update is executed.
+         * @return this.
+         */
+        public Update returnRowsAffected(int rowsAffected) {
+            mRowsAffected = rowsAffected;
+            return this;
+        }
+
+        private boolean equals(Uri uri,
+                               ContentValues contentValues,
+                               @Nullable String selection,
+                               @Nullable String[] selectionArgs) {
+            return mUri.equals(uri) && mContentValues.equals(contentValues) &&
+                    Objects.equals(mSelection, selection) &&
+                    Objects.equals(mSelectionArgs, selectionArgs);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            Update update = (Update) o;
+            return mAnyNumberOfTimes == update.mAnyNumberOfTimes &&
+                    mIsExecuted == update.mIsExecuted &&
+                    Objects.equals(mUri, update.mUri) &&
+                    Objects.equals(mContentValues, update.mContentValues) &&
+                    Objects.equals(mSelection, update.mSelection) &&
+                    Objects.equals(mSelectionArgs, update.mSelectionArgs);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mUri, mContentValues, mAnyNumberOfTimes, mIsExecuted, mSelection,
+                    mSelectionArgs);
+        }
+
+        @Override
+        public String toString() {
+            return "Update{" +
+                    "mUri=" + mUri +
+                    ", mContentValues=" + mContentValues +
+                    ", mAnyNumberOfTimes=" + mAnyNumberOfTimes +
+                    ", mIsExecuted=" + mIsExecuted +
+                    ", mSelection=" + mSelection +
+                    ", mSelectionArgs=" + mSelectionArgs +
+                    '}';
+        }
+    }
+
+    private List<Query> mExpectedQueries = new ArrayList<>();
+    private Map<Uri, String> mExpectedTypeQueries = Maps.newHashMap();
+    private List<Insert> mExpectedInserts = new ArrayList<>();
+    private List<Delete> mExpectedDeletes = new ArrayList<>();
+    private List<Update> mExpectedUpdates = new ArrayList<>();
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    public Query expectQuery(Uri contentUri) {
+        Query query = new Query(contentUri);
+        mExpectedQueries.add(query);
+        return query;
+    }
+
+    public void expectTypeQuery(Uri uri, String type) {
+        mExpectedTypeQueries.put(uri, type);
+    }
+
+    public void expectInsert(Uri contentUri, ContentValues contentValues, Uri resultUri) {
+        mExpectedInserts.add(new Insert(contentUri, contentValues, resultUri));
+    }
+
+    public Update expectUpdate(Uri contentUri,
+                               ContentValues contentValues,
+                               @Nullable String selection,
+                               @Nullable String[] selectionArgs) {
+        Update update = new Update(contentUri, contentValues, selection, selectionArgs);
+        mExpectedUpdates.add(update);
+        return update;
+    }
+
+    public Delete expectDelete(Uri contentUri) {
+        Delete delete = new Delete(contentUri);
+        mExpectedDeletes.add(delete);
+        return delete;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        if (mExpectedQueries.isEmpty()) {
+            Assert.fail("Unexpected query: Actual:"
+                    + queryToString(uri, projection, selection, selectionArgs, sortOrder));
+        }
+
+        for (Iterator<Query> iterator = mExpectedQueries.iterator(); iterator.hasNext();) {
+            Query query = iterator.next();
+            if (query.equals(uri, projection, selection, selectionArgs, sortOrder)) {
+                query.mExecuted = true;
+                if (!query.mAnyNumberOfTimes) {
+                    iterator.remove();
+                }
+                return query.getResult(projection);
+            }
+        }
+
+        Assert.fail("Incorrect query. Expected one of: " + mExpectedQueries + ". Actual: " +
+                queryToString(uri, projection, selection, selectionArgs, sortOrder));
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        if (mExpectedTypeQueries.isEmpty()) {
+            Assert.fail("Unexpected getType query: " + uri);
+        }
+
+        String mimeType = mExpectedTypeQueries.get(uri);
+        if (mimeType != null) {
+            return mimeType;
+        }
+
+        Assert.fail("Unknown mime type for: " + uri);
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        if (mExpectedInserts.isEmpty()) {
+            Assert.fail("Unexpected insert. Actual: " + insertToString(uri, values));
+        }
+        for (Iterator<Insert> iterator = mExpectedInserts.iterator(); iterator.hasNext(); ) {
+            Insert insert = iterator.next();
+            if (insert.equals(uri, values)) {
+                insert.mIsExecuted = true;
+                if (!insert.mAnyNumberOfTimes) {
+                    iterator.remove();
+                }
+                return insert.mResultUri;
+            }
+        }
+
+        Assert.fail("Incorrect insert. Expected one of: " + mExpectedInserts + ". Actual: "
+                + insertToString(uri, values));
+        return null;
+    }
+
+    private String insertToString(Uri uri, ContentValues contentValues) {
+        return "Insert { uri=" + uri + ", contentValues=" + contentValues + '}';
+    }
+
+    @Override
+    public int update(Uri uri,
+                      ContentValues values,
+                      @Nullable String selection,
+                      @Nullable String[] selectionArgs) {
+        if (mExpectedUpdates.isEmpty()) {
+            Assert.fail("Unexpected update. Actual: "
+                    + updateToString(uri, values, selection, selectionArgs));
+        }
+        for (Iterator<Update> iterator = mExpectedUpdates.iterator(); iterator.hasNext(); ) {
+            Update update = iterator.next();
+            if (update.equals(uri, values, selection, selectionArgs)) {
+                update.mIsExecuted = true;
+                if (!update.mAnyNumberOfTimes) {
+                    iterator.remove();
+                }
+                return update.mRowsAffected;
+            }
+        }
+
+        Assert.fail("Incorrect update. Expected one of: " + mExpectedUpdates + ". Actual: "
+                + updateToString(uri, values, selection, selectionArgs));
+        return - 1;
+    }
+
+    private String updateToString(Uri uri,
+                                  ContentValues contentValues,
+                                  @Nullable String selection,
+                                  @Nullable String[] selectionArgs) {
+        return "Update { uri=" + uri + ", contentValues=" + contentValues + ", selection=" +
+                selection + ", selectionArgs" + Arrays.toString(selectionArgs) + '}';
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        if (mExpectedDeletes.isEmpty()) {
+            Assert.fail("Unexpected delete. Actual: " + deleteToString(uri, selection,
+                    selectionArgs));
+        }
+        for (Iterator<Delete> iterator = mExpectedDeletes.iterator(); iterator.hasNext(); ) {
+            Delete delete = iterator.next();
+            if (delete.equals(uri, selection, selectionArgs)) {
+                delete.mIsExecuted = true;
+                if (!delete.mAnyNumberOfTimes) {
+                    iterator.remove();
+                }
+                return delete.mRowsAffected;
+            }
+        }
+        Assert.fail("Incorrect delete. Expected one of: " + mExpectedDeletes + ". Actual: "
+                + deleteToString(uri, selection, selectionArgs));
+        return -1;
+    }
+
+    private String deleteToString(Uri uri, String selection, String[] selectionArgs) {
+        return "Delete { uri=" + uri + ", selection=" + selection + ", selectionArgs"
+                + Arrays.toString(selectionArgs) + '}';
+    }
+
+    private static String queryToString(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(uri).append(" ");
+        if (projection != null) {
+            sb.append(Arrays.toString(projection));
+        } else {
+            sb.append("[]");
+        }
+        if (selection != null) {
+            sb.append(" selection: '").append(selection).append("'");
+            if (selectionArgs != null) {
+                sb.append(Arrays.toString(selectionArgs));
+            } else {
+                sb.append("[]");
+            }
+        }
+        if (sortOrder != null) {
+            sb.append(" sort: '").append(sortOrder).append("'");
+        }
+        return sb.toString();
+    }
+
+    public void verify() {
+        verifyQueries();
+        verifyInserts();
+        verifyDeletes();
+    }
+
+    private void verifyQueries() {
+        List<Query> missedQueries = new ArrayList<>();
+        for (Query query : mExpectedQueries) {
+            if (!query.mExecuted) {
+                missedQueries.add(query);
+            }
+        }
+        Assert.assertTrue("Not all expected queries have been called: " + missedQueries,
+                missedQueries.isEmpty());
+    }
+
+    private void verifyInserts() {
+        List<Insert> missedInserts = new ArrayList<>();
+        for (Insert insert : mExpectedInserts) {
+            if (!insert.mIsExecuted) {
+                missedInserts.add(insert);
+            }
+        }
+        Assert.assertTrue("Not all expected inserts have been called: " + missedInserts,
+                missedInserts.isEmpty());
+    }
+
+    private void verifyDeletes() {
+        List<Delete> missedDeletes = new ArrayList<>();
+        for (Delete delete : mExpectedDeletes) {
+            if (!delete.mIsExecuted) {
+                missedDeletes.add(delete);
+            }
+        }
+        Assert.assertTrue("Not all expected deletes have been called: " + missedDeletes,
+                missedDeletes.isEmpty());
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/MockSharedPreferences.java b/tests/src/com/android/contacts/common/test/mocks/MockSharedPreferences.java
new file mode 100644
index 0000000..13d035e
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/MockSharedPreferences.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.test.mocks;
+
+import android.content.SharedPreferences;
+
+import com.google.common.collect.Maps;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * A programmable mock content provider.
+ */
+public class MockSharedPreferences implements SharedPreferences, SharedPreferences.Editor {
+
+    private HashMap<String, Object> mValues = Maps.newHashMap();
+    private HashMap<String, Object> mTempValues = Maps.newHashMap();
+
+    public Editor edit() {
+        return this;
+    }
+
+    public boolean contains(String key) {
+        return mValues.containsKey(key);
+    }
+
+    public Map<String, ?> getAll() {
+        return new HashMap<String, Object>(mValues);
+    }
+
+    public boolean getBoolean(String key, boolean defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Boolean)mValues.get(key)).booleanValue();
+        }
+        return defValue;
+    }
+
+    public float getFloat(String key, float defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Float)mValues.get(key)).floatValue();
+        }
+        return defValue;
+    }
+
+    public int getInt(String key, int defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Integer)mValues.get(key)).intValue();
+        }
+        return defValue;
+    }
+
+    public long getLong(String key, long defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Long)mValues.get(key)).longValue();
+        }
+        return defValue;
+    }
+
+    public String getString(String key, String defValue) {
+        if (mValues.containsKey(key))
+            return (String)mValues.get(key);
+        return defValue;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Set<String> getStringSet(String key, Set<String> defValues) {
+        if (mValues.containsKey(key)) {
+            return (Set<String>) mValues.get(key);
+        }
+        return defValues;
+    }
+
+    public void registerOnSharedPreferenceChangeListener(
+            OnSharedPreferenceChangeListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void unregisterOnSharedPreferenceChangeListener(
+            OnSharedPreferenceChangeListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Editor putBoolean(String key, boolean value) {
+        mTempValues.put(key, Boolean.valueOf(value));
+        return this;
+    }
+
+    public Editor putFloat(String key, float value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    public Editor putInt(String key, int value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    public Editor putLong(String key, long value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    public Editor putString(String key, String value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    public Editor putStringSet(String key, Set<String> values) {
+        mTempValues.put(key, values);
+        return this;
+    }
+
+    public Editor remove(String key) {
+        mTempValues.remove(key);
+        return this;
+    }
+
+    public Editor clear() {
+        mTempValues.clear();
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public boolean commit() {
+        mValues = (HashMap<String, Object>)mTempValues.clone();
+        return true;
+    }
+
+    public void apply() {
+        commit();
+    }
+}
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";
+}
diff --git a/tests/src/com/android/contacts/common/util/BitmapUtilTests.java b/tests/src/com/android/contacts/common/util/BitmapUtilTests.java
new file mode 100644
index 0000000..94394b1
--- /dev/null
+++ b/tests/src/com/android/contacts/common/util/BitmapUtilTests.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.graphics.Bitmap;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.contacts.common.util.BitmapUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Tests for {@link com.android.contacts.common.util.BitmapUtil}.
+ */
+@SmallTest
+public class BitmapUtilTests extends AndroidTestCase {
+    public void testGetSmallerExtentFromBytes1() throws Exception {
+        assertEquals(100, BitmapUtil.getSmallerExtentFromBytes(createJpegRawData(100, 100)));
+        assertEquals(100, BitmapUtil.getSmallerExtentFromBytes(createPngRawData(100, 100)));
+    }
+
+    public void testGetSmallerExtentFromBytes2() throws Exception {
+        assertEquals(50, BitmapUtil.getSmallerExtentFromBytes(createJpegRawData(200, 50)));
+        assertEquals(50, BitmapUtil.getSmallerExtentFromBytes(createPngRawData(200, 50)));
+    }
+
+    public void testGetSmallerExtentFromBytes3() throws Exception {
+        assertEquals(40, BitmapUtil.getSmallerExtentFromBytes(createJpegRawData(40, 150)));
+        assertEquals(40, BitmapUtil.getSmallerExtentFromBytes(createPngRawData(40, 150)));
+    }
+
+    public void testFindOptimalSampleSizeExact() throws Exception {
+        assertEquals(1, BitmapUtil.findOptimalSampleSize(512, 512));
+    }
+
+    public void testFindOptimalSampleSizeBigger() throws Exception {
+        assertEquals(1, BitmapUtil.findOptimalSampleSize(512, 1024));
+    }
+
+    public void testFindOptimalSampleSizeSmaller1() throws Exception {
+        assertEquals(2, BitmapUtil.findOptimalSampleSize(512, 256));
+    }
+
+    public void testFindOptimalSampleSizeSmaller2() throws Exception {
+        assertEquals(2, BitmapUtil.findOptimalSampleSize(512, 230));
+    }
+
+    public void testFindOptimalSampleSizeSmaller3() throws Exception {
+        assertEquals(4, BitmapUtil.findOptimalSampleSize(512, 129));
+    }
+
+    public void testFindOptimalSampleSizeSmaller4() throws Exception {
+        assertEquals(4, BitmapUtil.findOptimalSampleSize(512, 128));
+    }
+
+    public void testFindOptimalSampleSizeUnknownOriginal() throws Exception {
+        assertEquals(1, BitmapUtil.findOptimalSampleSize(-1, 128));
+    }
+
+    public void testFindOptimalSampleSizeUnknownTarget() throws Exception {
+        assertEquals(1, BitmapUtil.findOptimalSampleSize(128, -1));
+    }
+
+    public void testDecodeWithSampleSize1() throws IOException {
+        assertBitmapSize(128, 64, BitmapUtil.decodeBitmapFromBytes(createJpegRawData(128, 64), 1));
+        assertBitmapSize(128, 64, BitmapUtil.decodeBitmapFromBytes(createPngRawData(128, 64), 1));
+    }
+
+    public void testDecodeWithSampleSize2() throws IOException {
+        assertBitmapSize(64, 32, BitmapUtil.decodeBitmapFromBytes(createJpegRawData(128, 64), 2));
+        assertBitmapSize(64, 32, BitmapUtil.decodeBitmapFromBytes(createPngRawData(128, 64), 2));
+    }
+
+    public void testDecodeWithSampleSize2a() throws IOException {
+        assertBitmapSize(25, 20, BitmapUtil.decodeBitmapFromBytes(createJpegRawData(50, 40), 2));
+        assertBitmapSize(25, 20, BitmapUtil.decodeBitmapFromBytes(createPngRawData(50, 40), 2));
+    }
+
+    public void testDecodeWithSampleSize4() throws IOException {
+        assertBitmapSize(32, 16, BitmapUtil.decodeBitmapFromBytes(createJpegRawData(128, 64), 4));
+        assertBitmapSize(32, 16, BitmapUtil.decodeBitmapFromBytes(createPngRawData(128, 64), 4));
+    }
+
+    private void assertBitmapSize(int expectedWidth, int expectedHeight, Bitmap bitmap) {
+        assertEquals(expectedWidth, bitmap.getWidth());
+        assertEquals(expectedHeight, bitmap.getHeight());
+    }
+
+    private byte[] createJpegRawData(int sourceWidth, int sourceHeight) throws IOException {
+        return createRawData(Bitmap.CompressFormat.JPEG, sourceWidth, sourceHeight);
+    }
+
+    private byte[] createPngRawData(int sourceWidth, int sourceHeight) throws IOException {
+        return createRawData(Bitmap.CompressFormat.PNG, sourceWidth, sourceHeight);
+    }
+
+    private byte[] createRawData(Bitmap.CompressFormat format, int sourceWidth,
+            int sourceHeight) throws IOException {
+        // Create a temp bitmap as our source
+        Bitmap b = Bitmap.createBitmap(sourceWidth, sourceHeight, Bitmap.Config.ARGB_8888);
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        b.compress(format, 50, outputStream);
+        final byte[] data = outputStream.toByteArray();
+        outputStream.close();
+        return data;
+    }
+}
diff --git a/tests/src/com/android/contacts/common/util/ContactDisplayUtilTests.java b/tests/src/com/android/contacts/common/util/ContactDisplayUtilTests.java
new file mode 100644
index 0000000..b4cd1ca
--- /dev/null
+++ b/tests/src/com/android/contacts/common/util/ContactDisplayUtilTests.java
@@ -0,0 +1,202 @@
+/*
+ * 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 static android.provider.ContactsContract.CommonDataKinds.Phone;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for (@link ContactDisplayUtils}
+ */
+@SmallTest
+public class ContactDisplayUtilTests extends AndroidTestCase {
+
+    private static final String NAME_PRIMARY = "Name Primary";
+    private static final String NAME_ALTERNATIVE = "Name Alternative";
+
+    @Mock private ContactsPreferences mContactsPreferences;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+    }
+
+    public void testIsCustomPhoneTypeReturnsTrue() {
+        assertTrue(ContactDisplayUtils.isCustomPhoneType(Phone.TYPE_CUSTOM));
+        assertTrue(ContactDisplayUtils.isCustomPhoneType(Phone.TYPE_ASSISTANT));
+    }
+
+    public void testIsCustomPhoneTypeReturnsFalse() {
+        assertFalse(ContactDisplayUtils.isCustomPhoneType(Phone.TYPE_HOME));
+        assertFalse(ContactDisplayUtils.isCustomPhoneType(Phone.TYPE_FAX_WORK));
+        assertFalse(ContactDisplayUtils.isCustomPhoneType(Phone.TYPE_MOBILE));
+        assertFalse(ContactDisplayUtils.isCustomPhoneType(Phone.TYPE_OTHER));
+    }
+
+    public void testGetLabelForCallOrSmsReturnsCustomLabel() {
+        final CharSequence smsResult = ContactDisplayUtils.getLabelForCallOrSms(Phone.TYPE_CUSTOM,
+                "expected sms label", ContactDisplayUtils.INTERACTION_SMS, getContext());
+        assertEquals("expected sms label", smsResult);
+
+        final CharSequence callResult = ContactDisplayUtils.getLabelForCallOrSms(Phone.TYPE_CUSTOM,
+                "expected call label", ContactDisplayUtils.INTERACTION_CALL, getContext());
+        assertEquals("expected call label", callResult);
+    }
+
+    public void testGetLabelForCallOrSmsReturnsCallLabels() {
+        CharSequence result = ContactDisplayUtils.getLabelForCallOrSms(Phone.TYPE_HOME, "",
+                ContactDisplayUtils.INTERACTION_CALL, getContext());
+        CharSequence expected = getContext().getResources().getText(R.string.call_home);
+        assertEquals(expected, result);
+
+        result = ContactDisplayUtils.getLabelForCallOrSms(Phone.TYPE_MOBILE, "",
+                ContactDisplayUtils.INTERACTION_CALL, getContext());
+        expected = getContext().getResources().getText(R.string.call_mobile);
+        assertEquals(expected, result);
+    }
+
+    public void testGetLabelForCallOrSmsReturnsSmsLabels() {
+        CharSequence result = ContactDisplayUtils.getLabelForCallOrSms(Phone.TYPE_HOME, "",
+                ContactDisplayUtils.INTERACTION_SMS, getContext());
+        CharSequence expected = getContext().getResources().getText(R.string.sms_home);
+        assertEquals(expected, result);
+
+        result = ContactDisplayUtils.getLabelForCallOrSms(Phone.TYPE_MOBILE, "",
+                ContactDisplayUtils.INTERACTION_SMS, getContext());
+        expected = getContext().getResources().getText(R.string.sms_mobile);
+        assertEquals(expected, result);
+    }
+
+    public void testGetPhoneLabelResourceIdReturnsOther() {
+        assertEquals(R.string.call_other, ContactDisplayUtils.getPhoneLabelResourceId(null));
+    }
+
+    public void testGetPhoneLabelResourceIdReturnsMatchHome() {
+        assertEquals(R.string.call_home, ContactDisplayUtils.getPhoneLabelResourceId(
+                Phone.TYPE_HOME));
+    }
+
+    public void testGetSmsLabelResourceIdReturnsOther() {
+        assertEquals(R.string.sms_other, ContactDisplayUtils.getSmsLabelResourceId(null));
+    }
+
+    public void testGetSmsLabelResourceIdReturnsMatchHome() {
+        assertEquals(R.string.sms_home, ContactDisplayUtils.getSmsLabelResourceId(Phone.TYPE_HOME));
+    }
+
+    public void testGetPreferredDisplayName_NullContactsPreferences() {
+        assertEquals(NAME_PRIMARY, ContactDisplayUtils.getPreferredDisplayName(NAME_PRIMARY,
+                NAME_ALTERNATIVE, null));
+    }
+
+    public void testGetPreferredDisplayName_NullContactsPreferences_NullAlternative() {
+        assertEquals(NAME_PRIMARY, ContactDisplayUtils.getPreferredDisplayName(NAME_PRIMARY, null,
+                null));
+    }
+
+    public void testGetPreferredDisplayName_NullContactsPreferences_NullPrimary() {
+        assertEquals(NAME_ALTERNATIVE, ContactDisplayUtils.getPreferredDisplayName(null,
+                NAME_ALTERNATIVE, null));
+    }
+
+    public void testGetPreferredDisplayName_NullContactsPreferences_BothNull() {
+        assertNull(ContactDisplayUtils.getPreferredDisplayName(null, null, null));
+    }
+
+    public void testGetPreferredDisplayName_EmptyAlternative() {
+        Mockito.when(mContactsPreferences.getDisplayOrder())
+                .thenReturn(ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE);
+        assertEquals(NAME_PRIMARY, ContactDisplayUtils.getPreferredDisplayName(NAME_PRIMARY, "",
+                mContactsPreferences));
+    }
+
+    public void testGetPreferredDisplayName_InvalidPreference() {
+        Mockito.when(mContactsPreferences.getDisplayOrder()).thenReturn(-1);
+        assertEquals(NAME_PRIMARY, ContactDisplayUtils.getPreferredDisplayName(NAME_PRIMARY,
+                NAME_ALTERNATIVE, mContactsPreferences));
+    }
+
+    public void testGetPreferredDisplayName_Primary() {
+        Mockito.when(mContactsPreferences.getDisplayOrder())
+                .thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY);
+        assertEquals(NAME_PRIMARY, ContactDisplayUtils.getPreferredDisplayName(NAME_PRIMARY,
+                NAME_ALTERNATIVE, mContactsPreferences));
+    }
+
+    public void testGetPreferredDisplayName_Alternative() {
+        Mockito.when(mContactsPreferences.getDisplayOrder())
+                .thenReturn(ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE);
+        assertEquals(NAME_ALTERNATIVE, ContactDisplayUtils.getPreferredDisplayName(NAME_PRIMARY,
+                NAME_ALTERNATIVE, mContactsPreferences));
+    }
+
+    public void testGetPreferredSortName_NullContactsPreferences() {
+        assertEquals(NAME_PRIMARY, ContactDisplayUtils.getPreferredSortName(NAME_PRIMARY,
+                NAME_ALTERNATIVE, null));
+    }
+
+    public void testGetPreferredSortName_NullContactsPreferences_NullAlternative() {
+        assertEquals(NAME_PRIMARY, ContactDisplayUtils.getPreferredSortName(NAME_PRIMARY, null,
+                null));
+    }
+
+    public void testGetPreferredSortName_NullContactsPreferences_NullPrimary() {
+        assertEquals(NAME_ALTERNATIVE, ContactDisplayUtils.getPreferredSortName(null,
+                NAME_ALTERNATIVE, null));
+    }
+
+    public void testGetPreferredSortName_NullContactsPreferences_BothNull() {
+        assertNull(ContactDisplayUtils.getPreferredSortName(null, null, null));
+    }
+
+    public void testGetPreferredSortName_EmptyAlternative() {
+        Mockito.when(mContactsPreferences.getSortOrder())
+                .thenReturn(ContactsPreferences.SORT_ORDER_ALTERNATIVE);
+        assertEquals(NAME_PRIMARY, ContactDisplayUtils.getPreferredSortName(NAME_PRIMARY, "",
+                mContactsPreferences));
+    }
+
+    public void testGetPreferredSortName_InvalidPreference() {
+        Mockito.when(mContactsPreferences.getSortOrder()).thenReturn(-1);
+        assertEquals(NAME_PRIMARY, ContactDisplayUtils.getPreferredSortName(NAME_PRIMARY,
+                NAME_ALTERNATIVE, mContactsPreferences));
+    }
+
+    public void testGetPreferredSortName_Primary() {
+        Mockito.when(mContactsPreferences.getSortOrder())
+                .thenReturn(ContactsPreferences.SORT_ORDER_PRIMARY);
+        assertEquals(NAME_PRIMARY, ContactDisplayUtils.getPreferredSortName(NAME_PRIMARY,
+                NAME_ALTERNATIVE, mContactsPreferences));
+    }
+
+    public void testGetPreferredSortName_Alternative() {
+        Mockito.when(mContactsPreferences.getSortOrder())
+                .thenReturn(ContactsPreferences.SORT_ORDER_ALTERNATIVE);
+        assertEquals(NAME_ALTERNATIVE, ContactDisplayUtils.getPreferredSortName(NAME_PRIMARY,
+                NAME_ALTERNATIVE, mContactsPreferences));
+    }
+}
diff --git a/tests/src/com/android/contacts/common/util/DateUtilTests.java b/tests/src/com/android/contacts/common/util/DateUtilTests.java
new file mode 100644
index 0000000..f460289
--- /dev/null
+++ b/tests/src/com/android/contacts/common/util/DateUtilTests.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014 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 junit.framework.TestCase;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.format.Time;
+
+/**
+ * Unit tests for {@link com.android.contacts.common.util.DateUtils}.
+ */
+@SmallTest
+public class DateUtilTests extends TestCase {
+
+    /**
+     * Test date differences which are in the same day.
+     */
+    public void testDayDiffNone() {
+        Time time = new Time();
+        long date1 = System.currentTimeMillis();
+        long date2 = System.currentTimeMillis() + android.text.format.DateUtils.HOUR_IN_MILLIS;
+        assertEquals(0, DateUtils.getDayDifference(time, date1, date2));
+        assertEquals(0, DateUtils.getDayDifference(time, date2, date1));
+    }
+
+    /**
+     * Test date differences which are a day apart.
+     */
+    public void testDayDiffOne() {
+        Time time = new Time();
+        long date1 = System.currentTimeMillis();
+        long date2 = date1 + android.text.format.DateUtils.DAY_IN_MILLIS;
+        assertEquals(1, DateUtils.getDayDifference(time, date1, date2));
+        assertEquals(1, DateUtils.getDayDifference(time, date2, date1));
+    }
+
+    /**
+     * Test date differences which are two days apart.
+     */
+    public void testDayDiffTwo() {
+        Time time = new Time();
+        long date1 = System.currentTimeMillis();
+        long date2 = date1 + 2*android.text.format.DateUtils.DAY_IN_MILLIS;
+        assertEquals(2, DateUtils.getDayDifference(time, date1, date2));
+        assertEquals(2, DateUtils.getDayDifference(time, date2, date1));
+    }
+}
diff --git a/tests/src/com/android/contacts/common/util/NameConverterTests.java b/tests/src/com/android/contacts/common/util/NameConverterTests.java
new file mode 100644
index 0000000..5a261eb
--- /dev/null
+++ b/tests/src/com/android/contacts/common/util/NameConverterTests.java
@@ -0,0 +1,101 @@
+/*
+ * 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.util;
+
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+
+import com.android.contacts.common.util.NameConverter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tests for {@link NameConverter}.
+ */
+@SmallTest
+public class NameConverterTests extends AndroidTestCase {
+
+    public void testStructuredNameToDisplayName() {
+        Map<String, String> structuredName = new HashMap<String, String>();
+        structuredName.put(StructuredName.PREFIX, "Mr.");
+        structuredName.put(StructuredName.GIVEN_NAME, "John");
+        structuredName.put(StructuredName.MIDDLE_NAME, "Quincy");
+        structuredName.put(StructuredName.FAMILY_NAME, "Adams");
+        structuredName.put(StructuredName.SUFFIX, "Esquire");
+
+        assertEquals("Mr. John Quincy Adams, Esquire",
+                NameConverter.structuredNameToDisplayName(mContext, structuredName));
+
+        structuredName.remove(StructuredName.SUFFIX);
+        assertEquals("Mr. John Quincy Adams",
+                NameConverter.structuredNameToDisplayName(mContext, structuredName));
+
+        structuredName.remove(StructuredName.MIDDLE_NAME);
+        assertEquals("Mr. John Adams",
+                NameConverter.structuredNameToDisplayName(mContext, structuredName));
+    }
+
+    public void testDisplayNameToStructuredName() {
+        assertStructuredName("Mr. John Quincy Adams, Esquire",
+                "Mr.", "John", "Quincy", "Adams", "Esquire");
+        assertStructuredName("John Doe", null, "John", null, "Doe", null);
+        assertStructuredName("Ms. Jane Eyre", "Ms.", "Jane", null, "Eyre", null);
+        assertStructuredName("Dr Leo Spaceman, PhD", "Dr", "Leo", null, "Spaceman", "PhD");
+    }
+
+    /**
+     * Helper method to check whether a given display name parses out to the other parameters.
+     * @param displayName Display name to break into a structured name.
+     * @param prefix Expected prefix (null if not expected).
+     * @param givenName Expected given name (null if not expected).
+     * @param middleName Expected middle name (null if not expected).
+     * @param familyName Expected family name (null if not expected).
+     * @param suffix Expected suffix (null if not expected).
+     */
+    private void assertStructuredName(String displayName, String prefix,
+            String givenName, String middleName, String familyName, String suffix) {
+        Map<String, String> structuredName = NameConverter.displayNameToStructuredName(mContext,
+                displayName);
+        checkNameComponent(StructuredName.PREFIX, prefix, structuredName);
+        checkNameComponent(StructuredName.GIVEN_NAME, givenName, structuredName);
+        checkNameComponent(StructuredName.MIDDLE_NAME, middleName, structuredName);
+        checkNameComponent(StructuredName.FAMILY_NAME, familyName, structuredName);
+        checkNameComponent(StructuredName.SUFFIX, suffix, structuredName);
+        assertEquals(0, structuredName.size());
+    }
+
+    /**
+     * Checks that the given field and value are present in the structured name map (or not present
+     * if the given value is null).  If the value is present and matches, the key is removed from
+     * the map - once all components of the name are checked, the map should be empty.
+     * @param field Field to check.
+     * @param value Expected value for the field (null if it is not expected to be populated).
+     * @param structuredName The map of structured field names to values.
+     */
+    private void checkNameComponent(String field, String value,
+            Map<String, String> structuredName) {
+        if (TextUtils.isEmpty(value)) {
+            assertNull(structuredName.get(field));
+        } else {
+            assertEquals(value, structuredName.get(field));
+        }
+        structuredName.remove(field);
+    }
+}
diff --git a/tests/src/com/android/contacts/common/util/SearchUtilTest.java b/tests/src/com/android/contacts/common/util/SearchUtilTest.java
new file mode 100644
index 0000000..3176a3c
--- /dev/null
+++ b/tests/src/com/android/contacts/common/util/SearchUtilTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link SearchUtil}.
+ */
+@SmallTest
+public class SearchUtilTest extends TestCase {
+
+    public void testFindMatchingLine() {
+        final String actual = "this is a long test string.\nWith potentially many lines.\n" +
+                "test@google.com\nhello\nblah\n'leading punc";
+
+        SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(actual, "poten");
+        assertEquals("With potentially many lines.", matched.line);
+        assertEquals(5, matched.startIndex);
+
+        // Full line match.
+        matched = SearchUtil.findMatchingLine(actual, "hello");
+        assertEquals("hello", matched.line);
+        assertEquals(0, matched.startIndex);
+
+        // First line match
+        matched = SearchUtil.findMatchingLine(actual, "this");
+        assertEquals("this is a long test string.", matched.line);
+        assertEquals(0, matched.startIndex);
+
+        // Last line match
+        matched = SearchUtil.findMatchingLine(actual, "punc");
+        assertEquals("'leading punc", matched.line);
+        assertEquals(9, matched.startIndex);
+    }
+
+    public void testContains() {
+        final String actual = "this is a long test string.\nWith potentially many lines.\n" +
+                "test@google.com\nhello\nblah\n'leading punc";
+        assertEquals(0, SearchUtil.contains(actual, "this"));
+        assertEquals(10, SearchUtil.contains(actual, "lon"));
+
+        assertEquals(1, SearchUtil.contains("'leading punc", "lead"));
+        assertEquals(9, SearchUtil.contains("'leading punc", "punc"));
+
+    }
+
+    public void testContainsNotFound() {
+        final String actual = "this is a long test string.\nWith potentially many lines.\n" +
+                "test@google.com\nhello\nblah\n'leading punc";
+
+        // Non-prefix
+        assertEquals(-1, SearchUtil.contains(actual, "ith"));
+        assertEquals(-1, SearchUtil.contains(actual, "ing"));
+
+        // Complete misses
+        assertEquals(-1, SearchUtil.contains(actual, "thisx"));
+        assertEquals(-1, SearchUtil.contains(actual, "manyx"));
+        assertEquals(-1, SearchUtil.contains(actual, "hellox"));
+
+        // Test for partial match of start of query to end of line
+        assertEquals(-1, SearchUtil.contains(actual, "punctual"));
+    }
+
+    public void testFindNextTokenStart() {
+        final String actual = "....hello.kitty";
+        //                     012345678901234
+
+        // Find first token.
+        assertEquals(4, SearchUtil.findNextTokenStart(actual, 0));
+        assertEquals(4, SearchUtil.findNextTokenStart(actual, 1));
+        assertEquals(4, SearchUtil.findNextTokenStart(actual, 2));
+        assertEquals(4, SearchUtil.findNextTokenStart(actual, 3));
+
+        // Find second token.
+        assertEquals(10, SearchUtil.findNextTokenStart(actual, 4));
+        assertEquals(10, SearchUtil.findNextTokenStart(actual, 5));
+        assertEquals(10, SearchUtil.findNextTokenStart(actual, 6));
+        assertEquals(10, SearchUtil.findNextTokenStart(actual, 7));
+        assertEquals(10, SearchUtil.findNextTokenStart(actual, 8));
+        assertEquals(10, SearchUtil.findNextTokenStart(actual, 9));
+
+        // No token.
+        assertEquals(actual.length(), SearchUtil.findNextTokenStart(actual, 10));
+        assertEquals(actual.length(), SearchUtil.findNextTokenStart(actual, 11));
+        assertEquals(actual.length(), SearchUtil.findNextTokenStart(actual, 12));
+        assertEquals(actual.length(), SearchUtil.findNextTokenStart(actual, 13));
+        assertEquals(actual.length(), SearchUtil.findNextTokenStart(actual, 14));
+    }
+
+    public void testCleanStartAndEndOfSearchQuery() {
+        assertEquals("test", SearchUtil.cleanStartAndEndOfSearchQuery("...test..."));
+        assertEquals("test", SearchUtil.cleanStartAndEndOfSearchQuery(" test "));
+        assertEquals("test", SearchUtil.cleanStartAndEndOfSearchQuery(" ||test"));
+        assertEquals("test", SearchUtil.cleanStartAndEndOfSearchQuery("test.."));
+    }
+
+}
diff --git a/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java b/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java
index 4802b46..86167c1 100644
--- a/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java
+++ b/tests/src/com/android/contacts/interactions/ContactInteractionUtilTest.java
@@ -15,12 +15,9 @@
  */
 package com.android.contacts.interactions;
 
-import com.android.contacts.common.R;
-
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.test.AndroidTestCase;
-import android.text.format.DateUtils;
 
 import java.util.Calendar;
 import java.util.Locale;
@@ -80,50 +77,15 @@
                         getContext()));
     }
 
-    public void testFormatDateStringFromTimestamp_yesterday() {
-        // Test yesterday and tomorrow (Yesterday or Tomorrow shown)
-        calendar.add(Calendar.DAY_OF_YEAR, -1);
-        assertEquals(getContext().getResources().getString(R.string.yesterday),
-                ContactInteractionUtil.formatDateStringFromTimestamp(calendar.getTimeInMillis(),
-                        getContext()));
-    }
-
-    public void testFormatDateStringFromTimestamp_yesterdayLastYear() {
-        // Set to non leap year
-        calendar.set(Calendar.YEAR, 1999);
-        calendar.set(Calendar.DAY_OF_YEAR, 365);
-        long lastYear = calendar.getTimeInMillis();
-        calendar.add(Calendar.DAY_OF_YEAR, 1);
-
-        assertEquals(getContext().getResources().getString(R.string.yesterday),
-                ContactInteractionUtil.formatDateStringFromTimestamp(lastYear,
-                        getContext(), calendar));
-    }
-
-    public void testFormatDateStringFromTimestamp_tomorrow() {
-        calendar.add(Calendar.DAY_OF_YEAR, 1);
-        assertEquals(getContext().getResources().getString(R.string.tomorrow),
-                ContactInteractionUtil.formatDateStringFromTimestamp(calendar.getTimeInMillis(),
-                        getContext()));
-    }
-
-    public void testFormatDateStringFromTimestamp_tomorrowNewYear() {
-        calendar.set(Calendar.DAY_OF_YEAR, 1);
-        long thisYear = calendar.getTimeInMillis();
-        calendar.add(Calendar.DAY_OF_YEAR, -1);
-
-        assertEquals(getContext().getResources().getString(R.string.tomorrow),
-                ContactInteractionUtil.formatDateStringFromTimestamp(thisYear,
-                        getContext(), calendar));
-    }
-
     public void testFormatDateStringFromTimestamp_other() {
         // Test other (Month Date)
         calendar.set(
                 /* year = */ 1991,
                 /* month = */ Calendar.MONTH,
-                /* day = */ 11);
-        assertEquals("March 11",
+                /* day = */ 11,
+                /* hourOfDay = */ 8,
+                /* minute = */ 8);
+        assertEquals("Monday, March 11, 1991, 8:08 AM",
                 ContactInteractionUtil.formatDateStringFromTimestamp(calendar.getTimeInMillis(),
                         getContext()));
     }