am b591640f: am 7f2bbd3b: am 1cd70006: Merge "Adjust a variable type conversion."

* commit 'b591640f590b9ba133d1cae37aba038d1f9d4a86':
  Adjust a variable type conversion.
diff --git a/proguard.flags b/proguard.flags
index f56ffff..308513b 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -7,7 +7,10 @@
   int getCount();
 }
 
-# Any methods whose name is '*ForTest' are preserved.
--keep class ** {
-  *** *ForTest(...);
+# Any class or method annotated with NeededForTesting.
+-keep @com.android.providers.contacts.util.NeededForTesting class *
+-keepclassmembers class * {
+@com.android.providers.contacts.util.NeededForTesting *;
 }
+
+-verbose
\ No newline at end of file
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index df3e1f6..2f6562b 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android-kernprogramme"</string>
     <string name="app_label" msgid="3389954322874982620">"Kontakte-berging"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontakte"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Gradeer kontaktedatabasis op."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Kontakopgradering benodig meer berging"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Gradeer kontakberging op"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Kies om die opgradering te voltooi."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Gradeer kontaktedatabasis op."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontakte-opgradering benodig meer geheue."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Gradeer berging vir kontakte op"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Raak om die opgradering te voltooi."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontakte"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Ander"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Kry toegang tot alle stemboodskappe"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Laat die program alle stemboodskappe stoor en ophaal wat hierdie toestel kan lees."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Laat die program toe om alle stemboodskappe te stoor en op te haal wat hierdie toestel kan lees."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Stemboodskap van "</string>
 </resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index ce2da2f..6aa55ec 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"የAndroid ኮር ትግበራ"</string>
     <string name="app_label" msgid="3389954322874982620">"የዕውቂያ ማከማቻ"</string>
     <string name="provider_label" msgid="6012150850819899907">"እውቅያዎች"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"የእውቂያዎችን ውሂብ ጎታ በማሻሻል ላይ፡፡"</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"የእውቅያ አልቅ ተጨማሪ ማህደረ ትውስታ ይፈልጋል"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"የእውቂያ ማከማቻን ማላቅ"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"አልቁን ለማጠናቀቅ ምረጥ።"</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"የእውቂያዎችን ውሂብ ጎታ በማሻሻል ላይ፡፡"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"የእውቅያዎች  አሻሽል  ተጨማሪ ማህደረ ትውስታ ይፈልጋል"</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"ለእውቅያዎች ማከማቻ በማሻሻል ላይ"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"አሻሽሉን ለማጠናቀቅ ንካ፡፡"</string>
     <string name="default_directory" msgid="93961630309570294">"እውቅያዎች"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"ሌላ"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"ሁሉንም  የድምፅ መልዕክቶች ድረስ"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"ትግበራ ይህ መሣሪያ መድረስ  የሚችለውን የድምፅ መልዕክቶች በሙሉ ለማከማቸት እና ሰርስሮ ለማውጣት ይፈቅዳል።"</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"ትግበራ ይህ መሣሪያ መድረስ የሚችለውን የድምፅ መልዕክቶች በሙሉ ለማከማቸት እና ሰርስሮ ለማውጣት ይፈቅዳል።"</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"ከ....የድምፅ መልዕክት "</string>
 </resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 9e0ac4e..3cd298d 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"تطبيقات Android المركزية"</string>
     <string name="app_label" msgid="3389954322874982620">"تخزين جهات الاتصال"</string>
     <string name="provider_label" msgid="6012150850819899907">"جهات الاتصال"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"جارٍ ترقية قاعدة بيانات جهات الاتصال."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"تحتاج ترقية جهة الاتصال إلى مزيد من الذاكرة"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"سعة تخزين ترقية جهة الاتصال"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"حدد لإكمال الترقية."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"جارٍ ترقية قاعدة بيانات جهات الاتصال."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"تتطلب ترقية جهات الاتصال مزيدًا من الذاكرة."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"ترقية وحدة التخزين لجهات الاتصال"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"المس لإكمال عملية الترقية."</string>
     <string name="default_directory" msgid="93961630309570294">"جهات الاتصال"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"غير ذلك"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"الوصول إلى جميع رسائل البريد الصوتي"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"للسماح للتطبيق بتخزين واسترداد جميع رسائل البريد الصوتي التي يمكن الوصول إليها عبر هذا الجهاز."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"للسماح للتطبيق بتخزين واسترداد جميع رسائل البريد الصوتي التي يمكن الوصول إليها عبر هذا الجهاز."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"بريد صوتي من "</string>
 </resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 7f24194..44357c3 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Асноўныя праграмы для Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Сховішча кантактаў"</string>
     <string name="provider_label" msgid="6012150850819899907">"Кантакты"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Абнаўленне базы дадзеных кантактаў."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Для абнаўлення кантакту патрабуецца больш памяці"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Абнаўленне сховішча кантактаў"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Выберыце, каб завяршыць абнаўленне."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Абнаўленне базы дадзеных кантактаў."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Для абнаўлення кантактаў патрабуецца больш памяці."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Каб абнавiць кантакты, патрабуецца больш памяцi"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Націсніце, каб завершыць абнаўленне."</string>
     <string name="default_directory" msgid="93961630309570294">"Кантакты"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Іншае"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Доступ да ўсіх галасавых паведамленняў"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Дазваляе прыкладанням захоўваць і ўзнаўляць усе галасавыя паведамленні, да якіх можа даступіцца гэта прылада"</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Дазваляе прыкладанню захоўваць і прайграваць усе даступныя для гэтай прылады паведамленні галасавой пошты."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Галасавое паведамленне ад "</string>
 </resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 13df956..dee34a7 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Основни приложения на Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Хранилище на контакти"</string>
     <string name="provider_label" msgid="6012150850819899907">"Контакти"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Базата от данни на контактите се надгражда."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Надстройването на контакта се нуждае от още памет"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Надстройка на хранилището за контакти"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Изберете за завършване на надстройването."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Базата от данни на контактите се надстройва."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"За надстройването на контактите е необходима още памет."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Хранилището за контакти се надстройва"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Докоснете, за да завършите надстройването."</string>
     <string name="default_directory" msgid="93961630309570294">"Контакти"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Други"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Достъп до всички гласови съобщения"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Разрешава на приложението да съхранява и извлича всички гласови съобщения, до които това устройство има достъп."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Разрешава на приложението да съхранява и извлича всички гласови съобщения, до които това устройство има достъп."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Гласова поща от "</string>
 </resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 8df6be3..ef073fa 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Aplicacions bàsiques d\'Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Emmagatzematge de contactes"</string>
     <string name="provider_label" msgid="6012150850819899907">"Contactes"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"S\'està actualitzant la base de dades de Contactes."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Per actualitzar els contactes cal tenir més memòria"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"S\'està actualitzant l\'emmagatzematge dels contactes"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Seleccioneu-ho per completar l\'actualització."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"S\'està actualitzant la base de dades dels contactes."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Per actualitzar els contactes cal tenir més memòria."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"S\'està actualitzant l\'emmagatzematge per als contactes"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toca-ho per completar l\'actualització."</string>
     <string name="default_directory" msgid="93961630309570294">"Contactes"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Altres"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Accés a tots els missatges de veu"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Permet que l\'aplicació emmagatzemi i recuperi tots els missatges de veu als quals pot accedir aquest dispositiu."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Permet que l\'aplicació emmagatzemi i recuperi totes les bústies de veu a les quals pot accedir aquest dispositiu."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Missatge de veu de "</string>
 </resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 3da1f3d..76335a0 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"Úložiště kontaktů"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontakty"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Aktualizace databáze kontaktů."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Upgrade kontaktu vyžaduje více paměti"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Nedostatek paměti pro upgrade kontaktu"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Výběrem této možnosti dokončíte upgrade."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Probíhá upgrade databáze kontaktů."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Upgrade kontaktů vyžaduje více paměti."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Probíhá upgrade úložiště kontaktů"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dotykem dokončíte upgrade."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontakty"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Jiné"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Přístup ke všem hlasovým zprávám"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Umožňuje aplikaci ukládat a načítat všechny hlasové zprávy, ke kterým má toto zařízení přístup."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Umožňuje aplikaci ukládat a načítat všechny hlasové zprávy, ke kterým má toto zařízení přístup."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Hlasová zpráva od uživatele "</string>
 </resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 76bb37f..7b708fb 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android-kerneapplikationer"</string>
     <string name="app_label" msgid="3389954322874982620">"Lagring af kontakter"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontakter"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Opgraderer databasen med kontaktpersoner."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Opgradering af kontaktpersoner kræver mere hukommelse"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Opgraderer lagerplads til kontaktpersoner"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Vælg for at fuldføre opgraderingen."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Opgraderer databasen med kontaktpersoner."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Opgradering af kontaktpersoner kræver mere hukommelse."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Opgraderer lagring af kontaktpersoner"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Tryk for at fuldføre opgraderingen."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontaktpersoner"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Andre"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Adgang til alle telefonsvarerbeskeder"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Tillader applikationen at gemme og hente alle telefonsvarerbeskeder, som denne enhed kan få adgang til."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Tillader, at applikationen gemmer og henter alle telefonsvarerbeskeder, som denne enhed kan få adgang til."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Telefonsvarerbesked fra "</string>
 </resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 6827f15..1f10b19 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"Kontakte-Speicher"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontakte"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Kontaktdatenbank wird aktualisiert..."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Kontaktupgrade erfordert mehr Speicher"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Kontaktspeicher wird aktualisiert"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Zum Abschluss des Upgrades auswählen"</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Kontaktdatenbank wird aktualisiert..."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontakte-Upgrade erfordert mehr Speicher."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Speicher für Kontakte wird aktualisiert..."</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Zum Abschluss des Upgrades berühren"</string>
     <string name="default_directory" msgid="93961630309570294">"Kontakte"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Sonstige"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Zugriff auf alle Mailbox-Nachrichten"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Ermöglicht der Anwendung das Speichern und Abrufen aller Mailbox-Nachrichten, auf die dieses Gerät zugreifen kann"</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Ermöglicht der App das Speichern und Abrufen aller Mailbox-Nachrichten, auf die dieses Gerät zugreifen kann"</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Mailbox-Nachricht von "</string>
 </resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 4fb21c9..2622515 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Βασικές εφαρμογές Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Χώρος αποθήκευσης επαφών"</string>
     <string name="provider_label" msgid="6012150850819899907">"Επαφές"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Αναβάθμιση βάσης δεδομένων επαφών"</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Η αναβάθμιση των επαφών απαιτεί περισσότερη μνήμη"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Αναβάθμιση χώρου αποθήκευσης επαφών"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Επιλέξτε για την ολοκλήρωση της αναβάθμισης."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Αναβάθμιση βάσης δεδομένων επαφών"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Απαιτείται περισσότερη μνήμη για την αναβάθμιση των επαφών."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Αναβάθμιση χώρου αποθήκευσης για επαφές"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Αγγίξτε για να ολοκληρώσετε την αναβάθμιση."</string>
     <string name="default_directory" msgid="93961630309570294">"Επαφές"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Άλλο"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Πρόσβαση σε όλα τα μηνύματα αυτόματου τηλεφωνητή"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Επιτρέπει στην εφαρμογή την αποθήκευση και ανάκτηση όλων των μηνυμάτων αυτόματου τηλεφωνητή, στα οποία έχει πρόσβαση αυτή η συσκευή."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Επιτρέπει στην εφαρμογή την αποθήκευση και ανάκτηση όλων των μηνυμάτων αυτόματου τηλεφωνητή, στα οποία μπορεί να έχει πρόσβαση αυτή η συσκευή."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Μήνυμα αυτόματου τηλεφωνητή από "</string>
 </resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index efa2d60..9cbc783 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"Contacts Storage"</string>
     <string name="provider_label" msgid="6012150850819899907">"Contacts"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Upgrading Contacts database."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Contact upgrade needs more memory"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Upgrading contact storage"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Select to complete the upgrade."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Upgrading contacts database."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Contact upgrade needs more memory."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Upgrading storage for contacts"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Touch to complete the upgrade."</string>
     <string name="default_directory" msgid="93961630309570294">"Contacts"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Other"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Access all voicemails"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Allows the application to store and retrieve all voicemails that this device can access."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Allows the app to store and retrieve all voicemails that this device can access."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Voicemail from "</string>
 </resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index b09e262..a3cddf2 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Aplicaciones básicas de Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Almacenamiento para contactos"</string>
     <string name="provider_label" msgid="6012150850819899907">"Contactos"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Actualizando la base de datos de  los contactos"</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"La actualización de los contactos necesita más memoria."</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Actualizando almacenamiento de contactos"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Selecciona para completar la actualización."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Actualizando la base de datos de los contactos"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"La actualización de contactos necesita más memoria."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Actualizando el espacio de almacenamiento de los contactos"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toca para completar la actualización."</string>
     <string name="default_directory" msgid="93961630309570294">"Contactos"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Otro"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Acceder a todos los mensajes de voz"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Permite que la aplicación almacene y recupere todos los mensajes de voz a los que este dispositivo puede acceder."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Permite que la aplicación almacene y recupere todos los mensajes de voz a los que este dispositivo puede acceder."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Mensaje de voz de "</string>
 </resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index b901512..d4f7edf 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Aplicaciones básicas de Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Información de los contactos"</string>
     <string name="provider_label" msgid="6012150850819899907">"Contactos"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Actualizando base de datos de contactos"</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"La actualización del contacto necesita más memoria."</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Espacio de almacenamiento de actualización de contactos"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Selecciona esta opción para completar la actualización."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Actualizando la base de datos de contactos..."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"La actualización de contactos necesita más memoria."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Actualizando el almacenamiento para contactos..."</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toca para completar la actualización."</string>
     <string name="default_directory" msgid="93961630309570294">"Contactos"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Otro"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Acceder a todos los mensajes de voz"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Permite que la aplicación almacene y recupere todos los mensajes de voz a los que puede acceder este dispositivo."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Permite que la aplicación almacene y recupere todos los mensajes de voz a los que puede acceder este dispositivo."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Mensaje de voz de "</string>
 </resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 5cac543..6fda0d5 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Androidi tuumrakendused"</string>
     <string name="app_label" msgid="3389954322874982620">"Kontaktiruum"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontaktid"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Kontaktide andmebaasi uuendamine."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Kontakti uuendamiseks on vaja rohkem mälu"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Kontakti mäluruumi uuendamine"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Valige uuenduse lõpule viimiseks."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Kontaktide andmebaasi uuendamine."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontaktisikute uuendamiseks on vaja rohkem mälu"</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Kontaktide salvestusruumi uuendamine"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Puudutage uuendamise lõpuleviimiseks."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontaktid"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Muu"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Juurdepääs kõigile kõnepostisõnumitele"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Võimaldab rakendusel hoida ja vastu võtta kõik kõnepostisõnumid, millele see seade juurde pääseb."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Võimaldab rakendusel salvestada ja vastu võtta kõik kõnepostisõnumid, mille juurde seade pääseb."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Kõnepost kontaktilt "</string>
 </resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 687b0c4..bcdf710 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"حافظه مخاطبین"</string>
     <string name="provider_label" msgid="6012150850819899907">"مخاطبین"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"ارتقاء پایگاه داده مخاطبین."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"برای ارتقاء مخاطب به حافظه بیشتری نیاز است"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"ارتقا حافظه مخاطب"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"برای کامل کردن ارتقا انتخاب کنید."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"ارتقا پایگاه داده مخاطبین."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"برای ارتقای مخاطبین به حافظه بیشتری نیاز است."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"ارتقا حافظه برای مخاطبین"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"برای تکمیل ارتقا لمس کنید."</string>
     <string name="default_directory" msgid="93961630309570294">"مخاطبین"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"سایر موارد"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"دسترسی به تمام پست‌های صوتی"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"به برنامه اجازه ذخیره و بازیابی تمام پست‌های صوتی قابل دسترس برای این دستگاه را می‌دهد."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"به برنامه اجازه ذخیره و بازیابی تمام پست‌های صوتی قابل دسترس برای این دستگاه را می‌دهد."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"پست صوتی از "</string>
 </resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index e39fc54..d9c341f 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Androidin ydinsovellukset"</string>
     <string name="app_label" msgid="3389954322874982620">"Yhteystietojen tallennus"</string>
     <string name="provider_label" msgid="6012150850819899907">"Yhteystiedot"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Päivitetään yhteystietojen tietokantaa."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Yhteystietojen päivittämiseen tarvitaan lisää muistia"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Päivitetään yhteystietojen tallennustilaa"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Suorita päivitys loppuun valitsemalla."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Päivitetään yhteystietojen tietokantaa."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Yhteystietojen päivittämiseen tarvitaan enemmän muistia."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Päivitetään yhteystietojen tallennustilaa"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Suorita päivitys loppuun koskettamalla."</string>
     <string name="default_directory" msgid="93961630309570294">"Yhteystiedot"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Muu"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Kaikkien vastaajaviestien käyttäminen"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Antaa sovelluksen tallentaa ja hakea kaikki vastaajaviestit, jotka ovat laitteen käytettävissä."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Antaa sovelluksen tallentaa ja hakea kaikki vastaajaviestit, jotka ovat laitteen käytettävissä."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Vastaajaviesti henkilöltä "</string>
 </resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 6d794f2..b25b763 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Applications de base Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Liste des contacts"</string>
     <string name="provider_label" msgid="6012150850819899907">"Contacts"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Mise à jour de la base de données Contacts."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"La mise à niveau des contacts requiert plus de mémoire."</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Mise à niveau du stockage des contacts"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Sélectionnez pour effectuer la mise à niveau."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Mise à jour de la base de données des contacts en cours…"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"La mise à jour des contacts requiert plus de mémoire."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Mise à jour du stockage des contacts"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Appuyez pour terminer la mise à jour."</string>
     <string name="default_directory" msgid="93961630309570294">"Contacts"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Autre"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Accéder à tous les messages vocaux"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Permet à l\'application de stocker et de récupérer tous les messages vocaux auxquels ce service peut accéder."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Permet à l\'application de stocker et de récupérer tous les messages vocaux auxquels cet appareil peut accéder."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Message vocal de "</string>
 </resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 153767b..f2d706a 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"संपर्क संग्रहण"</string>
     <string name="provider_label" msgid="6012150850819899907">"संपर्क"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"संपर्क डेटाबेस अपग्रेड हो रहा है."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"संपर्क अपग्रेड के लिए अधिक स्मृति की आवश्यकता होती है"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"संपर्क संग्रहण अपग्रेड किया जा रहा है"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"अपग्रेड पूर्ण करने के लिए चयन करें."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"संपर्क डेटाबेस अपग्रेड हो रहा है."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"संपर्क अपग्रेड के लिए अधिक स्मृति की आवश्यकता होती है."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"संपर्कों के लिए संग्रहण अपग्रेड करना"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"अपग्रेड पूर्ण करने के लिए स्पर्श करें."</string>
     <string name="default_directory" msgid="93961630309570294">"संपर्क"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"अन्य"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"सभी ध्‍वनि‍मेल पर पहुंचें"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"एप्‍लि‍केशन को ऐसे सभी ध्‍वनि‍मेल संग्रहीत और पुनर्प्राप्त करने देता है जि‍न पर यह उपकरण पहुंच सकता है."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"एप्‍लि‍केशन को ऐसे सभी ध्‍वनि‍मेल संग्रहीत और पुनर्प्राप्त करने देता है जि‍न पर यह उपकरण पहुंच सकता है."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"इनका ध्‍वनि‍मेल: "</string>
 </resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index f00b088..2b53781 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Matične aplikacije za Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Prostor za pohranu kontakata"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontakti"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Nadogradnja baze podataka kontakata."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Za nadogradnju kontakta potrebno je više memorije"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Nadogradnja pohrane kontakata"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Odaberite da biste dovršili nadogradnju."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Nadogradnja baze podataka kontakata."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Za nadogradnju kontakata potrebno je više memorije."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Nadogradnja pohrane za kontakte"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dodirnite da biste dovršili nadogradnju."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontakti"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Drugo"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Pristup svoj govornoj pošti"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Omogućuje aplikaciji pohranu i dohvat sve govorne pošte kojoj uređaj može pristupiti."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Aplikaciji omogućuje pohranjivanje i dohvaćanje svih poruka govorne pošte kojoj ovaj uređaj može pristupati."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Govorna pošta od "</string>
 </resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 934bd73..7f477e5 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Alap Android-alkalmazások"</string>
     <string name="app_label" msgid="3389954322874982620">"Névjegytár"</string>
     <string name="provider_label" msgid="6012150850819899907">"Címtár"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Névjegyek adatbázisának frissítése"</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"A névjegy frissítéséhez több memóriára van szükség"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"A névjegy-tárhely frissítése"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Válassza ezt a verziófrissítéshez."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Névjegyek adatbázisának frissítése."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"A névjegyek frissítéséhez több memóriára van szükség."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Névjegyek tárolójának frissítése"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Érintse meg a frissítés befejezéséhez."</string>
     <string name="default_directory" msgid="93961630309570294">"Címtár"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Egyéb"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Hozzáférés az összes hangüzenethez"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Lehetővé teszi, hogy az alkalmazás tárolja és lekérje azokat a hangüzeneteket, amelyekhez ez a készülék hozzáférhet."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Lehetővé teszi, hogy az alkalmazás tárolja és lekérje azokat a hangüzeneteket, amelyekhez ez a készülék hozzáférhet."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Hangüzenet tőle: "</string>
 </resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 34600a8..f958b12 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Apl Inti Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Penyimpanan Kenalan"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kenalan"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Meningkatkan versi basis data Kenalan."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Peningkatan versi kenalan memerlukan lebih banyak memori"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Meningkatkan penyimpanan kenalan"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Pilih untuk menyelesaikan peningkatan versi."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Meningkatkan versi basis data kenalan."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Peningkatan versi kenalan memerlukan lebih banyak memori."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Meningkatkan versi penyimpanan untuk kenalan"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Sentuh untuk menyelesaikan peningkatan versi."</string>
     <string name="default_directory" msgid="93961630309570294">"Kenalan"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Lainnya"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Akses semua pesan suara"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Mengizinkan aplikasi menyimpan dan mengambil semua pesan suara yang dapat diakses oleh perangkat ini."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Mengizinkan apl menyimpan dan mengambil semua pesan suara yang dapat diakses oleh perangkat ini."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Kotak pesan dari "</string>
 </resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 719dc98..dfea4d4 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Applicazioni di base Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Archiviazione contatti"</string>
     <string name="provider_label" msgid="6012150850819899907">"Contatti"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Upgrade del database di Contatti."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"L\'aggiornamento dei contatti richiede più memoria"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Aggiornamento dell\'archivio contatti"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Seleziona per completare l\'aggiornamento."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Upgrade del database dei contatti."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"L\'upgrade dei contatti richiede più memoria."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Upgrade dell\'archivio dei contatti"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Tocca per completare l\'upgrade."</string>
     <string name="default_directory" msgid="93961630309570294">"Contatti"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Altro"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Accesso a tutti i messaggi vocali"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Consente all\'applicazione di archiviare e recuperare tutti i messaggi vocali a cui questo dispositivo può accedere."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Consente all\'applicazione di archiviare recuperare tutti i messaggi vocali a cui questo dispositivo può accedere."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Messaggio vocale da "</string>
 </resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 0840371..c87a2f9 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"אחסון אנשי קשר"</string>
     <string name="provider_label" msgid="6012150850819899907">"אנשי קשר"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"משדרג את מסד הנתונים של אנשי הקשר."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"שדרוג אנשי קשר דורש זיכרון נוסף"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"משדרג אחסון של אנשי קשר"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"בחר כדי להשלים את השדרוג."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"משדרג את מסד הנתונים של אנשי הקשר."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"שדרוג אנשי הקשר מחייב זיכרון נוסף."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"משדרג את האחסון של אנשי קשר"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"גע כדי לבצע את השדרוג."</string>
     <string name="default_directory" msgid="93961630309570294">"אנשי קשר"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"אחר"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"גישה לכל הודעות הדואר הקולי"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"מאפשר ליישום לאחסן ולאחזר את כל הודעות הדואר הקולי שמכשיר זה יכול לגשת אליהן."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"מאפשר ליישום לאחסן ולאחזר את כל הודעות הדואר הקולי שלמכשיר זה יש גישה אליהן."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"הודעה קולית מאת "</string>
 </resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index ec64581..74044d8 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"アドレス帳"</string>
     <string name="provider_label" msgid="6012150850819899907">"連絡先"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"連絡先データベースをアップグレードしています。"</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"連絡先のアップグレードに必要なメモリが不足しています"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"連絡先ストレージのアップグレード"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"アップグレードを完了するには選択してください。"</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"連絡先データベースをアップグレードしています。"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"連絡先のアップグレードに必要なメモリが不足しています。"</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"連絡先用ストレージのアップグレード"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"アップグレードを完了するにはタップしてください。"</string>
     <string name="default_directory" msgid="93961630309570294">"連絡先"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"その他"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"すべてのボイスメールにアクセス"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"この端末でアクセス可能なすべてのボイスメールを保存、取得することをアプリケーションに許可します。"</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"この端末でアクセス可能なすべてのボイスメールを保存、取得することをアプリに許可します。"</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"受信ボイスメール: "</string>
 </resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index a1a1b91..041cc0b 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core 앱"</string>
     <string name="app_label" msgid="3389954322874982620">"주소록 저장소"</string>
     <string name="provider_label" msgid="6012150850819899907">"주소록"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"주소록 데이터베이스 업그레이드 중"</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"연락처를 업그레이드하려면 메모리가 더 필요합니다."</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"연락처 저장소 업그레이드"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"업그레이드를 완료하려면 선택하세요."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"주소록 데이터베이스 업그레이드 중"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"주소록을 업그레이드하려면 메모리가 더 필요합니다."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"주소록을 위한 저장소 업그레이드 중"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"업그레이드를 완료하려면 터치하세요."</string>
     <string name="default_directory" msgid="93961630309570294">"주소록"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"기타"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"모든 음성사서함에 액세스"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"애플리케이션을 사용하면 이 기기에서 액세스할 수 있는 모든 음성사서함을 저장하고 검색할 수 있습니다."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"앱이 이 기기에서 액세스할 수 있는 모든 음성사서함을 저장하고 검색하도록 허용합니다."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"음성사서함 발신자 "</string>
 </resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index b0f2cf2..b873d1f 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Pagrindinės „Android“ programos"</string>
     <string name="app_label" msgid="3389954322874982620">"Adresatų saugykla"</string>
     <string name="provider_label" msgid="6012150850819899907">"Adresinė"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Naujovinama Kontaktų duomenų bazė."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Norint naujovinti adresatą, reikia daugiau atminties"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Naujovinama adresatų saugykla"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Pasirinkite ir užbaikite naujovinimą."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Naujovinami kontaktų duomenys."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Norint naujovinti kontaktus, reikia daugiau atminties."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Naujovinama kontaktų atmintinė"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Palieskite, kad baigtumėte naujovinti."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontaktai"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Kita"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Pasiekti visus balso pašto pranešimus"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Leidžiama programai saugoti ir nuskaityti visus balso pašto pranešimus, kuriuos gali pasiekti šis įrenginys."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Leidžiama programai saugoti ir nuskaityti visus balso pašto pranešimus, kuriuos gali pasiekti šis įrenginys."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Balso pašto pranešimas nuo "</string>
 </resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 5c11d72..fa1e89e 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"Kontaktpersonu krātuve"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontaktpersonas"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Notiek kontaktpersonu datu bāzes jaunināšana."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Lai jauninātu kontaktpersonas, nepieciešams vairāk atmiņas."</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Kontaktpersonu krātuves jaunināšana"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Atlasiet, lai pabeigtu jaunināšanu."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Notiek kontaktpersonu datu bāzes jaunināšana."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Lai jauninātu kontaktpersonas, nepieciešams vairāk vietas atmiņā."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Notiek kontaktpersonu krātuves jaunināšana"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Pieskarieties, lai pabeigtu jaunināšanu."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontaktpersonas"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Cits"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Piekļuve visiem balss pasta ziņojumiem"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Ļauj lietojumprogrammai glabāt un izgūt visus balss pasta ziņojumus, kuriem var piekļūt šajā ierīcē."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Ļauj lietotnei glabāt un izgūt visus balss pasta ziņojumus, kuriem var piekļūt šajā ierīcē."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Balss pasta ziņojums no "</string>
 </resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index d00395e..653a8d9 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Apl Teras Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Storan Kenalan"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kenalan"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Menaiktaraf pangkalan data Kenalan."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Kemas kini kenalan memerlukan lagi memori"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Meningkatkan storan kenalan"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Pilih untuk menyelesaikan pengemaskinian."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Menaik taraf pangkalan data kenalan."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Peningkatan kenalan memerlukan lebih banyak memori."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Meningkatkan storan untuk kenalan"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Sentuh untuk menyelesaikan peningkatan."</string>
     <string name="default_directory" msgid="93961630309570294">"Kenalan"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Lain-lain"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Akses semua mel suara"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Membenarkan aplikasi menyimpan dan mengambil semula semua mel suara yang boleh diakses peranti ini."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Membenarkan aplikasi menyimpan dan mengambil semula semua mel suara yang boleh diakses peranti ini."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Mel suara daripada "</string>
 </resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 72bb3e9..5775854 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Kjerneapper for Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Kontaktlager"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontakter"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Oppgraderer kontaktdatabase."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Det er ikke nok minne for å oppgradere kontaktene"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Oppgraderer lagrede kontakter"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Velg for å fullføre oppgraderingen."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Oppgraderer kontaktdatabasen."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Du har ikke nok minne til å oppgradere kontaktene."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Oppgraderer lagring for kontakter"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Trykk for å fullføre oppgraderingen."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontakter"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Andre"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Tilgang til alle talemeldinger"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Gjør at applikasjonen kan lagre og hente alle talepostmeldinger som denne enheten har tilgang til."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Lar appen lagre og hente alle talepostmeldinger som denne enheten har tilgang til."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Talemelding fra "</string>
 </resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 82e7e70..1a09288 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Kernapps van Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Opslag contacten"</string>
     <string name="provider_label" msgid="6012150850819899907">"Contacten"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Contactendatabase bijwerken."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Voor het bijwerken van contacten is meer geheugen nodig"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Contactopslag bijwerken"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Selecteer om de upgrade te voltooien."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Contactendatabase bijwerken."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Voor het bijwerken van contacten is meer geheugen nodig."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Opslag voor contacten bijwerken"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Raak aan om de upgrade te voltooien."</string>
     <string name="default_directory" msgid="93961630309570294">"Contacten"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Overig"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Toegang tot alle voicemails"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Hiermee kan de app alle voicemails opslaan en ophalen waartoe dit apparaat toegang heeft."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Toestaan dat de app alle voicemails opslaat en ophaalt waartoe dit apparaat toegang heeft."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Voicemail van "</string>
 </resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index dfcf8fd..b598221 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Aplikacje główne Androida"</string>
     <string name="app_label" msgid="3389954322874982620">"Spis kontaktów"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontakty"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Uaktualnianie bazy danych Kontaktów."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Uaktualnienie kontaktów wymaga więcej pamięci"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Uaktualnianie magazynu kontaktów"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Wybierz, aby dokończyć uaktualnianie."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Uaktualnianie bazy danych kontaktów"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Uaktualnienie kontaktów wymaga więcej pamięci."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Pamięć na uaktualnienie kontaktów"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dotknij, aby dokończyć uaktualnianie."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontakty"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Inne"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Dostęp do wszystkich wiadomości głosowych"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Zezwala aplikacji na przechowywanie i pobieranie wszystkich wiadomości głosowych, do których ma dostęp to urządzenie."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Zezwala aplikacji na przechowywanie i pobieranie wszystkich wiadomości głosowych, do których ma dostęp to urządzenie."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Poczta głosowa od "</string>
 </resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index e84a22e..acdb0d2 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Aplicações Principais do Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Armazenamento de contactos"</string>
     <string name="provider_label" msgid="6012150850819899907">"Contactos"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"A atualizar a base de dados de Contactos."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"A actualização de contactos necessita de mais memória"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"A actualizar armazenamento de contactos"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Seleccione para concluir a actualização."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"A atualizar a base de dados de contactos."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"A atualização de contactos necessita de mais memória."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"A atualizar armazenamento de contactos"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toque para concluir a atualização."</string>
     <string name="default_directory" msgid="93961630309570294">"Contactos"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Outro"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Acesso a todas as mensagens de correio de voz"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Permite à aplicação guardar e recuperar todas as mensagens de correio de voz a que este aparelho pode aceder."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Permite à aplicação guardar e recuperar todas as mensagens de correio de voz a que este aparelho pode aceder."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Correio de voz de "</string>
 </resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index ec9efae..5d471a1 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Principais aplicativos do Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Armazenamento de contatos"</string>
     <string name="provider_label" msgid="6012150850819899907">"Contatos"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Atualizando o banco de dados de contatos."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"A atualização de contatos precisa de mais memória"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Armazenamento de atualização de contatos"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Selecione para concluir a atualização."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Atualizando o banco de dados de contatos."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"A atualização de contatos precisa de mais memória."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Atualização do armazenamento para contatos"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Toque para concluir a atualização."</string>
     <string name="default_directory" msgid="93961630309570294">"Contatos"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Outros"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Acessar todas as mensagens de voz"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Permite que o aplicativo armazene e recupere todas as mensagens de voz que esse dispositivo acessa."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Permite que o aplicativo armazene e recupere todas as mensagens de voz que esse dispositivo acessa."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Correio de voz de "</string>
 </resources>
diff --git a/res/values-rm/strings.xml b/res/values-rm/strings.xml
index e975b42..b0e2898 100644
--- a/res/values-rm/strings.xml
+++ b/res/values-rm/strings.xml
@@ -20,16 +20,19 @@
     <skip />
     <string name="app_label" msgid="3389954322874982620">"Glista da contacts"</string>
     <string name="provider_label" msgid="6012150850819899907">"Contacts"</string>
-    <!-- no translation found for upgrade_msg (6174884195179549239) -->
+    <!-- no translation found for upgrade_msg (8640807392794309950) -->
     <skip />
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Ina actualisaziun da contacts basegna dapli memoria"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Actualisar la memoria da contacts"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Tscherner per cumplettar l\'actualisaziun"</string>
+    <!-- no translation found for upgrade_out_of_memory_notification_ticker (7638747231223520477) -->
+    <skip />
+    <!-- no translation found for upgrade_out_of_memory_notification_title (8888171924684998531) -->
+    <skip />
+    <!-- no translation found for upgrade_out_of_memory_notification_text (8438179450336437626) -->
+    <skip />
     <string name="default_directory" msgid="93961630309570294">"Contacts"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Auter"</string>
     <!-- no translation found for read_write_all_voicemail_label (4557216100818257560) -->
     <skip />
-    <!-- no translation found for read_write_all_voicemail_description (2249895806470926882) -->
+    <!-- no translation found for read_write_all_voicemail_description (8029809937805761356) -->
     <skip />
     <!-- no translation found for voicemail_from_column (435732568832121444) -->
     <skip />
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 7ceb2ff..110f166 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"Stocarea datelor din Agendă"</string>
     <string name="provider_label" msgid="6012150850819899907">"Agendă"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Se actualizează baza de date a Agendei."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Actualizarea datelor despre persoanele din agendă necesită mai multă memorie"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Se trece la o versiune superioară a memoriei pentru persoanele din agendă"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Selectaţi pentru a finaliza trecerea la o versiune superioară."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Se actualizează baza de date a Agendei."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Actualizarea agendei necesită mai multă memorie."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Măriţi spaţiul de stocare pentru Agendă"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Atingeţi pentru a finaliza actualizarea."</string>
     <string name="default_directory" msgid="93961630309570294">"Agendă"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Altul"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Accesaţi toate mesajele vocale"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Permite aplicaţiei să stocheze şi să preia toate mesajele vocale pe care acest dispozitiv le poate accesa."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Permite aplicaţiei să stocheze şi să preia toate mesajele vocale pe care acest dispozitiv le poate accesa."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Mesaj vocal de la "</string>
 </resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 60ae7b9..45b28a0 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Основные приложения Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Хранилище контактов"</string>
     <string name="provider_label" msgid="6012150850819899907">"Контакты"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Обновление базы данных контактов..."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Необходимо больше памяти для обновления контактов"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Обновление памяти контактов"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Нажмите, чтобы завершить обновление."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Обновление базы данных контактов..."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Для обновления контактов нужно больше памяти."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Недостаточно места для обновления контактов"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Нажмите, чтобы завершить обновление."</string>
     <string name="default_directory" msgid="93961630309570294">"Контакты"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Другое"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Доступ к голосовым сообщениям"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Разрешить приложению сохранять и извлекать все голосовые сообщения, к которым есть доступ на этом устройстве."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Приложение сможет сохранять и загружать все голосовые сообщения, к которым есть доступ на этом устройстве."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Голосовое сообщение от абонента "</string>
 </resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index ec716f6..e8d82c6 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"Úložisko kontaktov"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontakty"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Prebieha inovácia databázy kontaktov."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Inovácia kontaktu vyžaduje viac pamäte"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Prebieha inovácia ukladacieho priestoru kontaktov"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Výberom tejto možnosti dokončíte inováciu"</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Prebieha inovácia databázy kontaktov."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Inovácia kontaktov vyžaduje viac pamäte."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Prebieha inovácia úložiska pre kontakty"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dotykom na túto možnosť dokončíte inováciu."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontakty"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Iné"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Prístup ku všetkým hlasovým správam"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Umožňuje aplikácii uchovávať a načítavať všetky hlasové správy, ku ktorým má zariadenie prístup."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Umožňuje aplikácii uchovávať a načítavať všetky hlasové správy, ku ktorým má zariadenie prístup."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Hlasová správa od "</string>
 </resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index ec1fe50..af43531 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Osrednji programi sistema Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Shramba za stike"</string>
     <string name="provider_label" msgid="6012150850819899907">"Stiki"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Nadgradnja zbirke podatkov stikov."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Za nadgradnjo stika je potrebnega več pomnilnika"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Nadgradnja shrambe za stike"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Izberite, da dokončate nadgradnjo."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Nadgradnja zbirke podatkov stikov."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Za nadgradnjo stikov je potrebno več pomnilnika"</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Nadgradnja shrambe za stike"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Dotaknite se, da končate nadgradnjo."</string>
     <string name="default_directory" msgid="93961630309570294">"Stiki"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Drugo"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Dostop do glasovne pošte"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Programu dovoli shranjevanje in prenos sporočil glasovne pošte, do katerih lahko dostopa ta naprava."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Programu dovoli shranjevanje in prenos sporočil v odzivniku, do katerih lahko dostopa ta naprava."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Govorna pošta s številke "</string>
 </resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index fca0af9..c8888db 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core апликације"</string>
     <string name="app_label" msgid="3389954322874982620">"Складиште контаката"</string>
     <string name="provider_label" msgid="6012150850819899907">"Контакти"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Надограђивање базе података о контактима."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"За ажурирање контаката потребно је више меморије"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Ажурирање складишта контаката"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Изаберите опцију за довршетак надоградње."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Надограђивање базе података о контактима."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"За ажурирање контаката потребно је више меморије."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Надограђивање меморије за контакте"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Додирните да бисте довршили надоградњу."</string>
     <string name="default_directory" msgid="93961630309570294">"Контакти"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Други"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Приступ свим порукама говорне поште"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Омогућава апликацији да складишти и преузима све поруке говорне поште којима овај уређај може да приступи."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Дозвољава апликацији да складишти и преузима све поруке говорне поште којима овај уређај може да приступи."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Говорна пошта од "</string>
 </resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 897fbff..06ecf9a 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"Kontakter"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kontakter"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Uppgraderar kontaktdatabas."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Kontaktuppgradering kräver mer minne"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Uppgradera kontaktminne"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Välj för att slutföra uppgraderingen."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Kontaktdatabasen uppgraderas."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kontaktuppgradering kräver mer minne."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Lagringsutrymmet för kontakter uppgraderas"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Tryck om du vill slutföra uppgraderingen."</string>
     <string name="default_directory" msgid="93961630309570294">"Kontakter"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Övrigt"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Åtkomst till alla röstmeddelanden"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Tillåter att programmet sparar och hämtar alla röstmeddelanden som enheten har åtkomst till."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Tillåter att appen sparar och hämtar alla röstmeddelanden som enheten har åtkomst till."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Röstmeddelande från "</string>
 </resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 1c8ebd1..b83c636 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Programu Msingi za Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Hifadhi ya Anwani"</string>
     <string name="provider_label" msgid="6012150850819899907">"Anwani"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Inapandisha daraja la hifadhidata ya Anwani."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Kupandisha gredi kwa anwani kunahitaji kumbukumbu zaidi"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Upandishaji gredi wa hifadhi ya anwani"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Chagua ili kukamilisha kupandisha daraja."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Inapandisha gredi hifadhidata ya Anwani."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kupandishwa gredi kwa anwani kunahitaji kumbukumbu zaidi."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Inapandisha gredi hifadhi ya anwani"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Gusa ili kukamilisha kupandisha gredi."</string>
     <string name="default_directory" msgid="93961630309570294">"Anwani"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Nyingineyo"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Fikia barua zote za sauti"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Huruhusu programu kuhifadhi na kuepua barua zote za sauti ambazo kifaa hiki kinaweza kufikia."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Inaruhusu programu kuhifadhi na kutoa jumbe zote za sauti ambazo kifaa hiki kinaweza kufikia."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Barua ya sauti kutoka "</string>
 </resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index d9b996c..0cb6adc 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"แอปพลิเคชันหลักของ Android"</string>
     <string name="app_label" msgid="3389954322874982620">"ที่จัดเก็บรายชื่อ"</string>
     <string name="provider_label" msgid="6012150850819899907">"สมุดโทรศัพท์"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"กำลังอัปเกรดฐานข้อมูลรายชื่อติดต่อ"</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"ต้องใช้หน่วยความจำเพิ่มเพื่ออัปเกรดสมุดโทรศัพท์"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"กำลังอัปเกรดที่เก็บรายชื่อในสมุดโทรศัพท์"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"เลือกเพื่อทำการอัปเกรดให้สมบูรณ์"</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"กำลังอัปเกรดฐานข้อมูลสมุดโทรศัพท์"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"ต้องใช้หน่วยความจำเพิ่มเพื่ออัปเกรดสมุดโทรศัพท์"</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"กำลังอัปเกรดที่จัดเก็บข้อมูลสำหรับสมุดโทรศัพท์"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"แตะเพื่อทำการอัปเกรดให้สมบูรณ์"</string>
     <string name="default_directory" msgid="93961630309570294">"สมุดโทรศัพท์"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"อื่นๆ"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"เข้าถึงข้อความเสียงทั้งหมด"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"อนุญาตให้แอปพลิเคชันจัดเก็บและเรียกข้อความเสียงทั้งหมดที่อุปกรณ์นี้สามารถเข้าถึงได้"</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"อนุญาตให้แอปพลิเคชันจัดเก็บและเรียกข้อความเสียงทั้งหมดที่อุปกรณ์นี้สามารถเข้าถึงได้"</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"ข้อความเสียงจาก "</string>
 </resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index eb88576..44f6b73 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Core Apps ng Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Imbakan ng Mga Contact"</string>
     <string name="provider_label" msgid="6012150850819899907">"Mga Contact"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Ina-upgrade ang database ng Mga Contact."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Nangangailangan ang pag-upgrade sa contact nang higit pang memory"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Pag-upgrade ng imbakan ng contact"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Piliing kumpletuhin ang pag-upgrade."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Ina-upgrade ang database ng mga contact."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Nangangailangan ng higit pang memory ang pag-upgrade sa mga contact."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Ina-upgrade ang storage para sa mga contact"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Pindutin upang kumpletuhin ang pag-upgrade."</string>
     <string name="default_directory" msgid="93961630309570294">"Mga Contact"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Iba pa"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"I-access ang lahat ng voicemail"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Binibigyang-daan ang application upang maimbak at mabawi ang lahat ng voicemail na naa-access ng device na ito."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Binibigyang-daan ang app upang iimbak at kuning muli ang lahat ng voicemail na naa-access ng device na ito."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Voicemail mula sa/kay "</string>
 </resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 3cc061a..672288a 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Çekirdek Uygulamaları"</string>
     <string name="app_label" msgid="3389954322874982620">"Kişi Deposu"</string>
     <string name="provider_label" msgid="6012150850819899907">"Kişiler"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Kişiler veritabanı yeni sürüme geçiriliyor."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Kişiyi yeni sürüme geçirmek için daha fazla bellek gerekiyor"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Kişi deposu yükseltiliyor"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Yeni sürüme geçmeyi tamamlamak için seçin."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Kişiler veritabanı yeni sürüme geçiriliyor."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Kişileri yeni sürüme geçirmek için daha fazla bellek gerekiyor."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Kişiler için depolama alanı yeni sürüme geçiriliyor"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Yeni sürüme geçmeyi tamamlamak için dokunun."</string>
     <string name="default_directory" msgid="93961630309570294">"Kişiler"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Diğer"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Tüm sesli mesajlara erişim"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Uygulamaya, bu cihazın erişebileceği tüm sesli mesajları depolama ve alma izni verir."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Uygulamaya, bu cihazın erişebileceği tüm sesli mesajları depolama ve alma izni verir."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Sesli mesaj gönderen: "</string>
 </resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 28e6148..1829219 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android Core Apps"</string>
     <string name="app_label" msgid="3389954322874982620">"Пам\'ять контактів"</string>
     <string name="provider_label" msgid="6012150850819899907">"Контакти"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Оновлення бази даних контактів."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Оновл. контакту потреб. більше пам\'яті"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Оновлення пам\'яті контактів"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Виберіть для заверш. оновлення."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Оновлення бази даних контактів."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Оновлення контактів потребує більше пам’яті."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Оновлення пам’яті для контактів"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Торкніться, щоб завершити оновлення."</string>
     <string name="default_directory" msgid="93961630309570294">"Контакти"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Інші"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Доступ до всієї голосової пошти"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Дозволяє програмі зберігати й отримувати всю голосову пошту, доступ до якої має цей пристрій."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Дозволяє програмі зберігати й отримувати всю голосову пошту, доступ до якої має цей пристрій."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Голосова пошта від "</string>
 </resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index d793fa9..431a403 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Ứng dụng lõi Android"</string>
     <string name="app_label" msgid="3389954322874982620">"Bộ nhớ Danh bạ"</string>
     <string name="provider_label" msgid="6012150850819899907">"Danh bạ"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Đang nâng cấp cơ sở dữ liệu Danh bạ."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Nâng cấp liên hệ cần thêm bộ nhớ"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Nâng cấp bộ nhớ liên hệ"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Chọn để hoàn tất nâng cấp."</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Đang nâng cấp cơ sở dữ liệu danh bạ."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Nâng cấp danh bạ cần thêm bộ nhớ."</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Đang nâng cấp dung lượng cho danh bạ"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Chạm để hoàn tất nâng cấp."</string>
     <string name="default_directory" msgid="93961630309570294">"Danh bạ"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Khác"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Truy cập tất cả các thư thoại"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Cho phép ứng dụng lưu trữ và truy xuất tất cả thư thoại mà thiết bị này có thể truy cập."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Cho phép ứng dụng lưu trữ và truy xuất tất cả thư thoại mà thiết bị này có thể truy cập."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Thư thoại từ "</string>
 </resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 554e401..5dd88e4 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android 核心应用程序"</string>
     <string name="app_label" msgid="3389954322874982620">"联系人存储"</string>
     <string name="provider_label" msgid="6012150850819899907">"联系人"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"正在升级联系人数据库。"</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"联系人升级需要更多的存储空间"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"升级联系人时存储空间不足"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"请选择以完成升级。"</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"正在升级联系人数据库。"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"联系人升级需要更多的存储空间。"</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"正在升级存储器以容纳更多联系人"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"请触摸以完成升级。"</string>
     <string name="default_directory" msgid="93961630309570294">"联系人"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"其他"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"访问所有语音邮件"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"允许应用程序存储和检索此设备可访问的所有语音邮件。"</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"允许应用程序存储和检索此设备可访问的所有语音邮件。"</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"语音邮件发件人 "</string>
 </resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 2d8761d..ba929c3 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Android 核心應用程式"</string>
     <string name="app_label" msgid="3389954322874982620">"聯絡人儲存空間"</string>
     <string name="provider_label" msgid="6012150850819899907">"聯絡人"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"正在升級聯絡人資料庫。"</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"需要更多記憶體才能將聯絡人升級"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"升級聯絡人儲存空間"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"選取以完成升級。"</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"正在升級聯絡人資料庫。"</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"需要更多記憶體才能將聯絡人升級。"</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"正在升級聯絡人儲存空間"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"輕觸即可完成升級。"</string>
     <string name="default_directory" msgid="93961630309570294">"聯絡人"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"其他"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"存取所有語音留言"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"允許應用程式儲存和擷取這個裝置可存取的所有語音留言。"</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"允許應用程式儲存及擷取這個裝置可存取的所有語音留言。"</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"語音郵件寄件者: "</string>
 </resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index b170638..76de636 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -19,13 +19,13 @@
     <string name="sharedUserLabel" msgid="8024311725474286801">"Izinhlelo Zokusebenza ze-Android Core"</string>
     <string name="app_label" msgid="3389954322874982620">"Isitoreji Sothintana Nabo"</string>
     <string name="provider_label" msgid="6012150850819899907">"Othintana nabo"</string>
-    <string name="upgrade_msg" msgid="6174884195179549239">"Ukufaka ezakamuva kwimininingo egciniwe yothintana naye."</string>
-    <string name="upgrade_out_of_memory_notification_ticker" msgid="4089605622758004662">"Ukuthuthukisa othintana naye kudinga enye imemori"</string>
-    <string name="upgrade_out_of_memory_notification_title" msgid="7849508493764133004">"Ithuthukisa isitoreji sothintana naye"</string>
-    <string name="upgrade_out_of_memory_notification_text" msgid="3967762223137708403">"Khetha ukuqedela ukuthuthukisa"</string>
+    <string name="upgrade_msg" msgid="8640807392794309950">"Ukufaka ezakamuva kwimininingo egciniwe yothintana naye."</string>
+    <string name="upgrade_out_of_memory_notification_ticker" msgid="7638747231223520477">"Ukuthuthukisa othintana naye kudinga enye imemori"</string>
+    <string name="upgrade_out_of_memory_notification_title" msgid="8888171924684998531">"Ukuthuthukiswa kwesilondolozi soxhumana nabo"</string>
+    <string name="upgrade_out_of_memory_notification_text" msgid="8438179450336437626">"Thinta ukuqedela ukuthuthukisa."</string>
     <string name="default_directory" msgid="93961630309570294">"Othintana nabo"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Okunye"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Finyelela kuwo wonke amavoyisimeyili"</string>
-    <string name="read_write_all_voicemail_description" msgid="2249895806470926882">"Ivumela uhlelo olusebenzayo lulonde futhi lulande wonke ama-imeyli ezwi ledivayisi engakwazi ukuwafinyelela."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Ivumela uhlelo olusebenzayo lulonde futhi lulande wonke ama-imeyli ezwi ledivayisi engakwazi ukuwafinyelela."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Imeyili yezwi kusuka "</string>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index bc14294..aaa7f44 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -27,16 +27,16 @@
     <string name="provider_label">Contacts</string>
 
     <!-- [CHAR LIMIT=NONE] Boot message while upgrading contacts. -->
-    <string name="upgrade_msg">Upgrading Contacts database.</string>
+    <string name="upgrade_msg">Upgrading contacts database.</string>
 
     <!-- Ticker for the notification shown when updating contacts fails because of memory shortage -->
-    <string name="upgrade_out_of_memory_notification_ticker">Contact upgrade needs more memory</string>
+    <string name="upgrade_out_of_memory_notification_ticker">Contacts upgrade needs more memory.</string>
 
     <!-- Title for the notification shown when updating contacts fails because of memory shortage -->
-    <string name="upgrade_out_of_memory_notification_title">Upgrading contact storage</string>
+    <string name="upgrade_out_of_memory_notification_title">Upgrading storage for contacts</string>
 
     <!-- Text for the notification shown when updating contacts fails because of memory shortage -->
-    <string name="upgrade_out_of_memory_notification_text">Select to complete the upgrade.</string>
+    <string name="upgrade_out_of_memory_notification_text">Touch to complete the upgrade.</string>
 
     <!-- The name of the default contact directory -->
     <string name="default_directory">Contacts</string>
@@ -49,7 +49,7 @@
     <string name="read_write_all_voicemail_label">Access all voicemails</string>
     <!-- Description of an application permission, listed so the user can choose whether
          they want to allow the application to do this. [CHAR LIMIT=NONE] -->
-    <string name="read_write_all_voicemail_description">Allows the application to store and retrieve
+    <string name="read_write_all_voicemail_description">Allows the app to store and retrieve
         all voicemails that this device can access.</string>
 
     <!-- The prefix string before the number used for the display name for the voicemail table.
diff --git a/src/com/android/providers/contacts/AbstractContactsProvider.java b/src/com/android/providers/contacts/AbstractContactsProvider.java
index 0d90f18..a33320f 100644
--- a/src/com/android/providers/contacts/AbstractContactsProvider.java
+++ b/src/com/android/providers/contacts/AbstractContactsProvider.java
@@ -25,6 +25,7 @@
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteTransactionListener;
 import android.net.Uri;
+import android.util.Log;
 
 import java.util.ArrayList;
 
@@ -37,6 +38,10 @@
 public abstract class AbstractContactsProvider extends ContentProvider
         implements SQLiteTransactionListener {
 
+    protected static final String TAG = "ContactsProvider";
+
+    protected static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
+
     /**
      * Duration in ms to sleep after successfully yielding the lock during a batch operation.
      */
@@ -174,6 +179,9 @@
     @Override
     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
             throws OperationApplicationException {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "applyBatch: " + operations.size() + " ops");
+        }
         int ypCount = 0;
         int opCount = 0;
         ContactsTransaction transaction = startTransaction(true);
@@ -189,6 +197,9 @@
                 }
                 final ContentProviderOperation operation = operations.get(i);
                 if (i > 0 && operation.isYieldAllowed()) {
+                    if (VERBOSE_LOGGING) {
+                        Log.v(TAG, "applyBatch: " + opCount + " ops finished; about to yield...");
+                    }
                     opCount = 0;
                     try {
                         if (yield(transaction)) {
diff --git a/src/com/android/providers/contacts/AccountWithDataSet.java b/src/com/android/providers/contacts/AccountWithDataSet.java
index d667432..3fea8a6 100644
--- a/src/com/android/providers/contacts/AccountWithDataSet.java
+++ b/src/com/android/providers/contacts/AccountWithDataSet.java
@@ -18,18 +18,35 @@
 
 import com.android.internal.util.Objects;
 
+import android.accounts.Account;
+import android.text.TextUtils;
+
 /**
  * Account information that includes the data set, if any.
  */
 public class AccountWithDataSet {
+    public static final AccountWithDataSet LOCAL = new AccountWithDataSet(null, null, null);
+
     private final String mAccountName;
     private final String mAccountType;
     private final String mDataSet;
 
     public AccountWithDataSet(String accountName, String accountType, String dataSet) {
-        mAccountName = accountName;
-        mAccountType = accountType;
-        mDataSet = dataSet;
+        mAccountName = emptyToNull(accountName);
+        mAccountType = emptyToNull(accountType);
+        mDataSet = emptyToNull(dataSet);
+    }
+
+    private static final String emptyToNull(String text) {
+        return TextUtils.isEmpty(text) ? null : text;
+    }
+
+    public static AccountWithDataSet get(String accountName, String accountType, String dataSet) {
+        return new AccountWithDataSet(accountName, accountType, dataSet);
+    }
+
+    public static AccountWithDataSet get(Account account, String dataSet) {
+        return new AccountWithDataSet(account.name, account.type, null);
     }
 
     public String getAccountName() {
@@ -44,6 +61,10 @@
         return mDataSet;
     }
 
+    public boolean isLocalAccount() {
+        return (mAccountName == null) && (mAccountType == null);
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (obj instanceof AccountWithDataSet) {
@@ -68,4 +89,20 @@
         return "AccountWithDataSet {name=" + mAccountName + ", type=" + mAccountType + ", dataSet="
                 + mDataSet + "}";
     }
+
+    /**
+     * @return {@code true} if the owning {@link Account} is in the passed array.
+     */
+    public boolean inSystemAccounts(Account[] systemAccounts) {
+        // Note we don't want to create a new Account object from this instance, as it may contain
+        // null account name/type, which Account wouldn't accept.  So we need to compare field by
+        // field.
+        for (Account systemAccount : systemAccounts) {
+            if (Objects.equal(systemAccount.name, getAccountName())
+                    && Objects.equal(systemAccount.type, getAccountType())) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index 3c39625..89ae591 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -46,7 +46,7 @@
 public class CallLogProvider extends ContentProvider {
     /** Selection clause to use to exclude voicemail records.  */
     private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause(
-            Calls.TYPE, Integer.toString(Calls.VOICEMAIL_TYPE));
+            Calls.TYPE, Calls.VOICEMAIL_TYPE);
 
     private static final int CALLS = 1;
 
@@ -320,10 +320,9 @@
     * matches CALLS_ID. For other uri types the behaviour is undefined.
     * @throws IllegalArgumentException if the id included in the Uri is not a valid long value.
     */
-    private String parseCallIdFromUri(Uri uri) {
+    private long parseCallIdFromUri(Uri uri) {
         try {
-            Long id = Long.valueOf(uri.getPathSegments().get(1));
-            return id.toString();
+            return Long.parseLong(uri.getPathSegments().get(1));
         } catch (NumberFormatException e) {
             throw new IllegalArgumentException("Invalid call id in uri: " + uri, e);
         }
diff --git a/src/com/android/providers/contacts/CommonNicknameCache.java b/src/com/android/providers/contacts/CommonNicknameCache.java
index 0276743..e1dfae3 100644
--- a/src/com/android/providers/contacts/CommonNicknameCache.java
+++ b/src/com/android/providers/contacts/CommonNicknameCache.java
@@ -87,7 +87,7 @@
     /**
      * Returns nickname cluster IDs or null. Maintains cache.
      */
-    protected String[] getCommonNicknameClusters(String normalizedName) {
+    public String[] getCommonNicknameClusters(String normalizedName) {
         if (mNicknameBloomFilter == null) {
             preloadNicknameBloomFilter();
         }
diff --git a/src/com/android/providers/contacts/ContactDirectoryManager.java b/src/com/android/providers/contacts/ContactDirectoryManager.java
index 1473679..7116ed6 100644
--- a/src/com/android/providers/contacts/ContactDirectoryManager.java
+++ b/src/com/android/providers/contacts/ContactDirectoryManager.java
@@ -16,6 +16,7 @@
 
 package com.android.providers.contacts;
 
+import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
 import com.android.providers.contacts.ContactsDatabaseHelper.DirectoryColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 import com.google.android.collect.Lists;
@@ -47,16 +48,14 @@
 /**
  * Manages the contents of the {@link Directory} table.
  */
-// TODO: Determine whether directories need to be aware of data sets under the account.
 public class ContactDirectoryManager {
 
     private static final String TAG = "ContactDirectoryManager";
     private static final boolean DEBUG = false; // DON'T SUBMIT WITH TRUE
 
-    public static final String PROPERTY_DIRECTORY_SCAN_COMPLETE = "directoryScanComplete";
     public static final String CONTACT_DIRECTORY_META_DATA = "android.content.ContactDirectory";
 
-    public class DirectoryInfo {
+    public static class DirectoryInfo {
         long id;
         String packageName;
         String authority;
@@ -176,22 +175,22 @@
      */
     public void scanAllPackages(boolean rescan) {
         if (rescan || !areTypeResourceIdsValid()) {
-            getDbHelper().setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
+            getDbHelper().setProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
         }
 
         scanAllPackagesIfNeeded();
     }
 
     private void scanAllPackagesIfNeeded() {
-        String scanComplete = getDbHelper().getProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
+        String scanComplete = getDbHelper().getProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
         if (!"0".equals(scanComplete)) {
             return;
         }
 
-        long start = SystemClock.currentThreadTimeMillis();
+        final long start = SystemClock.elapsedRealtime();
         int count = scanAllPackages();
-        getDbHelper().setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "1");
-        long end = SystemClock.currentThreadTimeMillis();
+        getDbHelper().setProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "1");
+        final long end = SystemClock.elapsedRealtime();
         Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms");
 
         // Announce the change to listeners of the contacts authority
@@ -257,6 +256,13 @@
         for (String packageName : getDirectoryProviderPackages(mPackageManager)) {
             if (DEBUG) Log.d(TAG, "package=" + packageName);
 
+            // getDirectoryProviderPackages() shouldn't return the contacts provider package
+            // because it doesn't have CONTACT_DIRECTORY_META_DATA, but just to make sure...
+            if (mContext.getPackageName().equals(packageName)) {
+                Log.w(TAG, "  skipping self");
+                continue;
+            }
+
             final PackageInfo packageInfo;
             try {
                 packageInfo = mPackageManager.getPackageInfo(packageName,
@@ -337,6 +343,10 @@
             packageInfo.packageName = packageName;
         }
 
+        if (mContext.getPackageName().equals(packageInfo.packageName)) {
+            if (DEBUG) Log.d(TAG, "Ignoring onPackageChanged for self");
+            return;
+        }
         updateDirectoriesForPackage(packageInfo, false);
     }
 
@@ -347,6 +357,11 @@
      */
     private List<DirectoryInfo> updateDirectoriesForPackage(
             PackageInfo packageInfo, boolean initialScan) {
+        if (DEBUG) {
+            Log.d(TAG, "updateDirectoriesForPackage  packageName=" + packageInfo.packageName
+                    + " initialScan=" + initialScan);
+        }
+
         ArrayList<DirectoryInfo> directories = Lists.newArrayList();
 
         ProviderInfo[] providers = packageInfo.providers;
@@ -376,7 +391,11 @@
                 sb.setLength(sb.length() - 1);  // Remove the extra comma
                 sb.append(")");
             }
-            db.delete(Tables.DIRECTORIES, sb.toString(), new String[] { packageInfo.packageName });
+            final int numDeleted = db.delete(Tables.DIRECTORIES, sb.toString(),
+                    new String[] { packageInfo.packageName });
+            if (DEBUG) {
+                Log.d(TAG, "  deleted " + numDeleted + " stale rows");
+            }
             db.setTransactionSuccessful();
         } finally {
             db.endTransaction();
diff --git a/src/com/android/providers/contacts/ContactLocaleUtils.java b/src/com/android/providers/contacts/ContactLocaleUtils.java
index 0fc8231..2eb2ad6 100644
--- a/src/com/android/providers/contacts/ContactLocaleUtils.java
+++ b/src/com/android/providers/contacts/ContactLocaleUtils.java
@@ -19,9 +19,9 @@
 import com.android.providers.contacts.HanziToPinyin.Token;
 
 import android.provider.ContactsContract.FullNameStyle;
+import android.util.SparseArray;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Locale;
@@ -40,6 +40,7 @@
         public String getSortKey(String displayName) {
             return displayName;
         }
+        @SuppressWarnings("unused")
         public Iterator<String> getNameLookupKeys(String name) {
             return null;
         }
@@ -128,10 +129,10 @@
     private static final String KOREAN_LANGUAGE = Locale.KOREAN.getLanguage().toLowerCase();
 
     private static ContactLocaleUtils sSingleton;
-    private HashMap<Integer, ContactLocaleUtilsBase> mUtils =
-            new HashMap<Integer, ContactLocaleUtilsBase>();
+    private final SparseArray<ContactLocaleUtilsBase> mUtils =
+            new SparseArray<ContactLocaleUtilsBase>();
 
-    private ContactLocaleUtilsBase mBase = new ContactLocaleUtilsBase();
+    private final ContactLocaleUtilsBase mBase = new ContactLocaleUtilsBase();
 
     private String mLanguage;
 
@@ -180,7 +181,7 @@
                 mUtils.put(nameStyle, utils);
             }
         }
-        return (utils == null) ? mBase: utils;
+        return (utils == null) ? mBase : utils;
     }
 
     /**
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index f5f1a6a..32928ba 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -17,6 +17,9 @@
 package com.android.providers.contacts;
 
 import com.android.common.content.SyncStateContentProviderHelper;
+import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
+import com.android.providers.contacts.util.NeededForTesting;
+import com.google.android.collect.Sets;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -80,13 +83,14 @@
 
 import java.util.HashMap;
 import java.util.Locale;
+import java.util.Set;
 
 /**
  * Database helper for contacts. Designed as a singleton to make sure that all
  * {@link android.content.ContentProvider} users get the same reference.
  * Provides handy methods for maintaining package and mime-type lookup tables.
  */
-/* package */ class ContactsDatabaseHelper extends SQLiteOpenHelper {
+public class ContactsDatabaseHelper extends SQLiteOpenHelper {
     private static final String TAG = "ContactsDatabaseHelper";
 
     /**
@@ -101,9 +105,10 @@
      *   500-549 Honeycomb-MR1
      *   550-599 Honeycomb-MR2
      *   600-699 Ice Cream Sandwich
+     *   700-799 Jelly Bean
      * </pre>
      */
-    static final int DATABASE_VERSION = 625;
+    static final int DATABASE_VERSION = 700;
 
     private static final String DATABASE_NAME = "contacts2.db";
     private static final String DATABASE_PRESENCE = "presence_db";
@@ -160,27 +165,47 @@
         public static final String DATA_JOIN_RAW_CONTACTS = "data "
                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
 
+        // NOTE: If you want to refer to account name/type/data_set, AccountsColumns.CONCRETE_XXX
+        // MUST be used, as upgraded raw_contacts may have the account info columns too.
         public static final String DATA_JOIN_MIMETYPE_RAW_CONTACTS = "data "
                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
-                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
+                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)"
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                    + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
+                    + ")";
 
         // NOTE: This requires late binding of GroupMembership MIME-type
-        public static final String RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS = "raw_contacts "
-                + "LEFT OUTER JOIN settings ON ("
-                    + "raw_contacts.account_name = settings.account_name AND "
-                    + "raw_contacts.account_type = settings.account_type AND "
-                    + "((raw_contacts.data_set IS NULL AND settings.data_set IS NULL) "
-                    + "OR (raw_contacts.data_set = settings.data_set))) "
+        // TODO Consolidate settings and accounts
+        public static final String RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS = Tables.RAW_CONTACTS
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
+                    + ")"
+                + "LEFT OUTER JOIN " + Tables.SETTINGS + " ON ("
+                    + AccountsColumns.CONCRETE_ACCOUNT_NAME + "="
+                        + SettingsColumns.CONCRETE_ACCOUNT_NAME + " AND "
+                    + AccountsColumns.CONCRETE_ACCOUNT_TYPE + "="
+                        + SettingsColumns.CONCRETE_ACCOUNT_TYPE + " AND "
+                    + "((" + AccountsColumns.CONCRETE_DATA_SET + " IS NULL AND "
+                            + SettingsColumns.CONCRETE_DATA_SET + " IS NULL) OR ("
+                        + AccountsColumns.CONCRETE_DATA_SET + "="
+                            + SettingsColumns.CONCRETE_DATA_SET + "))) "
                 + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND "
                     + "data.raw_contact_id = raw_contacts._id) "
                 + "LEFT OUTER JOIN groups ON (groups._id = data." + GroupMembership.GROUP_ROW_ID
                 + ")";
 
         // NOTE: This requires late binding of GroupMembership MIME-type
+        // TODO Add missing DATA_SET join -- or just consolidate settings and accounts
         public static final String SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS = "settings "
                 + "LEFT OUTER JOIN raw_contacts ON ("
-                    + "raw_contacts.account_name = settings.account_name AND "
-                    + "raw_contacts.account_type = settings.account_type) "
+                    + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=(SELECT "
+                        + AccountsColumns.CONCRETE_ID
+                        + " FROM " + Tables.ACCOUNTS
+                        + " WHERE "
+                            + "(" + AccountsColumns.CONCRETE_ACCOUNT_NAME
+                                + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME + ") AND "
+                            + "(" + AccountsColumns.CONCRETE_ACCOUNT_TYPE
+                                + "=" + SettingsColumns.CONCRETE_ACCOUNT_TYPE + ")))"
                 + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND "
                     + "data.raw_contact_id = raw_contacts._id) "
                 + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
@@ -205,18 +230,19 @@
                             + ")"
                         + ")";
 
+        // NOTE: If you want to refer to account name/type/data_set, AccountsColumns.CONCRETE_XXX
+        // MUST be used, as upgraded raw_contacts may have the account info columns too.
         public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS = "data "
                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                    + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
+                    + ")"
                 + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
                 + "LEFT OUTER JOIN groups "
                 + "  ON (mimetypes.mimetype='" + GroupMembership.CONTENT_ITEM_TYPE + "' "
                 + "      AND groups._id = data." + GroupMembership.GROUP_ROW_ID + ") ";
 
-        public static final String GROUPS_JOIN_PACKAGES = "groups "
-                + "LEFT OUTER JOIN packages ON (groups.package_id = packages._id)";
-
-
         public static final String ACTIVITIES = "activities";
 
         public static final String ACTIVITIES_JOIN_MIMETYPES = "activities "
@@ -233,6 +259,11 @@
         public static final String NAME_LOOKUP_JOIN_RAW_CONTACTS = "name_lookup "
                 + "INNER JOIN view_raw_contacts ON (name_lookup.raw_contact_id = "
                 + "view_raw_contacts._id)";
+
+        public static final String RAW_CONTACTS_JOIN_ACCOUNTS = Tables.RAW_CONTACTS
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
+                + ")";
     }
 
     public interface Joins {
@@ -272,9 +303,16 @@
         final String GROUP_BY_ACCOUNT_CONTACT_ID = SettingsColumns.CONCRETE_ACCOUNT_NAME + ","
                 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "," + RawContacts.CONTACT_ID;
 
-        final String RAW_CONTACT_IS_LOCAL = RawContactsColumns.CONCRETE_ACCOUNT_NAME
-                + " IS NULL AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL AND "
-                + RawContactsColumns.CONCRETE_DATA_SET + " IS NULL";
+        String LOCAL_ACCOUNT_ID =
+                "(SELECT " + AccountsColumns._ID +
+                " FROM " + Tables.ACCOUNTS +
+                " WHERE " +
+                    AccountsColumns.ACCOUNT_NAME + " IS NULL AND " +
+                    AccountsColumns.ACCOUNT_TYPE + " IS NULL AND " +
+                    AccountsColumns.DATA_SET + " IS NULL)";
+
+        final String RAW_CONTACT_IS_LOCAL = RawContactsColumns.CONCRETE_ACCOUNT_ID
+                + "=" + LOCAL_ACCOUNT_ID;
 
         final String ZERO_GROUP_MEMBERSHIPS = "COUNT(" + GroupsColumns.CONCRETE_ID + ")=0";
 
@@ -298,12 +336,7 @@
                 " GROUP BY " + RawContacts.CONTACT_ID;
 
         final String GROUP_HAS_ACCOUNT_AND_SOURCE_ID = Groups.SOURCE_ID + "=? AND "
-                + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=? AND "
-                + Groups.DATA_SET + " IS NULL";
-
-        final String GROUP_HAS_ACCOUNT_AND_DATA_SET_AND_SOURCE_ID = Groups.SOURCE_ID + "=? AND "
-                + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=? AND "
-                + Groups.DATA_SET + "=?";
+                + GroupsColumns.ACCOUNT_ID + "=?";
 
         public static final String CONTACT_VISIBLE =
             "EXISTS (SELECT _id FROM " + Tables.VISIBLE_CONTACTS
@@ -334,14 +367,9 @@
     public interface RawContactsColumns {
         public static final String CONCRETE_ID =
                 Tables.RAW_CONTACTS + "." + BaseColumns._ID;
-        public static final String CONCRETE_ACCOUNT_NAME =
-                Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_NAME;
-        public static final String CONCRETE_ACCOUNT_TYPE =
-                Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_TYPE;
-        public static final String CONCRETE_DATA_SET =
-                Tables.RAW_CONTACTS + "." + RawContacts.DATA_SET;
-        public static final String CONCRETE_ACCOUNT_TYPE_AND_DATA_SET =
-                Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_TYPE_AND_DATA_SET;
+
+        public static final String ACCOUNT_ID = "account_id";
+        public static final String CONCRETE_ACCOUNT_ID = Tables.RAW_CONTACTS + "." + ACCOUNT_ID;
         public static final String CONCRETE_SOURCE_ID =
                 Tables.RAW_CONTACTS + "." + RawContacts.SOURCE_ID;
         public static final String CONCRETE_VERSION =
@@ -381,6 +409,12 @@
                 Tables.RAW_CONTACTS + "." + RawContacts.NAME_VERIFIED;
     }
 
+    public interface ViewRawContactsColumns {
+        String CONCRETE_ACCOUNT_NAME = Views.RAW_CONTACTS + "." + RawContacts.ACCOUNT_NAME;
+        String CONCRETE_ACCOUNT_TYPE = Views.RAW_CONTACTS + "." + RawContacts.ACCOUNT_TYPE;
+        String CONCRETE_DATA_SET = Views.RAW_CONTACTS + "." + RawContacts.DATA_SET;
+    }
+
     public interface DataColumns {
         public static final String PACKAGE_ID = "package_id";
         public static final String MIMETYPE_ID = "mimetype_id";
@@ -422,23 +456,21 @@
         public static final String GROUP_ROW_ID = GroupMembership.GROUP_ROW_ID;
     }
 
-    public interface PhoneColumns {
-        public static final String NORMALIZED_NUMBER = Data.DATA4;
-        public static final String CONCRETE_NORMALIZED_NUMBER = DataColumns.CONCRETE_DATA4;
-    }
-
     public interface GroupsColumns {
         public static final String PACKAGE_ID = "package_id";
+        public static final String CONCRETE_PACKAGE_ID = Tables.GROUPS + "." + PACKAGE_ID;
 
         public static final String CONCRETE_ID = Tables.GROUPS + "." + BaseColumns._ID;
         public static final String CONCRETE_SOURCE_ID = Tables.GROUPS + "." + Groups.SOURCE_ID;
-        public static final String CONCRETE_ACCOUNT_NAME =
-                Tables.GROUPS + "." + Groups.ACCOUNT_NAME;
-        public static final String CONCRETE_ACCOUNT_TYPE =
-                Tables.GROUPS + "." + Groups.ACCOUNT_TYPE;
-        public static final String CONCRETE_DATA_SET = Tables.GROUPS + "." + Groups.DATA_SET;
-        public static final String CONCRETE_ACCOUNT_TYPE_AND_DATA_SET = Tables.GROUPS + "." +
-                Groups.ACCOUNT_TYPE_AND_DATA_SET;
+
+        public static final String ACCOUNT_ID = "account_id";
+        public static final String CONCRETE_ACCOUNT_ID = Tables.GROUPS + "." + ACCOUNT_ID;
+    }
+
+    public interface ViewGroupsColumns {
+        String CONCRETE_ACCOUNT_NAME = Views.GROUPS + "." + Groups.ACCOUNT_NAME;
+        String CONCRETE_ACCOUNT_TYPE = Views.GROUPS + "." + Groups.ACCOUNT_TYPE;
+        String CONCRETE_DATA_SET = Views.GROUPS + "." + Groups.DATA_SET;
     }
 
     public interface ActivitiesColumns {
@@ -592,10 +624,16 @@
         String PROPERTY_VALUE = "property_value";
     }
 
-    public interface AccountsColumns {
+    public interface AccountsColumns extends BaseColumns {
+        String CONCRETE_ID = Tables.ACCOUNTS + "." + BaseColumns._ID;
+
         String ACCOUNT_NAME = RawContacts.ACCOUNT_NAME;
         String ACCOUNT_TYPE = RawContacts.ACCOUNT_TYPE;
         String DATA_SET = RawContacts.DATA_SET;
+
+        String CONCRETE_ACCOUNT_NAME = Tables.ACCOUNTS + "." + ACCOUNT_NAME;
+        String CONCRETE_ACCOUNT_TYPE = Tables.ACCOUNTS + "." + ACCOUNT_TYPE;
+        String CONCRETE_DATA_SET = Tables.ACCOUNTS + "." + DATA_SET;
     }
 
     public static final class DirectoryColumns {
@@ -646,9 +684,26 @@
         public static final int USAGE_TYPE_INT_SHORT_TEXT = 2;
     }
 
+    public interface Projections {
+        String[] ID = new String[] {BaseColumns._ID};
+        String[] LITERAL_ONE = new String[] {"1"};
+    }
+
+    /**
+     * Property names for {@link ContactsDatabaseHelper#getProperty} and
+     * {@link ContactsDatabaseHelper#setProperty}.
+     */
+    public interface DbProperties {
+        String DIRECTORY_SCAN_COMPLETE = "directoryScanComplete";
+        String AGGREGATION_ALGORITHM = "aggregation_v2";
+        String KNOWN_ACCOUNTS = "known_accounts";
+    }
+
     /** In-memory cache of previously found MIME-type mappings */
+    // TODO Use ConcurrentHashMap?
     private final HashMap<String, Long> mMimetypeCache = new HashMap<String, Long>();
     /** In-memory cache of previously found package name mappings */
+    // TODO Use ConcurrentHashMap?
     private final HashMap<String, Long> mPackageCache = new HashMap<String, Long>();
 
     private long mMimeTypeIdEmail;
@@ -691,8 +746,6 @@
     private final CountryMonitor mCountryMonitor;
     private StringBuilder mSb = new StringBuilder();
 
-    private boolean mReopenDatabase = false;
-
     private static ContactsDatabaseHelper sSingleton = null;
 
     private boolean mUseStrictPhoneNumberComparison;
@@ -710,11 +763,11 @@
     }
 
     /**
-     * Private constructor, callers except unit tests should obtain an instance through
-     * {@link #getInstance(android.content.Context)} instead.
+     * Returns a new instance for unit tests.
      */
-    ContactsDatabaseHelper(Context context) {
-        this(context, null, false);
+    @NeededForTesting
+    static ContactsDatabaseHelper getNewInstanceForTest(Context context) {
+        return new ContactsDatabaseHelper(context, null, false);
     }
 
     protected ContactsDatabaseHelper(
@@ -731,6 +784,14 @@
                         com.android.internal.R.bool.config_use_strict_phone_number_comparation);
     }
 
+    /**
+     * Clear all the cached database information and re-initialize it.
+     *
+     * @param db target database
+     * @param accountTableHasId {@code true} if the "accounts" table exists and has the ID column.
+     *    This is normally {@code true}, but needs to be false during database upgrade until
+     *    step 626, where the account ID was introduced.
+     */
     private void refreshDatabaseCaches(SQLiteDatabase db) {
         mStatusUpdateDelete = null;
         mStatusUpdateReplace = null;
@@ -750,10 +811,15 @@
         mAggregationModeQuery = null;
         mContactInDefaultDirectoryQuery = null;
 
-        populateMimeTypeCache(db);
+        initializeCache(db);
     }
 
-    private void populateMimeTypeCache(SQLiteDatabase db) {
+    /**
+     * (Re-)initialize the cached database information.
+     *
+     * @param db target database
+     */
+    private void initializeCache(SQLiteDatabase db) {
         mMimetypeCache.clear();
         mPackageCache.clear();
 
@@ -864,6 +930,13 @@
 
         mSyncState.createDatabase(db);
 
+        db.execSQL("CREATE TABLE " + Tables.ACCOUNTS + " (" +
+                AccountsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                AccountsColumns.ACCOUNT_NAME + " TEXT, " +
+                AccountsColumns.ACCOUNT_TYPE + " TEXT, " +
+                AccountsColumns.DATA_SET + " TEXT" +
+        ");");
+
         // One row per group of contacts corresponding to the same person
         db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" +
                 BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -888,12 +961,11 @@
                 Contacts.NAME_RAW_CONTACT_ID +
         ");");
 
-        // Contacts table
+        // Raw_contacts table
         db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" +
                 RawContacts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
-                RawContacts.ACCOUNT_NAME + " STRING DEFAULT NULL, " +
-                RawContacts.ACCOUNT_TYPE + " STRING DEFAULT NULL, " +
-                RawContacts.DATA_SET + " STRING DEFAULT NULL, " +
+                RawContactsColumns.ACCOUNT_ID + " INTEGER REFERENCES " +
+                    Tables.ACCOUNTS + "(" + AccountsColumns._ID + ")," +
                 RawContacts.SOURCE_ID + " TEXT," +
                 RawContacts.RAW_CONTACT_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," +
@@ -929,19 +1001,11 @@
                 RawContacts.CONTACT_ID +
         ");");
 
-        db.execSQL("CREATE INDEX raw_contacts_source_id_index ON " + Tables.RAW_CONTACTS + " (" +
-                RawContacts.SOURCE_ID + ", " +
-                RawContacts.ACCOUNT_TYPE + ", " +
-                RawContacts.ACCOUNT_NAME +
-        ");");
-
-        db.execSQL("CREATE INDEX raw_contacts_source_id_data_set_index ON " +
+        db.execSQL("CREATE INDEX raw_contacts_source_id_account_id_index ON " +
                 Tables.RAW_CONTACTS + " (" +
-                    RawContacts.SOURCE_ID + ", " +
-                    RawContacts.ACCOUNT_TYPE + ", " +
-                    RawContacts.ACCOUNT_NAME + ", " +
-                    RawContacts.DATA_SET +
-                ");");
+                RawContacts.SOURCE_ID + ", " +
+                RawContactsColumns.ACCOUNT_ID +
+        ");");
 
         db.execSQL("CREATE TABLE " + Tables.STREAM_ITEMS + " (" +
                 StreamItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
@@ -1099,9 +1163,8 @@
         db.execSQL("CREATE TABLE " + Tables.GROUPS + " (" +
                 Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                 GroupsColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
-                Groups.ACCOUNT_NAME + " STRING DEFAULT NULL, " +
-                Groups.ACCOUNT_TYPE + " STRING DEFAULT NULL, " +
-                Groups.DATA_SET + " STRING DEFAULT NULL, " +
+                GroupsColumns.ACCOUNT_ID + " INTEGER REFERENCES " +
+                    Tables.ACCOUNTS + "(" + AccountsColumns._ID + ")," +
                 Groups.SOURCE_ID + " TEXT," +
                 Groups.VERSION + " INTEGER NOT NULL DEFAULT 1," +
                 Groups.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
@@ -1121,17 +1184,9 @@
                 Groups.SYNC4 + " TEXT " +
         ");");
 
-        db.execSQL("CREATE INDEX groups_source_id_index ON " + Tables.GROUPS + " (" +
+        db.execSQL("CREATE INDEX groups_source_id_account_id_index ON " + Tables.GROUPS + " (" +
                 Groups.SOURCE_ID + ", " +
-                Groups.ACCOUNT_TYPE + ", " +
-                Groups.ACCOUNT_NAME +
-        ");");
-
-        db.execSQL("CREATE INDEX groups_source_id_data_set_index ON " + Tables.GROUPS + " (" +
-                Groups.SOURCE_ID + ", " +
-                Groups.ACCOUNT_TYPE + ", " +
-                Groups.ACCOUNT_NAME + ", " +
-                Groups.DATA_SET +
+                GroupsColumns.ACCOUNT_ID +
         ");");
 
         db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.AGGREGATION_EXCEPTIONS + " (" +
@@ -1242,16 +1297,6 @@
                 PropertiesColumns.PROPERTY_VALUE + " TEXT " +
         ");");
 
-        db.execSQL("CREATE TABLE " + Tables.ACCOUNTS + " (" +
-                AccountsColumns.ACCOUNT_NAME + " TEXT, " +
-                AccountsColumns.ACCOUNT_TYPE + " TEXT, " +
-                AccountsColumns.DATA_SET + " TEXT" +
-        ");");
-
-        // Allow contacts without any account to be created for now.  Achieve that
-        // by inserting a fake account with both type and name as NULL.
-        db.execSQL("INSERT INTO " + Tables.ACCOUNTS + " VALUES(NULL, NULL, NULL)");
-
         createDirectoriesTable(db);
         createSearchIndexTable(db);
 
@@ -1289,11 +1334,6 @@
             db.execSQL("ANALYZE;");
 
             updateSqliteStats(db);
-
-            // We need to close and reopen the database connection so that the stats are
-            // taken into account. Make a note of it and do the actual reopening in the
-            // getWritableDatabase method.
-            mReopenDatabase = true;
         }
 
         ContentResolver.requestSync(null /* all accounts */,
@@ -1323,7 +1363,7 @@
         ");");
 
         // Trigger a full scan of directories in the system
-        setProperty(db, ContactDirectoryManager.PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
+        setProperty(db, DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
     }
 
     public void createSearchIndexTable(SQLiteDatabase db) {
@@ -1433,8 +1473,8 @@
                 " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
                 "     SELECT " + RawContacts.CONTACT_ID +
                 "     FROM " + Tables.RAW_CONTACTS +
-                "     WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " IS NULL " +
-                "     AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL; ");
+                "     WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID +
+                            "=" + Clauses.LOCAL_ACCOUNT_ID + ";");
         final String insertContactsWithAccountNoDefaultGroup = (
                 " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
                 "     SELECT " + RawContacts.CONTACT_ID +
@@ -1442,10 +1482,8 @@
                 "     WHERE NOT EXISTS" +
                 "         (SELECT " + Groups._ID +
                 "             FROM " + Tables.GROUPS +
-                "             WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " = " +
-                        GroupsColumns.CONCRETE_ACCOUNT_NAME +
-                "             AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " = " +
-                        GroupsColumns.CONCRETE_ACCOUNT_TYPE +
+                "             WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID + " = " +
+                                    GroupsColumns.CONCRETE_ACCOUNT_ID +
                 "             AND " + Groups.AUTO_ADD + " != 0" + ");");
         final String insertContactsWithAccountDefaultGroup = (
                 " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
@@ -1461,10 +1499,8 @@
                 "     AND EXISTS" +
                 "         (SELECT " + Groups._ID +
                 "             FROM " + Tables.GROUPS +
-                "                 WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " = " +
-                        GroupsColumns.CONCRETE_ACCOUNT_NAME +
-                "                 AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " = " +
-                        GroupsColumns.CONCRETE_ACCOUNT_TYPE +
+                "                 WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID + " = " +
+                                        GroupsColumns.CONCRETE_ACCOUNT_ID +
                 "                 AND " + Groups.AUTO_ADD + " != 0" + ");");
 
         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_auto_add_updated1;");
@@ -1537,14 +1573,15 @@
                 + Data.SYNC4;
 
         String syncColumns =
-                RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + ","
-                + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + ","
-                + RawContactsColumns.CONCRETE_DATA_SET + " AS " + RawContacts.DATA_SET + ","
-                + "(CASE WHEN " + RawContactsColumns.CONCRETE_DATA_SET + " IS NULL THEN "
-                        + RawContactsColumns.CONCRETE_ACCOUNT_TYPE
-                        + " ELSE " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + "||'/'||"
-                        + RawContactsColumns.CONCRETE_DATA_SET + " END) AS "
-                        + RawContacts.ACCOUNT_TYPE_AND_DATA_SET + ","
+                RawContactsColumns.CONCRETE_ACCOUNT_ID + ","
+                + AccountsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + ","
+                + AccountsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + ","
+                + AccountsColumns.CONCRETE_DATA_SET + " AS " + RawContacts.DATA_SET + ","
+                + "(CASE WHEN " + AccountsColumns.CONCRETE_DATA_SET + " IS NULL THEN "
+                            + AccountsColumns.CONCRETE_ACCOUNT_TYPE
+                        + " ELSE " + AccountsColumns.CONCRETE_ACCOUNT_TYPE + "||'/'||"
+                            + AccountsColumns.CONCRETE_DATA_SET + " END) AS "
+                                + RawContacts.ACCOUNT_TYPE_AND_DATA_SET + ","
                 + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
                 + RawContactsColumns.CONCRETE_NAME_VERIFIED + " AS "
                         + RawContacts.NAME_VERIFIED + ","
@@ -1613,6 +1650,9 @@
                 +   DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
                 +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
+                    + ")"
                 + " JOIN " + Tables.CONTACTS + " ON ("
                 +   RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
                 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
@@ -1649,7 +1689,10 @@
                 + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ", "
                 + rawContactOptionColumns + ", "
                 + syncColumns
-                + " FROM " + Tables.RAW_CONTACTS;
+                + " FROM " + Tables.RAW_CONTACTS
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
+                    + ")";
 
         db.execSQL("CREATE VIEW " + Views.RAW_CONTACTS + " AS " + rawContactsSelect);
 
@@ -1695,6 +1738,9 @@
                 + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ","
                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
                 + " FROM " + Tables.RAW_CONTACTS
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
+                    + ")"
                 + " LEFT OUTER JOIN " + Tables.DATA + " ON ("
                 +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
                 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
@@ -1729,6 +1775,9 @@
                 + DataColumns.CONCRETE_ID + " AS " + Contacts.Entity.DATA_ID + ","
                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
                 + " FROM " + Tables.RAW_CONTACTS
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
+                    + ")"
                 + " JOIN " + Tables.CONTACTS + " ON ("
                 +   RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
                 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
@@ -1771,9 +1820,9 @@
                 ContactsColumns.CONCRETE_ID + " AS " + StreamItems.CONTACT_ID + ", " +
                 ContactsColumns.CONCRETE_LOOKUP_KEY +
                         " AS " + StreamItems.CONTACT_LOOKUP_KEY + ", " +
-                RawContactsColumns.CONCRETE_ACCOUNT_NAME + ", " +
-                RawContactsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
-                RawContactsColumns.CONCRETE_DATA_SET + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
+                AccountsColumns.CONCRETE_DATA_SET + ", " +
                 StreamItemsColumns.CONCRETE_RAW_CONTACT_ID +
                         " as " + StreamItems.RAW_CONTACT_ID + ", " +
                 RawContactsColumns.CONCRETE_SOURCE_ID +
@@ -1791,7 +1840,11 @@
                 " FROM " + Tables.STREAM_ITEMS
                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
                 + StreamItemsColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
-                + ") JOIN " + Tables.CONTACTS + " ON ("
+                    + ")"
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
+                    + ")"
+                + " JOIN " + Tables.CONTACTS + " ON ("
                 + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")";
 
         db.execSQL("CREATE VIEW " + Views.STREAM_ITEMS + " AS " + streamItemSelect);
@@ -1831,13 +1884,17 @@
 
     private void createGroupsView(SQLiteDatabase db) {
         db.execSQL("DROP VIEW IF EXISTS " + Views.GROUPS + ";");
+
         String groupsColumns =
-                Groups.ACCOUNT_NAME + ","
-                + Groups.ACCOUNT_TYPE + ","
-                + Groups.DATA_SET + ","
-                + "(CASE WHEN " + Groups.DATA_SET + " IS NULL THEN " + Groups.ACCOUNT_TYPE
-                    + " ELSE " + Groups.ACCOUNT_TYPE + "||" + Groups.DATA_SET + " END) AS "
-                    + Groups.ACCOUNT_TYPE_AND_DATA_SET + ","
+                GroupsColumns.CONCRETE_ACCOUNT_ID + " AS " + GroupsColumns.ACCOUNT_ID + ","
+                + AccountsColumns.CONCRETE_ACCOUNT_NAME + " AS " + Groups.ACCOUNT_NAME + ","
+                + AccountsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + Groups.ACCOUNT_TYPE + ","
+                + AccountsColumns.CONCRETE_DATA_SET + " AS " + Groups.DATA_SET + ","
+                + "(CASE WHEN " + AccountsColumns.CONCRETE_DATA_SET
+                    + " IS NULL THEN " + AccountsColumns.CONCRETE_ACCOUNT_TYPE
+                    + " ELSE " + AccountsColumns.CONCRETE_ACCOUNT_TYPE
+                        + "||'/'||" + AccountsColumns.CONCRETE_DATA_SET + " END) AS "
+                            + Groups.ACCOUNT_TYPE_AND_DATA_SET + ","
                 + Groups.SOURCE_ID + ","
                 + Groups.VERSION + ","
                 + Groups.DIRTY + ","
@@ -1860,7 +1917,11 @@
         String groupsSelect = "SELECT "
                 + GroupsColumns.CONCRETE_ID + " AS " + Groups._ID + ","
                 + groupsColumns
-                + " FROM " + Tables.GROUPS_JOIN_PACKAGES;
+                + " FROM " + Tables.GROUPS
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                    + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID + ")"
+                + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
+                    + GroupsColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")";
 
         db.execSQL("CREATE VIEW " + Views.GROUPS + " AS " + groupsSelect);
     }
@@ -1900,6 +1961,7 @@
         boolean upgradeNameLookup = false;
         boolean upgradeLegacyApiSupport = false;
         boolean upgradeSearchIndex = false;
+        boolean rescanDirectories = false;
 
         if (oldVersion == 99) {
             upgradeViewsAndTriggers = true;
@@ -2303,6 +2365,17 @@
             oldVersion = 625;
         }
 
+        if (oldVersion < 626) {
+            upgradeToVersion626(db);
+            upgradeViewsAndTriggers = true;
+            oldVersion = 626;
+        }
+
+        if (oldVersion < 700) {
+            rescanDirectories = true;
+            oldVersion = 700;
+        }
+
         if (upgradeViewsAndTriggers) {
             createContactsViews(db);
             createGroupsView(db);
@@ -2310,7 +2383,6 @@
             createContactsIndexes(db);
             updateSqliteStats(db);
             upgradeLegacyApiSupport = true;
-            mReopenDatabase = true;
         }
 
         if (upgradeLegacyApiSupport) {
@@ -2326,6 +2398,12 @@
             setProperty(db, SearchIndexManager.PROPERTY_SEARCH_INDEX_VERSION, "0");
         }
 
+        if (rescanDirectories) {
+            // Force the next ContactDirectoryManager.scanAllPackages() to rescan all packages.
+            // (It's called from the BACKGROUND_TASK_UPDATE_ACCOUNTS background task.)
+            setProperty(db, DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
+        }
+
         if (oldVersion != newVersion) {
             throw new IllegalStateException(
                     "error upgrading the database to version " + newVersion);
@@ -2899,7 +2977,7 @@
     public void setLocale(ContactsProvider2 provider, Locale locale) {
         Log.i(TAG, "Switching to locale " + locale);
 
-        long start = SystemClock.uptimeMillis();
+        final long start = SystemClock.elapsedRealtime();
         SQLiteDatabase db = getWritableDatabase();
         db.setLocale(locale);
         db.beginTransaction();
@@ -2917,7 +2995,7 @@
             db.endTransaction();
         }
 
-        Log.i(TAG, "Locale change completed in " + (SystemClock.uptimeMillis() - start) + "ms");
+        Log.i(TAG, "Locale change completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
     }
 
     /**
@@ -3328,7 +3406,7 @@
     }
 
     private void upgradeToVersion504(SQLiteDatabase db) {
-        populateMimeTypeCache(db);
+        initializeCache(db);
 
         // Find all names with prefixes and recreate display name
         Cursor cursor = db.rawQuery(
@@ -3533,6 +3611,81 @@
         db.execSQL("ALTER TABLE calls ADD formatted_number TEXT DEFAULT NULL;");
     }
 
+    private void upgradeToVersion626(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS accounts");
+
+        db.execSQL("CREATE TABLE accounts (" +
+                "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
+                "account_name TEXT, " +
+                "account_type TEXT, " +
+                "data_set TEXT" +
+        ");");
+
+        // Add "account_id" column to groups and raw_contacts
+        db.execSQL("ALTER TABLE raw_contacts ADD " +
+                "account_id INTEGER REFERENCES accounts(_id)");
+        db.execSQL("ALTER TABLE groups ADD " +
+                "account_id INTEGER REFERENCES accounts(_id)");
+
+        // Update indexes.
+        db.execSQL("DROP INDEX IF EXISTS raw_contacts_source_id_index");
+        db.execSQL("DROP INDEX IF EXISTS raw_contacts_source_id_data_set_index");
+        db.execSQL("DROP INDEX IF EXISTS groups_source_id_index");
+        db.execSQL("DROP INDEX IF EXISTS groups_source_id_data_set_index");
+
+        db.execSQL("CREATE INDEX raw_contacts_source_id_account_id_index ON raw_contacts ("
+                + "sourceid, account_id);");
+        db.execSQL("CREATE INDEX groups_source_id_account_id_index ON groups ("
+                + "sourceid, account_id);");
+
+        // Migrate account_name/account_type/data_set to accounts table
+
+        final Set<AccountWithDataSet> accountsWithDataSets = Sets.newHashSet();
+        upgradeToVersion626_findAccountsWithDataSets(accountsWithDataSets, db, "raw_contacts");
+        upgradeToVersion626_findAccountsWithDataSets(accountsWithDataSets, db, "groups");
+
+        for (AccountWithDataSet accountWithDataSet : accountsWithDataSets) {
+            db.execSQL("INSERT INTO accounts (account_name,account_type,data_set)VALUES(?, ?, ?)",
+                    new String[] {
+                            accountWithDataSet.getAccountName(),
+                            accountWithDataSet.getAccountType(),
+                            accountWithDataSet.getDataSet()
+                    });
+        }
+        upgradeToVersion626_fillAccountId(db, "raw_contacts");
+        upgradeToVersion626_fillAccountId(db, "groups");
+    }
+
+    private static void upgradeToVersion626_findAccountsWithDataSets(
+            Set<AccountWithDataSet> result, SQLiteDatabase db, String table) {
+        Cursor c = db.rawQuery(
+                "SELECT DISTINCT account_name, account_type, data_set FROM " + table, null);
+        try {
+            while (c.moveToNext()) {
+                result.add(AccountWithDataSet.get(c.getString(0), c.getString(1), c.getString(2)));
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    private static void upgradeToVersion626_fillAccountId(SQLiteDatabase db, String table) {
+        StringBuilder sb = new StringBuilder();
+
+        // Set account_id and null out account_name, account_type and data_set
+
+        sb.append("UPDATE " + table + " SET account_id = (SELECT _id FROM accounts WHERE ");
+
+        addJoinExpressionAllowingNull(sb, table + ".account_name", "accounts.account_name");
+        sb.append("AND");
+        addJoinExpressionAllowingNull(sb, table + ".account_type", "accounts.account_type");
+        sb.append("AND");
+        addJoinExpressionAllowingNull(sb, table + ".data_set", "accounts.data_set");
+
+        sb.append("), account_name = null, account_type = null, data_set = null");
+        db.execSQL(sb.toString());
+    }
+
     public String extractHandleFromEmailAddress(String email) {
         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
         if (tokens.length == 0) {
@@ -3586,6 +3739,17 @@
     }
 
     /**
+     * Add a string like "(((column1) = (column2)) OR ((column1) IS NULL AND (column2) IS NULL))"
+     */
+    private static StringBuilder addJoinExpressionAllowingNull(StringBuilder sb,
+            String column1, String column2) {
+        sb.append("(((").append(column1).append(")=(").append(column2);
+        sb.append("))OR((");
+        sb.append(column1).append(") IS NULL AND (").append(column2).append(") IS NULL))");
+        return sb;
+    }
+
+    /**
      * Adds index stats into the SQLite database to force it to always use the lookup indexes.
      */
     private void updateSqliteStats(SQLiteDatabase db) {
@@ -3594,6 +3758,9 @@
         // Important here are relative sizes. Raw-Contacts is slightly bigger than Contacts
         // Warning: Missing tables in here will make SQLite assume to contain 1000000 rows,
         // which can lead to catastrophic query plans for small tables
+
+        // See the latest of version of http://www.sqlite.org/cgi/src/finfo?name=src/analyze.c
+        // for what these numbers mean.
         try {
             db.execSQL("DELETE FROM sqlite_stat1");
             updateIndexStats(db, Tables.CONTACTS,
@@ -3602,15 +3769,13 @@
                     "contacts_name_raw_contact_id_index", "9000 1");
 
             updateIndexStats(db, Tables.RAW_CONTACTS,
-                    "raw_contacts_source_id_index", "10000 1 1 1");
-            updateIndexStats(db, Tables.RAW_CONTACTS,
                     "raw_contacts_contact_id_index", "10000 2");
             updateIndexStats(db, Tables.RAW_CONTACTS,
                     "raw_contact_sort_key2_index", "10000 2");
             updateIndexStats(db, Tables.RAW_CONTACTS,
                     "raw_contact_sort_key1_index", "10000 2");
             updateIndexStats(db, Tables.RAW_CONTACTS,
-                    "raw_contacts_source_id_data_set_index", "10000 1 1 1 1");
+                    "raw_contacts_source_id_account_id_index", "10000 1 1 1 1");
 
             updateIndexStats(db, Tables.NAME_LOOKUP,
                     "name_lookup_raw_contact_id_index", "35000 4");
@@ -3632,9 +3797,7 @@
                     "data_raw_contact_id", "60000 10");
 
             updateIndexStats(db, Tables.GROUPS,
-                    "groups_source_id_index", "50 2 2 1");
-            updateIndexStats(db, Tables.GROUPS,
-                    "groups_source_id_data_set_index", "50 2 2 1 1");
+                    "groups_source_id_account_id_index", "50 2 2 1 1");
 
             updateIndexStats(db, Tables.NICKNAME_LOOKUP,
                     "nickname_lookup_index", "500 2 1");
@@ -3706,6 +3869,8 @@
             updateIndexStats(db, "search_index_segdir",
                     "sqlite_autoindex_search_index_segdir_1", "9 5 1");
 
+            // Force sqlite to reload sqlite_stat1.
+            db.execSQL("ANALYZE sqlite_master;");
         } catch (SQLException e) {
             Log.e(TAG, "Could not update index stats", e);
         }
@@ -3731,17 +3896,6 @@
                 new String[] { table, index, stats });
     }
 
-    @Override
-    public synchronized SQLiteDatabase getWritableDatabase() {
-        SQLiteDatabase db = super.getWritableDatabase();
-        if (mReopenDatabase) {
-            mReopenDatabase = false;
-            close();
-            db = super.getWritableDatabase();
-        }
-        return db;
-    }
-
     /**
      * Wipes all data except mime type and package lookup tables.
      */
@@ -3749,8 +3903,6 @@
         SQLiteDatabase db = getWritableDatabase();
 
         db.execSQL("DELETE FROM " + Tables.ACCOUNTS + ";");
-        db.execSQL("INSERT INTO " + Tables.ACCOUNTS + " VALUES(NULL, NULL, NULL)");
-
         db.execSQL("DELETE FROM " + Tables.CONTACTS + ";");
         db.execSQL("DELETE FROM " + Tables.RAW_CONTACTS + ";");
         db.execSQL("DELETE FROM " + Tables.STREAM_ITEMS + ";");
@@ -3767,6 +3919,8 @@
         db.execSQL("DELETE FROM " + Tables.DIRECTORIES + ";");
         db.execSQL("DELETE FROM " + Tables.SEARCH_INDEX + ";");
 
+        initializeCache(db);
+
         // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
     }
 
@@ -3969,10 +4123,91 @@
     }
 
     /**
+     * Gets all accounts in the accounts table.
+     */
+    public Set<AccountWithDataSet> getAllAccountsWithDataSets() {
+        final Set<AccountWithDataSet> result = Sets.newHashSet();
+        Cursor c = getReadableDatabase().rawQuery(
+                "SELECT DISTINCT " +  AccountsColumns._ID + "," + AccountsColumns.ACCOUNT_NAME +
+                "," + AccountsColumns.ACCOUNT_TYPE + "," + AccountsColumns.DATA_SET +
+                " FROM " + Tables.ACCOUNTS, null);
+        try {
+            while (c.moveToNext()) {
+                result.add(AccountWithDataSet.get(c.getString(1), c.getString(2), c.getString(3)));
+            }
+        } finally {
+            c.close();
+        }
+        return result;
+    }
+
+    /**
+     * @return ID of the specified account, or null if the account doesn't exist.
+     */
+    public Long getAccountIdOrNull(AccountWithDataSet accountWithDataSet) {
+        if (accountWithDataSet == null) {
+            accountWithDataSet = AccountWithDataSet.LOCAL;
+        }
+        final SQLiteStatement select = getWritableDatabase().compileStatement(
+                "SELECT " + AccountsColumns._ID +
+                " FROM " + Tables.ACCOUNTS +
+                " WHERE " +
+                "((?1 IS NULL AND " + AccountsColumns.ACCOUNT_NAME + " IS NULL) OR " +
+                "(" + AccountsColumns.ACCOUNT_NAME + "=?1)) AND " +
+                "((?2 IS NULL AND " + AccountsColumns.ACCOUNT_TYPE + " IS NULL) OR " +
+                "(" + AccountsColumns.ACCOUNT_TYPE + "=?2)) AND " +
+                "((?3 IS NULL AND " + AccountsColumns.DATA_SET + " IS NULL) OR " +
+                "(" + AccountsColumns.DATA_SET + "=?3))");
+        try {
+            DatabaseUtils.bindObjectToProgram(select, 1, accountWithDataSet.getAccountName());
+            DatabaseUtils.bindObjectToProgram(select, 2, accountWithDataSet.getAccountType());
+            DatabaseUtils.bindObjectToProgram(select, 3, accountWithDataSet.getDataSet());
+            try {
+                return select.simpleQueryForLong();
+            } catch (SQLiteDoneException notFound) {
+                return null;
+            }
+        } finally {
+            select.close();
+        }
+    }
+
+    /**
+     * @return ID of the specified account.  This method will create a record in the accounts table
+     *     if the account doesn't exist in the accounts table.
+     *
+     * This must be used in a transaction, so there's no need for synchronization.
+     */
+    public long getOrCreateAccountIdInTransaction(AccountWithDataSet accountWithDataSet) {
+        if (accountWithDataSet == null) {
+            accountWithDataSet = AccountWithDataSet.LOCAL;
+        }
+        Long id = getAccountIdOrNull(accountWithDataSet);
+        if (id != null) {
+            return id;
+        }
+        final SQLiteStatement insert = getWritableDatabase().compileStatement(
+                "INSERT INTO " + Tables.ACCOUNTS +
+                " (" + AccountsColumns.ACCOUNT_NAME + ", " +
+                AccountsColumns.ACCOUNT_TYPE + ", " +
+                AccountsColumns.DATA_SET + ") VALUES (?, ?, ?)");
+        try {
+            DatabaseUtils.bindObjectToProgram(insert, 1, accountWithDataSet.getAccountName());
+            DatabaseUtils.bindObjectToProgram(insert, 2, accountWithDataSet.getAccountType());
+            DatabaseUtils.bindObjectToProgram(insert, 3, accountWithDataSet.getDataSet());
+            id = insert.executeInsert();
+        } finally {
+            insert.close();
+        }
+
+        return id;
+    }
+
+    /**
      * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts.
      */
     public void updateAllVisible() {
-        updateCustomContactVisibility(getWritableDatabase(), "");
+        updateCustomContactVisibility(getWritableDatabase(), -1);
     }
 
     /**
@@ -3993,7 +4228,7 @@
     public boolean updateContactVisible(
             TransactionContext txContext, long contactId, boolean onlyIfChanged) {
         SQLiteDatabase db = getWritableDatabase();
-        updateCustomContactVisibility(db, " AND " + Contacts._ID + "=" + contactId);
+        updateCustomContactVisibility(db, contactId);
 
         String contactIdAsString = String.valueOf(contactId);
         long mimetype = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
@@ -4008,38 +4243,29 @@
                     " JOIN " + Tables.DATA +
                     "   ON (" + RawContactsColumns.CONCRETE_ID + "="
                             + Data.RAW_CONTACT_ID + ")" +
-                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
-                    "   AND " + DataColumns.MIMETYPE_ID + "=?" +
+                    " WHERE " + RawContacts.CONTACT_ID + "=?1" +
+                    "   AND " + DataColumns.MIMETYPE_ID + "=?2" +
                 ") OR EXISTS (" +
                     "SELECT " + RawContacts._ID +
                     " FROM " + Tables.RAW_CONTACTS +
-                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
+                    " WHERE " + RawContacts.CONTACT_ID + "=?1" +
                     "   AND NOT EXISTS" +
                         " (SELECT " + Groups._ID +
                         "  FROM " + Tables.GROUPS +
-                        "  WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " = "
-                                + GroupsColumns.CONCRETE_ACCOUNT_NAME +
-                        "  AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " = "
-                                + GroupsColumns.CONCRETE_ACCOUNT_TYPE +
-                        "  AND (" + RawContactsColumns.CONCRETE_DATA_SET + " = "
-                                + GroupsColumns.CONCRETE_DATA_SET
-                                + " OR " + RawContactsColumns.CONCRETE_DATA_SET + " IS NULL AND "
-                                + GroupsColumns.CONCRETE_DATA_SET + " IS NULL)" +
+                        "  WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID + " = "
+                                + GroupsColumns.CONCRETE_ACCOUNT_ID +
                         "  AND " + Groups.AUTO_ADD + " != 0" +
                         ")" +
                 ") OR EXISTS (" +
                     "SELECT " + RawContacts._ID +
                     " FROM " + Tables.RAW_CONTACTS +
-                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
-                    "   AND " + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " IS NULL " +
-                    "   AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL" +
-                    "   AND " + RawContactsColumns.CONCRETE_DATA_SET + " IS NULL" +
+                    " WHERE " + RawContacts.CONTACT_ID + "=?1" +
+                    "   AND " + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" +
+                        Clauses.LOCAL_ACCOUNT_ID +
                 ")",
                 new String[] {
                     contactIdAsString,
-                    String.valueOf(mimetype),
-                    contactIdAsString,
-                    contactIdAsString
+                    String.valueOf(mimetype)
                 }) != 0;
 
         if (onlyIfChanged) {
@@ -4075,26 +4301,38 @@
         return mContactInDefaultDirectoryQuery.simpleQueryForLong() != 0;
     }
 
-    private void updateCustomContactVisibility(SQLiteDatabase db, String selection) {
+    /**
+     * Update the visible_contacts table according to the current visibility of contacts, which
+     * is defined by {@link Clauses#CONTACT_IS_VISIBLE}.
+     *
+     * If {@code optionalContactId} is non-negative, it'll update only for the specified contact.
+     */
+    private void updateCustomContactVisibility(SQLiteDatabase db, long optionalContactId) {
         final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
         String[] selectionArgs = new String[]{String.valueOf(groupMembershipMimetypeId)};
 
+        final String contactIdSelect = (optionalContactId < 0) ? "" :
+                (Contacts._ID + "=" + optionalContactId + " AND ");
+
         // First delete what needs to be deleted, then insert what needs to be added.
         // Since flash writes are very expensive, this approach is much better than
         // delete-all-insert-all.
-        db.execSQL("DELETE FROM " + Tables.VISIBLE_CONTACTS +
-                   " WHERE " + "_id NOT IN" +
-                        "(SELECT " + Contacts._ID +
-                        " FROM " + Tables.CONTACTS +
-                        " WHERE (" + Clauses.CONTACT_IS_VISIBLE + ")=1) " + selection,
+        db.execSQL(
+                "DELETE FROM " + Tables.VISIBLE_CONTACTS +
+                " WHERE " + Contacts._ID + " IN" +
+                    "(SELECT " + Contacts._ID +
+                    " FROM " + Tables.CONTACTS +
+                    " WHERE " + contactIdSelect + "(" + Clauses.CONTACT_IS_VISIBLE + ")=0) ",
                 selectionArgs);
 
-        db.execSQL("INSERT INTO " + Tables.VISIBLE_CONTACTS +
-                   " SELECT " + Contacts._ID +
-                   " FROM " + Tables.CONTACTS +
-                   " WHERE " + Contacts._ID +
-                   " NOT IN " + Tables.VISIBLE_CONTACTS +
-                           " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1 " + selection,
+        db.execSQL(
+                "INSERT INTO " + Tables.VISIBLE_CONTACTS +
+                " SELECT " + Contacts._ID +
+                " FROM " + Tables.CONTACTS +
+                " WHERE " +
+                    contactIdSelect +
+                    Contacts._ID + " NOT IN " + Tables.VISIBLE_CONTACTS +
+                    " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1 ",
                 selectionArgs);
     }
 
@@ -4377,21 +4615,31 @@
     }
 
     /**
+     * Test if the given column appears in the given projection.
+     */
+    public static boolean isInProjection(String[] projection, String column) {
+        if (projection == null) {
+            return true; // Null means "all columns".  We can't really tell if it's in there...
+        }
+        for (String test : projection) {
+            if (column.equals(test)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Test if any of the columns appear in the given projection.
      */
-    public boolean isInProjection(String[] projection, String... columns) {
+    public static boolean isInProjection(String[] projection, String... columns) {
         if (projection == null) {
             return true;
         }
 
         // Optimized for a single-column test
         if (columns.length == 1) {
-            String column = columns[0];
-            for (String test : projection) {
-                if (column.equals(test)) {
-                    return true;
-                }
-            }
+            return isInProjection(projection, columns[0]);
         } else {
             for (String test : projection) {
                 for (String column : columns) {
@@ -4958,6 +5206,7 @@
         return mCountryMonitor.getCountryIso();
     }
 
+    @NeededForTesting
     /* package */ String querySearchIndexContentForTest(long contactId) {
         return DatabaseUtils.stringForQuery(getReadableDatabase(),
                 "SELECT " + SearchIndexColumns.CONTENT +
@@ -4966,6 +5215,7 @@
                 new String[] { String.valueOf(contactId) });
     }
 
+    @NeededForTesting
     /* package */ String querySearchIndexTokensForTest(long contactId) {
         return DatabaseUtils.stringForQuery(getReadableDatabase(),
                 "SELECT " + SearchIndexColumns.TOKENS +
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 59b2170..6de16df 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -16,9 +16,10 @@
 
 package com.android.providers.contacts;
 
+import com.android.common.content.ProjectionMap;
 import com.android.common.content.SyncStateContentProviderHelper;
-import com.android.providers.contacts.ContactAggregator.AggregationSuggestionParameter;
 import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
+import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
@@ -26,14 +27,15 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
 import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Joins;
 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
-import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Projections;
 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
@@ -41,9 +43,14 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemPhotosColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.ContactsDatabaseHelper.ViewGroupsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Views;
 import com.android.providers.contacts.SearchIndexManager.FtsQueryBuilder;
+import com.android.providers.contacts.aggregation.ContactAggregator;
+import com.android.providers.contacts.aggregation.ContactAggregator.AggregationSuggestionParameter;
+import com.android.providers.contacts.aggregation.ProfileAggregator;
 import com.android.providers.contacts.util.DbQueryUtils;
+import com.android.providers.contacts.util.NeededForTesting;
 import com.android.vcard.VCardComposer;
 import com.android.vcard.VCardConfig;
 import com.google.android.collect.Lists;
@@ -54,10 +61,8 @@
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.OnAccountsUpdateListener;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.app.SearchManager;
+import android.content.CancellationSignal;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
 import android.content.ContentResolver;
@@ -65,7 +70,6 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.IContentService;
-import android.content.Intent;
 import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
 import android.content.SyncAdapterType;
@@ -77,10 +81,7 @@
 import android.content.res.Resources;
 import android.content.res.Resources.NotFoundException;
 import android.database.AbstractCursor;
-import android.database.CrossProcessCursor;
 import android.database.Cursor;
-import android.database.CursorWindow;
-import android.database.CursorWrapper;
 import android.database.DatabaseUtils;
 import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
@@ -128,7 +129,6 @@
 import android.provider.ContactsContract.Directory;
 import android.provider.ContactsContract.DisplayPhoto;
 import android.provider.ContactsContract.Groups;
-import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.PhoneLookup;
 import android.provider.ContactsContract.PhotoFiles;
 import android.provider.ContactsContract.Profile;
@@ -150,10 +150,12 @@
 import java.io.BufferedWriter;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
 import java.io.Writer;
 import java.security.SecureRandom;
 import java.text.SimpleDateFormat;
@@ -176,13 +178,8 @@
 public class ContactsProvider2 extends AbstractContactsProvider
         implements OnAccountsUpdateListener {
 
-    private static final String TAG = "ContactsProvider";
-
-    private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
-
     private static final int BACKGROUND_TASK_INITIALIZE = 0;
     private static final int BACKGROUND_TASK_OPEN_WRITE_ACCESS = 1;
-    private static final int BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS = 2;
     private static final int BACKGROUND_TASK_UPDATE_ACCOUNTS = 3;
     private static final int BACKGROUND_TASK_UPDATE_LOCALE = 4;
     private static final int BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM = 5;
@@ -212,16 +209,8 @@
      */
     private static final String PREAUTHORIZED_URI_TOKEN = "perm_token";
 
-    /**
-     * Property key for the legacy contact import version. The need for a version
-     * as opposed to a boolean flag is that if we discover bugs in the contact import process,
-     * we can trigger re-import by incrementing the import version.
-     */
-    private static final String PROPERTY_CONTACTS_IMPORTED = "contacts_imported_v1";
-    private static final int PROPERTY_CONTACTS_IMPORT_VERSION = 1;
     private static final String PREF_LOCALE = "locale";
 
-    private static final String PROPERTY_AGGREGATION_ALGORITHM = "aggregation_v2";
     private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 2;
 
     private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";
@@ -283,8 +272,8 @@
 
     private static final int RAW_CONTACTS = 2002;
     private static final int RAW_CONTACTS_ID = 2003;
-    private static final int RAW_CONTACTS_DATA = 2004;
-    private static final int RAW_CONTACT_ENTITY_ID = 2005;
+    private static final int RAW_CONTACTS_ID_DATA = 2004;
+    private static final int RAW_CONTACT_ID_ENTITY = 2005;
     private static final int RAW_CONTACTS_ID_DISPLAY_PHOTO = 2006;
     private static final int RAW_CONTACTS_ID_STREAM_ITEMS = 2007;
     private static final int RAW_CONTACTS_ID_STREAM_ITEMS_ID = 2008;
@@ -300,6 +289,9 @@
     private static final int EMAILS_FILTER = 3008;
     private static final int POSTALS = 3009;
     private static final int POSTALS_ID = 3010;
+    private static final int CALLABLES = 3011;
+    private static final int CALLABLES_ID = 3012;
+    private static final int CALLABLES_FILTER = 3013;
 
     private static final int PHONE_LOOKUP = 4000;
 
@@ -357,7 +349,7 @@
     private static final int STREAM_ITEMS_ID_PHOTOS_ID = 21004;
     private static final int STREAM_ITEMS_LIMIT = 21005;
 
-    private static final int DISPLAY_PHOTO = 22000;
+    private static final int DISPLAY_PHOTO_ID = 22000;
     private static final int PHOTO_DIMENSIONS = 22001;
 
     // Inserts into URIs in this map will direct to the profile database if the parent record's
@@ -366,7 +358,7 @@
     private static final Map<Integer, String> INSERT_URI_ID_VALUE_MAP = Maps.newHashMap();
     static {
         INSERT_URI_ID_VALUE_MAP.put(DATA, Data.RAW_CONTACT_ID);
-        INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_DATA, Data.RAW_CONTACT_ID);
+        INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_ID_DATA, Data.RAW_CONTACT_ID);
         INSERT_URI_ID_VALUE_MAP.put(STATUS_UPDATES, StatusUpdates.DATA_ID);
         INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS, StreamItems.RAW_CONTACT_ID);
         INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_ID_STREAM_ITEMS, StreamItems.RAW_CONTACT_ID);
@@ -392,27 +384,13 @@
 
     private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID =
             RawContactsColumns.CONCRETE_ID + "=? AND "
-                    + GroupsColumns.CONCRETE_ACCOUNT_NAME
-                    + "=" + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND "
-                    + GroupsColumns.CONCRETE_ACCOUNT_TYPE
-                    + "=" + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND ("
-                    + GroupsColumns.CONCRETE_DATA_SET
-                    + "=" + RawContactsColumns.CONCRETE_DATA_SET + " OR "
-                    + GroupsColumns.CONCRETE_DATA_SET + " IS NULL AND "
-                    + RawContactsColumns.CONCRETE_DATA_SET + " IS NULL)"
-                    + " AND " + Groups.FAVORITES + " != 0";
+                + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
+                + " AND " + Groups.FAVORITES + " != 0";
 
     private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID =
             RawContactsColumns.CONCRETE_ID + "=? AND "
-                    + GroupsColumns.CONCRETE_ACCOUNT_NAME + "="
-                    + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND "
-                    + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
-                    + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND ("
-                    + GroupsColumns.CONCRETE_DATA_SET + "="
-                    + RawContactsColumns.CONCRETE_DATA_SET + " OR "
-                    + GroupsColumns.CONCRETE_DATA_SET + " IS NULL AND "
-                    + RawContactsColumns.CONCRETE_DATA_SET + " IS NULL)"
-                    + " AND " + Groups.AUTO_ADD + " != 0";
+                + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
+                + " AND " + Groups.AUTO_ADD + " != 0";
 
     private static final String[] PROJECTION_GROUP_ID
             = new String[]{Tables.GROUPS + "." + Groups._ID};
@@ -428,13 +406,16 @@
     private interface DataContactsQuery {
         public static final String TABLE = "data "
                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
+                + "JOIN " + Tables.ACCOUNTS + " ON ("
+                    + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
+                    + ")"
                 + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
 
         public static final String[] PROJECTION = new String[] {
             RawContactsColumns.CONCRETE_ID,
-            RawContactsColumns.CONCRETE_ACCOUNT_TYPE,
-            RawContactsColumns.CONCRETE_ACCOUNT_NAME,
-            RawContactsColumns.CONCRETE_DATA_SET,
+            AccountsColumns.CONCRETE_ACCOUNT_TYPE,
+            AccountsColumns.CONCRETE_ACCOUNT_NAME,
+            AccountsColumns.CONCRETE_DATA_SET,
             DataColumns.CONCRETE_ID,
             ContactsColumns.CONCRETE_ID
         };
@@ -448,19 +429,21 @@
     }
 
     interface RawContactsQuery {
-        String TABLE = Tables.RAW_CONTACTS;
+        String TABLE = Tables.RAW_CONTACTS_JOIN_ACCOUNTS;
 
         String[] COLUMNS = new String[] {
                 RawContacts.DELETED,
-                RawContacts.ACCOUNT_TYPE,
-                RawContacts.ACCOUNT_NAME,
-                RawContacts.DATA_SET,
+                RawContactsColumns.ACCOUNT_ID,
+                AccountsColumns.CONCRETE_ACCOUNT_TYPE,
+                AccountsColumns.CONCRETE_ACCOUNT_NAME,
+                AccountsColumns.CONCRETE_DATA_SET,
         };
 
         int DELETED = 0;
-        int ACCOUNT_TYPE = 1;
-        int ACCOUNT_NAME = 2;
-        int DATA_SET = 3;
+        int ACCOUNT_ID = 1;
+        int ACCOUNT_TYPE = 2;
+        int ACCOUNT_NAME = 3;
+        int DATA_SET = 4;
     }
 
     public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
@@ -502,22 +485,22 @@
 
     /*
      * Sorting order for email address suggestions: first starred, then the rest.
-     * second in_visible_group, then the rest.
-     * Within the four (starred/unstarred, in_visible_group/not-in_visible_group) groups
-     * - three buckets: very recently contacted, then fairly
-     * recently contacted, then the rest.  Within each of the bucket - descending count
-     * of times contacted (both for data row and for contact row). If all else fails, alphabetical.
+     * Within the two groups:
+     * - three buckets: very recently contacted, then fairly recently contacted, then the rest.
+     * Within each of the bucket - descending count of times contacted (both for data row and for
+     * contact row).
+     * If all else fails, in_visible_group, alphabetical.
      * (Super)primary email address is returned before other addresses for the same contact.
      */
     private static final String EMAIL_FILTER_SORT_ORDER =
         Contacts.STARRED + " DESC, "
-        + Contacts.IN_VISIBLE_GROUP + " DESC, "
         + "(CASE WHEN " + TIME_SINCE_LAST_USED + " < " + EMAIL_FILTER_CURRENT
         + " THEN 0 "
                 + " WHEN " + TIME_SINCE_LAST_USED + " < " + EMAIL_FILTER_RECENT
         + " THEN 1 "
         + " ELSE 2 END), "
         + DataUsageStatColumns.TIMES_USED + " DESC, "
+        + Contacts.IN_VISIBLE_GROUP + " DESC, "
         + Contacts.DISPLAY_NAME + ", "
         + Data.CONTACT_ID + ", "
         + Data.IS_SUPER_PRIMARY + " DESC, "
@@ -887,6 +870,8 @@
      * Note {@link Groups#SUMMARY_COUNT} doesn't exist in groups/view_groups.
      * When we detect this column being requested, we join {@link Joins#GROUP_MEMBER_COUNT} to
      * generate it.
+     *
+     * TODO Support SUMMARY_GROUP_COUNT_PER_ACCOUNT too.  See also queryLocal().
      */
     private static final ProjectionMap sGroupsSummaryProjectionMap = ProjectionMap.builder()
             .addAll(sGroupsProjectionMap)
@@ -895,27 +880,7 @@
                     "(SELECT COUNT(" + ContactsColumns.CONCRETE_ID + ") FROM "
                         + Tables.CONTACTS_JOIN_RAW_CONTACTS_DATA_FILTERED_BY_GROUPMEMBERSHIP
                         + " WHERE " + Contacts.HAS_PHONE_NUMBER + ")")
-            .build();
-
-    // This is only exposed as hidden API for the contacts app, so we can be very specific in
-    // the filtering
-    private static final ProjectionMap sGroupsSummaryProjectionMapWithGroupCountPerAccount =
-            ProjectionMap.builder()
-            .addAll(sGroupsSummaryProjectionMap)
-            .add(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT,
-                    "(SELECT COUNT(*) FROM " + Views.GROUPS + " WHERE "
-                        + "(" + Groups.ACCOUNT_NAME + "="
-                            + GroupsColumns.CONCRETE_ACCOUNT_NAME
-                            + " AND "
-                            + Groups.ACCOUNT_TYPE + "=" + GroupsColumns.CONCRETE_ACCOUNT_TYPE
-                            + " AND "
-                            + Groups.DELETED + "=0 AND "
-                            + Groups.FAVORITES + "=0 AND "
-                            + Groups.AUTO_ADD + "=0"
-                        + ")"
-                        + " GROUP BY "
-                            + Groups.ACCOUNT_NAME + ", " + Groups.ACCOUNT_TYPE
-                   + ")")
+            .add(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, "0") // Always returns 0 for now.
             .build();
 
     /** Contains the agg_exceptions columns */
@@ -940,14 +905,14 @@
                                 + " THEN 1"
                                 + " ELSE MIN(" + Groups.SHOULD_SYNC + ")"
                                 + " END)"
-                            + " FROM " + Tables.GROUPS
-                            + " WHERE " + GroupsColumns.CONCRETE_ACCOUNT_NAME + "="
+                            + " FROM " + Views.GROUPS
+                            + " WHERE " + ViewGroupsColumns.CONCRETE_ACCOUNT_NAME + "="
                                     + SettingsColumns.CONCRETE_ACCOUNT_NAME
-                                + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
+                                + " AND " + ViewGroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
                                     + SettingsColumns.CONCRETE_ACCOUNT_TYPE
-                                + " AND ((" + GroupsColumns.CONCRETE_DATA_SET + " IS NULL AND "
+                                + " AND ((" + ViewGroupsColumns.CONCRETE_DATA_SET + " IS NULL AND "
                                     + SettingsColumns.CONCRETE_DATA_SET + " IS NULL) OR ("
-                                    + GroupsColumns.CONCRETE_DATA_SET + "="
+                                    + ViewGroupsColumns.CONCRETE_DATA_SET + "="
                                     + SettingsColumns.CONCRETE_DATA_SET + "))))=0"
                     + " THEN 1"
                     + " ELSE 0"
@@ -1063,13 +1028,13 @@
     private static final String DEFAULT_SNIPPET_ARG_ELLIPSIS = "...";
     private static final int DEFAULT_SNIPPET_ARG_MAX_TOKENS = -10;
 
-    private boolean sIsPhoneInitialized;
-    private boolean sIsPhone;
+    private boolean mIsPhoneInitialized;
+    private boolean mIsPhone;
 
-    private StringBuilder mSb = new StringBuilder();
-    private String[] mSelectionArgs1 = new String[1];
-    private String[] mSelectionArgs2 = new String[2];
-    private ArrayList<String> mSelectionArgs = Lists.newArrayList();
+    private final StringBuilder mSb = new StringBuilder();
+    private final String[] mSelectionArgs1 = new String[1];
+    private final String[] mSelectionArgs2 = new String[2];
+    private final ArrayList<String> mSelectionArgs = Lists.newArrayList();
 
     private Account mAccount;
 
@@ -1129,10 +1094,10 @@
 
         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
-        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
+        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_ID_DATA);
         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/display_photo",
                 RAW_CONTACTS_ID_DISPLAY_PHOTO);
-        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID);
+        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ID_ENTITY);
         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items",
                 RAW_CONTACTS_ID_STREAM_ITEMS);
         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items/#",
@@ -1156,6 +1121,10 @@
         matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
         /** "*" is in CSV form with data ids ("123,456,789") */
         matcher.addURI(ContactsContract.AUTHORITY, "data/usagefeedback/*", DATA_USAGE_FEEDBACK_ID);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/callables/", CALLABLES);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/callables/#", CALLABLES_ID);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter", CALLABLES_FILTER);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter/*", CALLABLES_FILTER);
 
         matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
         matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
@@ -1222,7 +1191,7 @@
                 STREAM_ITEMS_ID_PHOTOS_ID);
         matcher.addURI(ContactsContract.AUTHORITY, "stream_items_limit", STREAM_ITEMS_LIMIT);
 
-        matcher.addURI(ContactsContract.AUTHORITY, "display_photo/#", DISPLAY_PHOTO);
+        matcher.addURI(ContactsContract.AUTHORITY, "display_photo/#", DISPLAY_PHOTO_ID);
         matcher.addURI(ContactsContract.AUTHORITY, "photo_dimensions", PHOTO_DIMENSIONS);
 
         HashMap<String, Integer> tmpTypeMap = new HashMap<String, Integer>();
@@ -1247,20 +1216,23 @@
     private boolean mDirectoryCacheValid = false;
 
     /**
-     * An entry in group id cache. It maps the combination of (account type, account name, data set,
-     * and source id) to group row id.
+     * An entry in group id cache.
+     *
+     * TODO: Move this and {@link #mGroupIdCache} to {@link DataRowHandlerForGroupMembership}.
      */
     public static class GroupIdCacheEntry {
-        String accountType;
-        String accountName;
-        String dataSet;
+        long accountId;
         String sourceId;
         long groupId;
     }
 
-    // We don't need a soft cache for groups - the assumption is that there will only
-    // be a small number of contact groups. The cache is keyed off source id.  The value
-    // is a list of groups with this group id.
+    /**
+     * Map from group source IDs to lists of {@link GroupIdCacheEntry}s.
+     *
+     * We don't need a soft cache for groups - the assumption is that there will only
+     * be a small number of contact groups. The cache is keyed off source id.  The value
+     * is a list of groups with this group id.
+     */
     private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
 
     /**
@@ -1335,8 +1307,8 @@
     // The active transaction context will switch depending on the operation being performed.
     // Both transaction contexts will be cleared out when a batch transaction is started, and
     // each will be processed separately when a batch transaction completes.
-    private TransactionContext mContactTransactionContext = new TransactionContext(false);
-    private TransactionContext mProfileTransactionContext = new TransactionContext(true);
+    private final TransactionContext mContactTransactionContext = new TransactionContext(false);
+    private final TransactionContext mProfileTransactionContext = new TransactionContext(true);
     private final ThreadLocal<TransactionContext> mTransactionContext =
             new ThreadLocal<TransactionContext>();
 
@@ -1344,18 +1316,18 @@
     private long mPreAuthorizedUriDuration;
 
     // Map of single-use pre-authorized URIs to expiration times.
-    private Map<Uri, Long> mPreAuthorizedUris = Maps.newHashMap();
+    private final Map<Uri, Long> mPreAuthorizedUris = Maps.newHashMap();
 
     // Random number generator.
-    private SecureRandom mRandom = new SecureRandom();
+    private final SecureRandom mRandom = new SecureRandom();
 
     private LegacyApiSupport mLegacyApiSupport;
     private GlobalSearchSupport mGlobalSearchSupport;
     private CommonNicknameCache mCommonNicknameCache;
     private SearchIndexManager mSearchIndexManager;
 
-    private ContentValues mValues = new ContentValues();
-    private HashMap<String, Boolean> mAccountWritability = Maps.newHashMap();
+    private final ContentValues mValues = new ContentValues();
+    private final HashMap<String, Boolean> mAccountWritability = Maps.newHashMap();
 
     private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
     private boolean mProviderStatusUpdateNeeded;
@@ -1377,6 +1349,13 @@
 
     private long mLastPhotoCleanup = 0;
 
+    private FastScrollingIndexCache mFastScrollingIndexCache;
+
+    // Stats about FastScrollingIndex.
+    private int mFastScrollingIndexCacheRequestCount;
+    private int mFastScrollingIndexCacheMissCount;
+    private long mTotalTimeFastScrollingIndexGenerate;
+
     @Override
     public boolean onCreate() {
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
@@ -1387,6 +1366,12 @@
             return initialize();
         } catch (RuntimeException e) {
             Log.e(TAG, "Cannot start provider", e);
+            // In production code we don't want to throw here, so that phone will still work
+            // in low storage situations.
+            // See I5c88a3024ff1c5a06b5756b29a2d903f8f6a2531
+            if (shouldThrowExceptionForInitializationError()) {
+                throw e;
+            }
             return false;
         } finally {
             if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
@@ -1395,6 +1380,10 @@
         }
     }
 
+    protected boolean shouldThrowExceptionForInitializationError() {
+        return false;
+    }
+
     private boolean initialize() {
         StrictMode.setThreadPolicy(
                 new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
@@ -1405,6 +1394,8 @@
         mMaxThumbnailPhotoDim = resources.getInteger(
                 R.integer.config_max_thumbnail_photo_dim);
 
+        mFastScrollingIndexCache = new FastScrollingIndexCache(getContext());
+
         mContactsHelper = getDatabaseHelper(getContext());
         mDbHelper.set(mContactsHelper);
 
@@ -1444,7 +1435,6 @@
                 DEFAULT_PREAUTHORIZED_URI_EXPIRATION);
 
         scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE);
-        scheduleBackgroundTask(BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS);
         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_LOCALE);
         scheduleBackgroundTask(BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM);
@@ -1521,10 +1511,8 @@
                 new DataRowHandlerForNote(context, dbHelper, contactAggregator));
     }
 
-    /**
-     * Visible for testing.
-     */
-    /* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) {
+    @VisibleForTesting
+    PhotoPriorityResolver createPhotoPriorityResolver(Context context) {
         return new PhotoPriorityResolver(context);
     }
 
@@ -1553,13 +1541,6 @@
                 break;
             }
 
-            case BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS: {
-                if (isLegacyContactImportNeeded()) {
-                    importLegacyContactsInBackground();
-                }
-                break;
-            }
-
             case BACKGROUND_TASK_UPDATE_ACCOUNTS: {
                 Context context = getContext();
                 if (!mAccountUpdateListenerRegistered) {
@@ -1665,6 +1646,7 @@
         mContactsHelper.setLocale(this, currentLocale);
         mProfileHelper.setLocale(this, currentLocale);
         prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).apply();
+        invalidateFastScrollingIndexCache();
         setProviderStatus(providerStatus);
     }
 
@@ -1725,7 +1707,7 @@
         }
     }
 
-    /* Visible for testing */
+    @VisibleForTesting
     protected void cleanupPhotoStore() {
         SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
         mActiveDb.set(db);
@@ -1752,32 +1734,23 @@
 
         // Also query for all social stream item photos.
         c = db.query(Tables.STREAM_ITEM_PHOTOS + " JOIN " + Tables.STREAM_ITEMS
-                + " ON " + StreamItemPhotos.STREAM_ITEM_ID + "=" + StreamItemsColumns.CONCRETE_ID
-                + " JOIN " + Tables.RAW_CONTACTS
-                + " ON " + StreamItems.RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID,
+                + " ON " + StreamItemPhotos.STREAM_ITEM_ID + "=" + StreamItemsColumns.CONCRETE_ID,
                 new String[]{
                         StreamItemPhotosColumns.CONCRETE_ID,
                         StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID,
-                        StreamItemPhotos.PHOTO_FILE_ID,
-                        RawContacts.ACCOUNT_TYPE,
-                        RawContacts.ACCOUNT_NAME
+                        StreamItemPhotos.PHOTO_FILE_ID
                 },
                 null, null, null, null, null);
         Map<Long, Long> photoFileIdToStreamItemPhotoId = Maps.newHashMap();
         Map<Long, Long> streamItemPhotoIdToStreamItemId = Maps.newHashMap();
-        Map<Long, Account> streamItemPhotoIdToAccount = Maps.newHashMap();
         try {
             while (c.moveToNext()) {
                 long streamItemPhotoId = c.getLong(0);
                 long streamItemId = c.getLong(1);
                 long photoFileId = c.getLong(2);
-                String accountType = c.getString(3);
-                String accountName = c.getString(4);
                 usedPhotoFileIds.add(photoFileId);
                 photoFileIdToStreamItemPhotoId.put(photoFileId, streamItemPhotoId);
                 streamItemPhotoIdToStreamItemId.put(streamItemPhotoId, streamItemId);
-                Account account = new Account(accountName, accountType);
-                streamItemPhotoIdToAccount.put(photoFileId, account);
             }
         } finally {
             c.close();
@@ -1841,11 +1814,11 @@
         return mProfilePhotoStore;
     }
 
-    /* package */ int getMaxDisplayPhotoDim() {
+    public int getMaxDisplayPhotoDim() {
         return mMaxDisplayPhotoDim;
     }
 
-    /* package */ int getMaxThumbnailPhotoDim() {
+    public int getMaxThumbnailPhotoDim() {
         return mMaxThumbnailPhotoDim;
     }
 
@@ -1857,12 +1830,12 @@
         return mNameLookupBuilder;
     }
 
-    /* Visible for testing */
+    @VisibleForTesting
     public ContactDirectoryManager getContactDirectoryManagerForTest() {
         return mContactDirectoryManager;
     }
 
-    /* Visible for testing */
+    @VisibleForTesting
     protected Locale getLocale() {
         return Locale.getDefault();
     }
@@ -1872,99 +1845,12 @@
         return profileMode != null && profileMode;
     }
 
-    protected boolean isLegacyContactImportNeeded() {
-        int version = Integer.parseInt(
-                mContactsHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0"));
-        return version < PROPERTY_CONTACTS_IMPORT_VERSION;
-    }
-
-    protected LegacyContactImporter getLegacyContactImporter() {
-        return new LegacyContactImporter(getContext(), this);
-    }
-
-    /**
-     * Imports legacy contacts as a background task.
-     */
-    private void importLegacyContactsInBackground() {
-        Log.v(TAG, "Importing legacy contacts");
-        setProviderStatus(ProviderStatus.STATUS_UPGRADING);
-
-        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
-        mContactsHelper.setLocale(this, mCurrentLocale);
-        prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit();
-
-        LegacyContactImporter importer = getLegacyContactImporter();
-        if (importLegacyContacts(importer)) {
-            onLegacyContactImportSuccess();
-        } else {
-            onLegacyContactImportFailure();
-        }
-    }
-
-    /**
-     * Unlocks the provider and declares that the import process is complete.
-     */
-    private void onLegacyContactImportSuccess() {
-        NotificationManager nm =
-            (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE);
-        nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION);
-
-        // Store a property in the database indicating that the conversion process succeeded
-        mContactsHelper.setProperty(PROPERTY_CONTACTS_IMPORTED,
-                String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION));
-        setProviderStatus(ProviderStatus.STATUS_NORMAL);
-        Log.v(TAG, "Completed import of legacy contacts");
-    }
-
-    /**
-     * Announces the provider status and keeps the provider locked.
-     */
-    private void onLegacyContactImportFailure() {
-        Context context = getContext();
-        NotificationManager nm =
-            (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
-
-        // Show a notification
-        Notification n = new Notification(android.R.drawable.stat_notify_error,
-                context.getString(R.string.upgrade_out_of_memory_notification_ticker),
-                System.currentTimeMillis());
-        n.setLatestEventInfo(context,
-                context.getString(R.string.upgrade_out_of_memory_notification_title),
-                context.getString(R.string.upgrade_out_of_memory_notification_text),
-                PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0));
-        n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
-
-        nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n);
-
-        setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY);
-        Log.v(TAG, "Failed to import legacy contacts");
-
-        // Do not let any database changes until this issue is resolved.
-        mOkToOpenAccess = false;
-    }
-
-    /* Visible for testing */
-    /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
-        boolean aggregatorEnabled = mContactAggregator.isEnabled();
-        mContactAggregator.setEnabled(false);
-        try {
-            if (importer.importContacts()) {
-
-                // TODO aggregate all newly added raw contacts
-                mContactAggregator.setEnabled(aggregatorEnabled);
-                return true;
-            }
-        } catch (Throwable e) {
-           Log.e(TAG, "Legacy contact import failed", e);
-        }
-        mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement();
-        return false;
-    }
-
     /**
      * Wipes all data from the contacts database.
      */
-    /* package */ void wipeData() {
+    @NeededForTesting
+    void wipeData() {
+        invalidateFastScrollingIndexCache();
         mContactsHelper.wipeData();
         mProfileHelper.wipeData();
         mContactsPhotoStore.clear();
@@ -2082,18 +1968,11 @@
     @Override
     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         if (mWriteAccessLatch != null) {
-            // We are stuck trying to upgrade contacts db.  The only update request
-            // allowed in this case is an update of provider status, which will trigger
-            // an attempt to upgrade contacts again.
+            // Update on PROVIDER_STATUS used to be used as a trigger to re-start legacy contact
+            // import.  Now that we no longer support it, we just ignore it.
             int match = sUriMatcher.match(uri);
             if (match == PROVIDER_STATUS) {
-                Integer newStatus = values.getAsInteger(ProviderStatus.STATUS);
-                if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) {
-                    scheduleBackgroundTask(BACKGROUND_TASK_IMPORT_LEGACY_CONTACTS);
-                    return 1;
-                } else {
-                    return 0;
-                }
+                return 0;
             }
         }
         waitForAccess(mWriteAccessLatch);
@@ -2258,6 +2137,9 @@
         if (mVisibleTouched) {
             mVisibleTouched = false;
             mDbHelper.get().updateAllVisible();
+
+            // Need to rebuild the fast-indxer bundle.
+            invalidateFastScrollingIndexCache();
         }
 
         updateSearchIndexInTransaction();
@@ -2400,6 +2282,7 @@
                 break;
 
             case CONTACTS: {
+                invalidateFastScrollingIndexCache();
                 insertContact(values);
                 break;
             }
@@ -2411,14 +2294,16 @@
 
             case RAW_CONTACTS:
             case PROFILE_RAW_CONTACTS: {
+                invalidateFastScrollingIndexCache();
                 id = insertRawContact(uri, values, callerIsSyncAdapter);
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 break;
             }
 
-            case RAW_CONTACTS_DATA:
+            case RAW_CONTACTS_ID_DATA:
             case PROFILE_RAW_CONTACTS_ID_DATA: {
-                int segment = match == RAW_CONTACTS_DATA ? 1 : 2;
+                invalidateFastScrollingIndexCache();
+                int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2;
                 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(segment));
                 id = insertData(values, callerIsSyncAdapter);
                 mSyncToNetwork |= !callerIsSyncAdapter;
@@ -2434,6 +2319,7 @@
 
             case DATA:
             case PROFILE_DATA: {
+                invalidateFastScrollingIndexCache();
                 id = insertData(values, callerIsSyncAdapter);
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 break;
@@ -2569,12 +2455,23 @@
             } else {
                 values.put(RawContacts.DATA_SET, dataSet);
             }
-            accountWithDataSet = new AccountWithDataSet(account.name, account.type, dataSet);
+            accountWithDataSet = AccountWithDataSet.get(account.name, account.type, dataSet);
         }
         return accountWithDataSet;
     }
 
     /**
+     * Same as {@link #resolveAccountWithDataSet}, but returns the account id for the
+     *     {@link AccountWithDataSet}.  Used for insert.
+     *
+     * May update the account cache; must be used only in a transaction.
+     */
+    private long resolveAccountIdInTransaction(Uri uri, ContentValues values) {
+        return mDbHelper.get().getOrCreateAccountIdInTransaction(
+                resolveAccountWithDataSet(uri, mValues));
+    }
+
+    /**
      * Inserts an item in the contacts table
      *
      * @param values the values for the new row
@@ -2597,7 +2494,11 @@
         mValues.putAll(values);
         mValues.putNull(RawContacts.CONTACT_ID);
 
-        AccountWithDataSet accountWithDataSet = resolveAccountWithDataSet(uri, mValues);
+        final long accountId = resolveAccountIdInTransaction(uri, mValues);
+        mValues.remove(RawContacts.ACCOUNT_NAME);
+        mValues.remove(RawContacts.ACCOUNT_TYPE);
+        mValues.remove(RawContacts.DATA_SET);
+        mValues.put(RawContactsColumns.ACCOUNT_ID, accountId);
 
         if (values.containsKey(RawContacts.DELETED)
                 && values.getAsInteger(RawContacts.DELETED) != 0) {
@@ -2613,7 +2514,7 @@
         mAggregator.get().markNewForAggregation(rawContactId, aggregationMode);
 
         // Trigger creation of a Contact based on this RawContact at the end of transaction
-        mTransactionContext.get().rawContactInserted(rawContactId, accountWithDataSet);
+        mTransactionContext.get().rawContactInserted(rawContactId, accountId);
 
         if (!callerIsSyncAdapter) {
             addAutoAddMembership(rawContactId);
@@ -2735,10 +2636,6 @@
 
         long rawContactId = mValues.getAsLong(StreamItems.RAW_CONTACT_ID);
 
-        // Ensure that the raw contact exists and belongs to the caller's account.
-        Account account = resolveAccount(uri, mValues);
-        enforceModifyingAccount(account, rawContactId);
-
         // Don't attempt to insert accounts params - they don't exist in the stream items table.
         mValues.remove(RawContacts.ACCOUNT_NAME);
         mValues.remove(RawContacts.ACCOUNT_TYPE);
@@ -2776,10 +2673,6 @@
         if (streamItemId != 0) {
             long rawContactId = lookupRawContactIdForStreamId(streamItemId);
 
-            // Ensure that the raw contact exists and belongs to the caller's account.
-            Account account = resolveAccount(uri, mValues);
-            enforceModifyingAccount(account, rawContactId);
-
             // Don't attempt to insert accounts params - they don't exist in the stream item
             // photos table.
             mValues.remove(RawContacts.ACCOUNT_NAME);
@@ -2880,104 +2773,6 @@
     }
 
     /**
-     * Checks whether the given raw contact ID is owned by the given account.
-     * If the resolved account is null, this will return true iff the raw contact
-     * is also associated with the "null" account.
-     *
-     * If the resolved account does not match, this will throw a security exception.
-     * @param account The resolved account (may be null).
-     * @param rawContactId The raw contact ID to check for.
-     */
-    private void enforceModifyingAccount(Account account, long rawContactId) {
-        String accountSelection = RawContactsColumns.CONCRETE_ID + "=? AND "
-                + RawContactsColumns.CONCRETE_ACCOUNT_NAME + "=? AND "
-                + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + "=?";
-        String noAccountSelection = RawContactsColumns.CONCRETE_ID + "=? AND "
-                + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " IS NULL AND "
-                + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL";
-        Cursor c;
-        if (account != null) {
-            c = mActiveDb.get().query(Tables.RAW_CONTACTS,
-                    new String[]{RawContactsColumns.CONCRETE_ID}, accountSelection,
-                    new String[]{String.valueOf(rawContactId), mAccount.name, mAccount.type},
-                    null, null, null);
-        } else {
-            c = mActiveDb.get().query(Tables.RAW_CONTACTS,
-                    new String[]{RawContactsColumns.CONCRETE_ID}, noAccountSelection,
-                    new String[]{String.valueOf(rawContactId)},
-                    null, null, null);
-        }
-        try {
-            if(c.getCount() == 0) {
-                throw new SecurityException("Caller account does not match raw contact ID "
-                    + rawContactId);
-            }
-        } finally {
-            c.close();
-        }
-    }
-
-    /**
-     * Checks whether the given selection of stream items matches up with the given
-     * account.  If any of the raw contacts fail the account check, this will throw a
-     * security exception.
-     * @param account The resolved account (may be null).
-     * @param selection The selection.
-     * @param selectionArgs The selection arguments.
-     * @return The list of stream item IDs that would be included in this selection.
-     */
-    private List<Long> enforceModifyingAccountForStreamItems(Account account, String selection,
-            String[] selectionArgs) {
-        List<Long> streamItemIds = Lists.newArrayList();
-        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-        setTablesAndProjectionMapForStreamItems(qb);
-        Cursor c = qb.query(mActiveDb.get(),
-                new String[]{StreamItems._ID, StreamItems.RAW_CONTACT_ID},
-                selection, selectionArgs, null, null, null);
-        try {
-            while (c.moveToNext()) {
-                streamItemIds.add(c.getLong(0));
-
-                // Throw a security exception if the account doesn't match the raw contact's.
-                enforceModifyingAccount(account, c.getLong(1));
-            }
-        } finally {
-            c.close();
-        }
-        return streamItemIds;
-    }
-
-    /**
-     * Checks whether the given selection of stream item photos matches up with the given
-     * account.  If any of the raw contacts fail the account check, this will throw a
-     * security exception.
-     * @param account The resolved account (may be null).
-     * @param selection The selection.
-     * @param selectionArgs The selection arguments.
-     * @return The list of stream item photo IDs that would be included in this selection.
-     */
-    private List<Long> enforceModifyingAccountForStreamItemPhotos(Account account, String selection,
-            String[] selectionArgs) {
-        List<Long> streamItemPhotoIds = Lists.newArrayList();
-        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-        setTablesAndProjectionMapForStreamItemPhotos(qb);
-        Cursor c = qb.query(mActiveDb.get(),
-                new String[]{StreamItemPhotos._ID, StreamItems.RAW_CONTACT_ID},
-                selection, selectionArgs, null, null, null);
-        try {
-            while (c.moveToNext()) {
-                streamItemPhotoIds.add(c.getLong(0));
-
-                // Throw a security exception if the account doesn't match the raw contact's.
-                enforceModifyingAccount(account, c.getLong(1));
-            }
-        } finally {
-            c.close();
-        }
-        return streamItemPhotoIds;
-    }
-
-    /**
      * Queries the database for stream items under the given raw contact.  If there are
      * more entries than {@link ContactsProvider2#MAX_STREAM_ITEMS_PER_RAW_CONTACT},
      * the oldest entries (as determined by timestamp) will be deleted.
@@ -3089,7 +2884,12 @@
         mValues.clear();
         mValues.putAll(values);
 
-        final AccountWithDataSet accountWithDataSet = resolveAccountWithDataSet(uri, mValues);
+        final long accountId = mDbHelper.get().getOrCreateAccountIdInTransaction(
+                resolveAccountWithDataSet(uri, mValues));
+        mValues.remove(Groups.ACCOUNT_NAME);
+        mValues.remove(Groups.ACCOUNT_TYPE);
+        mValues.remove(Groups.DATA_SET);
+        mValues.put(GroupsColumns.ACCOUNT_ID, accountId);
 
         // Replace package with internal mapping
         final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
@@ -3109,35 +2909,12 @@
         long result = mActiveDb.get().insert(Tables.GROUPS, Groups.TITLE, mValues);
 
         if (!callerIsSyncAdapter && isFavoritesGroup) {
-            // add all starred raw contacts to this group
-            String selection;
-            String[] selectionArgs;
-            if (accountWithDataSet == null) {
-                selection = RawContacts.ACCOUNT_NAME + " IS NULL AND "
-                        + RawContacts.ACCOUNT_TYPE + " IS NULL AND "
-                        + RawContacts.DATA_SET + " IS NULL";
-                selectionArgs = null;
-            } else if (accountWithDataSet.getDataSet() == null) {
-                selection = RawContacts.ACCOUNT_NAME + "=? AND "
-                        + RawContacts.ACCOUNT_TYPE + "=? AND "
-                        + RawContacts.DATA_SET + " IS NULL";
-                selectionArgs = new String[] {
-                        accountWithDataSet.getAccountName(),
-                        accountWithDataSet.getAccountType()
-                };
-            } else {
-                selection = RawContacts.ACCOUNT_NAME + "=? AND "
-                        + RawContacts.ACCOUNT_TYPE + "=? AND "
-                        + RawContacts.DATA_SET + "=?";
-                selectionArgs = new String[] {
-                        accountWithDataSet.getAccountName(),
-                        accountWithDataSet.getAccountType(),
-                        accountWithDataSet.getDataSet()
-                };
-            }
+            // If the inserted group is a favorite group, add all starred raw contacts to it.
+            mSelectionArgs1[0] = Long.toString(accountId);
             Cursor c = mActiveDb.get().query(Tables.RAW_CONTACTS,
                     new String[]{RawContacts._ID, RawContacts.STARRED},
-                    selection, selectionArgs, null, null, null);
+                    RawContactsColumns.CONCRETE_ACCOUNT_ID + "=?", mSelectionArgs1,
+                    null, null, null);
             try {
                 while (c.moveToNext()) {
                     if (c.getLong(1) != 0) {
@@ -3175,7 +2952,7 @@
         if (dataSet != null) {
             settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet);
         }
-        Cursor c = queryLocal(settingsUri.build(), null, null, null, null, 0);
+        Cursor c = queryLocal(settingsUri.build(), null, null, null, null, 0, null);
         try {
             if (c.getCount() > 0) {
                 // If a record was found, replace it with the new values.
@@ -3405,7 +3182,7 @@
                     Cursor c = queryLocal(streamUri, new String[]{StreamItems._ID},
                             StreamItems.RAW_CONTACT_ID + "=?",
                             new String[]{String.valueOf(rawContactId)},
-                            null, -1 /* directory ID */);
+                            null, -1 /* directory ID */, null);
                     try {
                         if (c.getCount() > 0) {
                             c.moveToFirst();
@@ -3490,16 +3267,19 @@
             }
 
             case CONTACTS: {
+                invalidateFastScrollingIndexCache();
                 // TODO
                 return 0;
             }
 
             case CONTACTS_ID: {
+                invalidateFastScrollingIndexCache();
                 long contactId = ContentUris.parseId(uri);
                 return deleteContact(contactId, callerIsSyncAdapter);
             }
 
             case CONTACTS_LOOKUP: {
+                invalidateFastScrollingIndexCache();
                 final List<String> pathSegments = uri.getPathSegments();
                 final int segmentCount = pathSegments.size();
                 if (segmentCount < 3) {
@@ -3512,6 +3292,7 @@
             }
 
             case CONTACTS_LOOKUP_ID: {
+                invalidateFastScrollingIndexCache();
                 // lookup contact by id and lookup key to see if they still match the actual record
                 final List<String> pathSegments = uri.getPathSegments();
                 final String lookupKey = pathSegments.get(2);
@@ -3529,7 +3310,7 @@
                 args[1] = Uri.encode(lookupKey);
                 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
                 Cursor c = query(mActiveDb.get(), lookupQb, null, selection, args, null, null,
-                        null);
+                        null, null);
                 try {
                     if (c.getCount() == 1) {
                         // contact was unmodified so go ahead and delete it
@@ -3546,10 +3327,12 @@
 
             case RAW_CONTACTS:
             case PROFILE_RAW_CONTACTS: {
+                invalidateFastScrollingIndexCache();
                 int numDeletes = 0;
-                Cursor c = mActiveDb.get().query(Tables.RAW_CONTACTS,
+                Cursor c = mActiveDb.get().query(Views.RAW_CONTACTS,
                         new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
-                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
+                        appendAccountIdToSelection(uri, selection), selectionArgs,
+                        null, null, null);
                 try {
                     while (c.moveToNext()) {
                         final long rawContactId = c.getLong(0);
@@ -3565,6 +3348,7 @@
 
             case RAW_CONTACTS_ID:
             case PROFILE_RAW_CONTACTS_ID: {
+                invalidateFastScrollingIndexCache();
                 final long rawContactId = ContentUris.parseId(uri);
                 return deleteRawContact(rawContactId, mDbHelper.get().getContactId(rawContactId),
                         callerIsSyncAdapter);
@@ -3572,6 +3356,7 @@
 
             case DATA:
             case PROFILE_DATA: {
+                invalidateFastScrollingIndexCache();
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
                         callerIsSyncAdapter);
@@ -3580,8 +3365,10 @@
             case DATA_ID:
             case PHONES_ID:
             case EMAILS_ID:
+            case CALLABLES_ID:
             case POSTALS_ID:
             case PROFILE_DATA_ID: {
+                invalidateFastScrollingIndexCache();
                 long dataId = ContentUris.parseId(uri);
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 mSelectionArgs1[0] = String.valueOf(dataId);
@@ -3595,8 +3382,9 @@
 
             case GROUPS: {
                 int numDeletes = 0;
-                Cursor c = mActiveDb.get().query(Tables.GROUPS, new String[]{Groups._ID},
-                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
+                Cursor c = mActiveDb.get().query(Views.GROUPS, Projections.ID,
+                        appendAccountIdToSelection(uri, selection), selectionArgs,
+                        null, null, null);
                 try {
                     while (c.moveToNext()) {
                         numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
@@ -3751,16 +3539,12 @@
      * Returns whether the given raw contact ID is local (i.e. has no account associated with it).
      */
     private boolean rawContactIsLocal(long rawContactId) {
-        Cursor c = mActiveDb.get().query(Tables.RAW_CONTACTS,
-                new String[] {
-                        RawContacts.ACCOUNT_NAME,
-                        RawContacts.ACCOUNT_TYPE,
-                        RawContacts.DATA_SET
-                },
-                RawContacts._ID + "=?",
+        Cursor c = mActiveDb.get().query(Tables.RAW_CONTACTS, Projections.LITERAL_ONE,
+                RawContactsColumns.CONCRETE_ID + "=? AND " +
+                RawContactsColumns.ACCOUNT_ID + "=" + Clauses.LOCAL_ACCOUNT_ID,
                 new String[] {String.valueOf(rawContactId)}, null, null, null);
         try {
-            return c.moveToFirst() && c.isNull(0) && c.isNull(1) && c.isNull(2);
+            return c.getCount() > 0;
         } finally {
             c.close();
         }
@@ -3779,23 +3563,21 @@
 
     private int deleteStreamItems(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
-        // First query for the stream items to be deleted, and check that they belong
-        // to the account.
-        Account account = resolveAccount(uri, values);
-        List<Long> streamItemIds = enforceModifyingAccountForStreamItems(
-                account, selection, selectionArgs);
-
-        // If no security exception has been thrown, we're fine to delete.
-        for (long streamItemId : streamItemIds) {
-            deleteStreamItem(streamItemId);
+        int count = 0;
+        final Cursor c = mActiveDb.get().query(Views.STREAM_ITEMS, Projections.ID,
+                selection, selectionArgs, null, null, null);
+        try {
+            c.moveToPosition(-1);
+            while (c.moveToNext()) {
+                count += deleteStreamItem(c.getLong(0));
+            }
+        } finally {
+            c.close();
         }
-
-        mVisibleTouched = true;
-        return streamItemIds.size();
+        return count;
     }
 
     private int deleteStreamItem(long streamItemId) {
-        // Note that this does not enforce the modifying account.
         deleteStreamItemPhotos(streamItemId);
         return mActiveDb.get().delete(Tables.STREAM_ITEMS, StreamItems._ID + "=?",
                 new String[]{String.valueOf(streamItemId)});
@@ -3803,12 +3585,6 @@
 
     private int deleteStreamItemPhotos(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
-        // First query for the stream item photos to be deleted, and check that they
-        // belong to the account.
-        Account account = resolveAccount(uri, values);
-        enforceModifyingAccountForStreamItemPhotos(account, selection, selectionArgs);
-
-        // If no security exception has been thrown, we're fine to delete.
         return mActiveDb.get().delete(Tables.STREAM_ITEM_PHOTOS, selection, selectionArgs);
     }
 
@@ -3881,17 +3657,20 @@
 
             case CONTACTS:
             case PROFILE: {
+                invalidateFastScrollingIndexCache();
                 count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter);
                 break;
             }
 
             case CONTACTS_ID: {
+                invalidateFastScrollingIndexCache();
                 count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter);
                 break;
             }
 
             case CONTACTS_LOOKUP:
             case CONTACTS_LOOKUP_ID: {
+                invalidateFastScrollingIndexCache();
                 final List<String> pathSegments = uri.getPathSegments();
                 final int segmentCount = pathSegments.size();
                 if (segmentCount < 3) {
@@ -3904,9 +3683,10 @@
                 break;
             }
 
-            case RAW_CONTACTS_DATA:
+            case RAW_CONTACTS_ID_DATA:
             case PROFILE_RAW_CONTACTS_ID_DATA: {
-                int segment = match == RAW_CONTACTS_DATA ? 1 : 2;
+                invalidateFastScrollingIndexCache();
+                int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2;
                 final String rawContactId = uri.getPathSegments().get(segment);
                 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
                     + (selection == null ? "" : " AND " + selection);
@@ -3918,6 +3698,7 @@
 
             case DATA:
             case PROFILE_DATA: {
+                invalidateFastScrollingIndexCache();
                 count = updateData(uri, values, appendAccountToSelection(uri, selection),
                         selectionArgs, callerIsSyncAdapter);
                 if (count > 0) {
@@ -3929,7 +3710,9 @@
             case DATA_ID:
             case PHONES_ID:
             case EMAILS_ID:
+            case CALLABLES_ID:
             case POSTALS_ID: {
+                invalidateFastScrollingIndexCache();
                 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
                 if (count > 0) {
                     mSyncToNetwork |= !callerIsSyncAdapter;
@@ -3939,12 +3722,14 @@
 
             case RAW_CONTACTS:
             case PROFILE_RAW_CONTACTS: {
-                selection = appendAccountToSelection(uri, selection);
+                invalidateFastScrollingIndexCache();
+                selection = appendAccountIdToSelection(uri, selection);
                 count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
                 break;
             }
 
             case RAW_CONTACTS_ID: {
+                invalidateFastScrollingIndexCache();
                 long rawContactId = ContentUris.parseId(uri);
                 if (selection != null) {
                     selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
@@ -3960,7 +3745,7 @@
             }
 
             case GROUPS: {
-                count = updateGroups(uri, values, appendAccountToSelection(uri, selection),
+               count = updateGroups(uri, values, appendAccountIdToSelection(uri, selection),
                         selectionArgs, callerIsSyncAdapter);
                 if (count > 0) {
                     mSyncToNetwork |= !callerIsSyncAdapter;
@@ -4094,10 +3879,6 @@
         // Stream items can't be moved to a new raw contact.
         values.remove(StreamItems.RAW_CONTACT_ID);
 
-        // Check that the stream items being updated belong to the account.
-        Account account = resolveAccount(uri, values);
-        enforceModifyingAccountForStreamItems(account, selection, selectionArgs);
-
         // Don't attempt to update accounts params - they don't exist in the stream items table.
         values.remove(RawContacts.ACCOUNT_NAME);
         values.remove(RawContacts.ACCOUNT_TYPE);
@@ -4111,10 +3892,6 @@
         // Stream item photos can't be moved to a new stream item.
         values.remove(StreamItemPhotos.STREAM_ITEM_ID);
 
-        // Check that the stream item photos being updated belong to the account.
-        Account account = resolveAccount(uri, values);
-        enforceModifyingAccountForStreamItemPhotos(account, selection, selectionArgs);
-
         // Don't attempt to update accounts params - they don't exist in the stream item
         // photos table.
         values.remove(RawContacts.ACCOUNT_NAME);
@@ -4164,53 +3941,106 @@
         return mValues;
     }
 
-    private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
-            String[] selectionArgs, boolean callerIsSyncAdapter) {
+    private interface GroupAccountQuery {
+        String TABLE = Views.GROUPS;
 
+        String[] COLUMNS = new String[] {
+                Groups._ID,
+                Groups.ACCOUNT_TYPE,
+                Groups.ACCOUNT_NAME,
+                Groups.DATA_SET,
+        };
+        int ID = 0;
+        int ACCOUNT_TYPE = 1;
+        int ACCOUNT_NAME = 2;
+        int DATA_SET = 3;
+    }
+
+    private int updateGroups(Uri uri, ContentValues originalValues, String selectionWithId,
+            String[] selectionArgs, boolean callerIsSyncAdapter) {
         mGroupIdCache.clear();
 
-        ContentValues updatedValues;
-        if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
-            updatedValues = mValues;
-            updatedValues.clear();
-            updatedValues.putAll(values);
-            updatedValues.put(Groups.DIRTY, 1);
-        } else {
-            updatedValues = values;
-        }
+        final SQLiteDatabase db = mActiveDb.get();
+        final ContactsDatabaseHelper dbHelper = mDbHelper.get();
 
-        int count = mActiveDb.get().update(Tables.GROUPS, updatedValues, selectionWithId,
-                selectionArgs);
+        final ContentValues updatedValues = new ContentValues();
+        updatedValues.putAll(originalValues);
+
+        if (!callerIsSyncAdapter && !updatedValues.containsKey(Groups.DIRTY)) {
+            updatedValues.put(Groups.DIRTY, 1);
+        }
         if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
             mVisibleTouched = true;
         }
 
+        // Prepare for account change
+        final boolean isAccountNameChanging = updatedValues.containsKey(Groups.ACCOUNT_NAME);
+        final boolean isAccountTypeChanging = updatedValues.containsKey(Groups.ACCOUNT_TYPE);
+        final boolean isDataSetChanging = updatedValues.containsKey(Groups.DATA_SET);
+        final boolean isAccountChanging = isAccountNameChanging || isAccountTypeChanging
+                || isDataSetChanging;
+        final String updatedAccountName = updatedValues.getAsString(Groups.ACCOUNT_NAME);
+        final String updatedAccountType = updatedValues.getAsString(Groups.ACCOUNT_TYPE);
+        final String updatedDataSet = updatedValues.getAsString(Groups.DATA_SET);
+
+        updatedValues.remove(Groups.ACCOUNT_NAME);
+        updatedValues.remove(Groups.ACCOUNT_TYPE);
+        updatedValues.remove(Groups.DATA_SET);
+
+        // We later call requestSync() on all affected accounts.
+        final Set<Account> affectedAccounts = Sets.newHashSet();
+
+        // Look for all affected rows, and change them row by row.
+        final Cursor c = db.query(GroupAccountQuery.TABLE, GroupAccountQuery.COLUMNS,
+                selectionWithId, selectionArgs, null, null, null);
+        int returnCount = 0;
+        try {
+            c.moveToPosition(-1);
+            while (c.moveToNext()) {
+                final long groupId = c.getLong(GroupAccountQuery.ID);
+
+                mSelectionArgs1[0] = Long.toString(groupId);
+
+                final String accountName = isAccountNameChanging
+                        ? updatedAccountName : c.getString(GroupAccountQuery.ACCOUNT_NAME);
+                final String accountType = isAccountTypeChanging
+                        ? updatedAccountType : c.getString(GroupAccountQuery.ACCOUNT_TYPE);
+                final String dataSet = isDataSetChanging
+                        ? updatedDataSet : c.getString(GroupAccountQuery.DATA_SET);
+
+                if (isAccountChanging) {
+                    final long accountId = dbHelper.getOrCreateAccountIdInTransaction(
+                            AccountWithDataSet.get(accountName, accountType, dataSet));
+                    updatedValues.put(GroupsColumns.ACCOUNT_ID, accountId);
+                }
+
+                // Finally do the actual update.
+                final int count = db.update(Tables.GROUPS, updatedValues,
+                        GroupsColumns.CONCRETE_ID + "=?", mSelectionArgs1);
+
+                if ((count > 0)
+                        && !TextUtils.isEmpty(accountName)
+                        && !TextUtils.isEmpty(accountType)) {
+                    affectedAccounts.add(new Account(accountName, accountType));
+                }
+
+                returnCount += count;
+            }
+        } finally {
+            c.close();
+        }
+
         // TODO: This will not work for groups that have a data set specified, since the content
         // resolver will not be able to request a sync for the right source (unless it is updated
         // to key off account with data set).
+        // i.e. requestSync only takes Account, not AccountWithDataSet.
         if (updatedValues.containsKey(Groups.SHOULD_SYNC)
                 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
-            Cursor c = mActiveDb.get().query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
-                    Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null,
-                    null, null);
-            String accountName;
-            String accountType;
-            try {
-                while (c.moveToNext()) {
-                    accountName = c.getString(0);
-                    accountType = c.getString(1);
-                    if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
-                        Account account = new Account(accountName, accountType);
-                        ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
-                                new Bundle());
-                        break;
-                    }
-                }
-            } finally {
-                c.close();
+            for (Account account : affectedAccounts) {
+                ContentResolver.requestSync(account, ContactsContract.AUTHORITY, new Bundle());
             }
         }
-        return count;
+        return returnCount;
     }
 
     private int updateSettings(Uri uri, ContentValues values, String selection,
@@ -4236,7 +4066,7 @@
 
         int count = 0;
         Cursor cursor = mActiveDb.get().query(Views.RAW_CONTACTS,
-                new String[] { RawContacts._ID }, selection,
+                Projections.ID, selection,
                 selectionArgs, null, null, null);
         try {
             while (cursor.moveToNext()) {
@@ -4253,27 +4083,68 @@
 
     private int updateRawContact(long rawContactId, ContentValues values,
             boolean callerIsSyncAdapter) {
-        final String selection = RawContacts._ID + " = ?";
+        final String selection = RawContactsColumns.CONCRETE_ID + " = ?";
         mSelectionArgs1[0] = Long.toString(rawContactId);
+
+        final ContactsDatabaseHelper dbHelper = mDbHelper.get();
+
         final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
                 && values.getAsInteger(RawContacts.DELETED) == 0);
+
+        final boolean isAccountNameChanging = values.containsKey(RawContacts.ACCOUNT_NAME);
+        final boolean isAccountTypeChanging = values.containsKey(RawContacts.ACCOUNT_TYPE);
+        final boolean isDataSetChanging = values.containsKey(RawContacts.DATA_SET);
+        final boolean isAccountChanging = isAccountNameChanging || isAccountTypeChanging
+                || isDataSetChanging;
+
         int previousDeleted = 0;
-        String accountType = null;
-        String accountName = null;
-        String dataSet = null;
-        if (requestUndoDelete) {
+        long accountId = 0;
+        String oldAccountType = null;
+        String oldAccountName = null;
+        String oldDataSet = null;
+
+        if (requestUndoDelete || isAccountChanging) {
             Cursor cursor = mActiveDb.get().query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
                     selection, mSelectionArgs1, null, null, null);
             try {
                 if (cursor.moveToFirst()) {
                     previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
-                    accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
-                    accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
-                    dataSet = cursor.getString(RawContactsQuery.DATA_SET);
+                    accountId = cursor.getLong(RawContactsQuery.ACCOUNT_ID);
+                    oldAccountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
+                    oldAccountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
+                    oldDataSet = cursor.getString(RawContactsQuery.DATA_SET);
                 }
             } finally {
                 cursor.close();
             }
+            if (isAccountChanging) {
+                // We can't change the original ContentValues, as it'll be re-used over all
+                // updateRawContact invocations in a transaction, so we need to create a new one.
+                // (However we don't want to use mValues here, because mValues may be used in some
+                // other methods that are called by this method.)
+                final ContentValues originalValues = values;
+                values = new ContentValues();
+                values.clear();
+                values.putAll(originalValues);
+
+                final AccountWithDataSet newAccountWithDataSet = AccountWithDataSet.get(
+                        isAccountNameChanging
+                            ? values.getAsString(RawContacts.ACCOUNT_NAME) : oldAccountName,
+                        isAccountTypeChanging
+                            ? values.getAsString(RawContacts.ACCOUNT_TYPE) : oldAccountType,
+                        isDataSetChanging
+                            ? values.getAsString(RawContacts.DATA_SET) : oldDataSet
+                        );
+                accountId = dbHelper.getOrCreateAccountIdInTransaction(newAccountWithDataSet);
+
+                values.put(RawContactsColumns.ACCOUNT_ID, accountId);
+
+                values.remove(RawContacts.ACCOUNT_NAME);
+                values.remove(RawContacts.ACCOUNT_TYPE);
+                values.remove(RawContacts.DATA_SET);
+            }
+        }
+        if (requestUndoDelete) {
             values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
                     ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
         }
@@ -4300,7 +4171,7 @@
                 // favorites group membership based on whether or not this contact is starred.
                 // If it is starred, add a group membership, if one doesn't already exist
                 // otherwise delete any matching group memberships.
-                if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
+                if (!callerIsSyncAdapter && isAccountChanging) {
                     boolean starred = 0 != DatabaseUtils.longForQuery(mActiveDb.get(),
                             SELECTION_STARRED_FROM_RAW_CONTACTS,
                             new String[]{Long.toString(rawContactId)});
@@ -4310,7 +4181,7 @@
 
             // if this raw contact is being associated with an account, then add a
             // group membership to the group marked as AutoAdd, if any.
-            if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
+            if (!callerIsSyncAdapter && isAccountChanging) {
                 addAutoAddMembership(rawContactId);
             }
 
@@ -4327,8 +4198,11 @@
                 mAggregator.get().updateDisplayNameForRawContact(mActiveDb.get(), rawContactId);
             }
             if (requestUndoDelete && previousDeleted == 1) {
-                mTransactionContext.get().rawContactInserted(rawContactId,
-                        new AccountWithDataSet(accountName, accountType, dataSet));
+                // Note before the accounts refactoring, we used to use the *old* account here,
+                // which doesn't make sense, so now we pass the *new* account.
+                // (In practice it doesn't matter because there's probably no apps that undo-delete
+                // and change accounts at the same time.)
+                mTransactionContext.get().rawContactInserted(rawContactId, accountId);
             }
         }
         return count;
@@ -4359,7 +4233,7 @@
         // so we don't need to worry about updating data we don't have permission to read.
         Cursor c = queryLocal(uri,
                 DataRowHandler.DataUpdateQuery.COLUMNS,
-                selection, selectionArgs, null, -1 /* directory ID */);
+                selection, selectionArgs, null, -1 /* directory ID */, null);
         try {
             while(c.moveToNext()) {
                 count += updateData(mValues, c, callerIsSyncAdapter);
@@ -4523,10 +4397,80 @@
         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
     }
 
-    protected boolean updateAccountsInBackground(Account[] accounts) {
-        // TODO : Check the unit test.
-        boolean accountsChanged = false;
-        SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
+    private static final String ACCOUNT_STRING_SEPARATOR_OUTER = "\u0001";
+    private static final String ACCOUNT_STRING_SEPARATOR_INNER = "\u0002";
+
+    /** return serialized version of {@code accounts} */
+    @VisibleForTesting
+    static String accountsToString(Set<Account> accounts) {
+        final StringBuilder sb = new StringBuilder();
+        for (Account account : accounts) {
+            if (sb.length() > 0) {
+                sb.append(ACCOUNT_STRING_SEPARATOR_OUTER);
+            }
+            sb.append(account.name);
+            sb.append(ACCOUNT_STRING_SEPARATOR_INNER);
+            sb.append(account.type);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * de-serialize string returned by {@link #accountsToString} and return it.
+     * If {@code accountsString} is malformed it'll throw {@link IllegalArgumentException}.
+     */
+    @VisibleForTesting
+    static Set<Account> stringToAccounts(String accountsString) {
+        final Set<Account> ret = Sets.newHashSet();
+        if (accountsString.length() == 0) return ret; // no accounts
+        try {
+            for (String accountString : accountsString.split(ACCOUNT_STRING_SEPARATOR_OUTER)) {
+                String[] nameAndType = accountString.split(ACCOUNT_STRING_SEPARATOR_INNER);
+                ret.add(new Account(nameAndType[0], nameAndType[1]));
+            }
+            return ret;
+        } catch (RuntimeException ex) {
+            throw new IllegalArgumentException("Malformed string", ex);
+        }
+    }
+
+    /**
+     * @return {@code true} if the given {@code currentSystemAccounts} are different from the
+     *    accounts we know, which are stored in the {@link DbProperties#KNOWN_ACCOUNTS} property.
+     */
+    @VisibleForTesting
+    boolean haveAccountsChanged(Account[] currentSystemAccounts) {
+        final ContactsDatabaseHelper dbHelper = mDbHelper.get();
+        final Set<Account> knownAccountSet;
+        try {
+            knownAccountSet = stringToAccounts(
+                    dbHelper.getProperty(DbProperties.KNOWN_ACCOUNTS, ""));
+        } catch (IllegalArgumentException e) {
+            // Failed to get the last known accounts for an unknown reason.  Let's just
+            // treat as if accounts have changed.
+            return true;
+        }
+        final Set<Account> currentAccounts = Sets.newHashSet(currentSystemAccounts);
+        return !knownAccountSet.equals(currentAccounts);
+    }
+
+    @VisibleForTesting
+    void saveAccounts(Account[] systemAccounts) {
+        final ContactsDatabaseHelper dbHelper = mDbHelper.get();
+        dbHelper.setProperty(DbProperties.KNOWN_ACCOUNTS,
+                accountsToString(Sets.newHashSet(systemAccounts)));
+    }
+
+    private boolean updateAccountsInBackground(Account[] systemAccounts) {
+        if (!haveAccountsChanged(systemAccounts)) {
+            return false;
+        }
+        Log.i(TAG, "Accounts changed");
+
+        invalidateFastScrollingIndexCache();
+
+        final ContactsDatabaseHelper dbHelper = mDbHelper.get();
+        final SQLiteDatabase db = dbHelper.getWritableDatabase();
         mActiveDb.set(db);
         db.beginTransaction();
 
@@ -4534,117 +4478,68 @@
         // absolutely imperative that no calls be made inside the following try block that can
         // interact with the contacts DB.  Otherwise it is quite possible for a deadlock to occur.
         try {
-            Set<AccountWithDataSet> existingAccountsWithDataSets =
-                    findValidAccountsWithDataSets(Tables.ACCOUNTS);
+            // First, remove stale rows from raw_contacts, groups, and related tables.
 
-            // Add a row to the ACCOUNTS table (with no data set) for each new account.
-            for (Account account : accounts) {
-                AccountWithDataSet accountWithDataSet = new AccountWithDataSet(
-                        account.name, account.type, null);
-                if (!existingAccountsWithDataSets.contains(accountWithDataSet)) {
-                    accountsChanged = true;
+            // All accounts that are used in raw_contacts and/or groups.
+            final Set<AccountWithDataSet> knownAccountsWithDataSets
+                    = dbHelper.getAllAccountsWithDataSets();
 
-                    // Add an account entry with an empty data set to match the account.
-                    db.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
-                            + ", " + RawContacts.ACCOUNT_TYPE + ", " + RawContacts.DATA_SET
-                            + ") VALUES (?, ?, ?)",
-                            new String[] {
-                                    accountWithDataSet.getAccountName(),
-                                    accountWithDataSet.getAccountType(),
-                                    accountWithDataSet.getDataSet()
-                            });
+            // Find the accounts that have been removed.
+            final List<AccountWithDataSet> accountsWithDataSetsToDelete = Lists.newArrayList();
+            for (AccountWithDataSet knownAccountWithDataSet : knownAccountsWithDataSets) {
+                if (knownAccountWithDataSet.isLocalAccount()
+                        || knownAccountWithDataSet.inSystemAccounts(systemAccounts)) {
+                    continue;
                 }
-            }
-
-            // Check each of the existing sub-accounts against the account list.  If the owning
-            // account no longer exists, the sub-account and all its data should be deleted.
-            List<AccountWithDataSet> accountsWithDataSetsToDelete =
-                    new ArrayList<AccountWithDataSet>();
-            List<Account> accountList = Arrays.asList(accounts);
-            for (AccountWithDataSet accountWithDataSet : existingAccountsWithDataSets) {
-                Account owningAccount = new Account(
-                        accountWithDataSet.getAccountName(), accountWithDataSet.getAccountType());
-                if (!accountList.contains(owningAccount)) {
-                    accountsWithDataSetsToDelete.add(accountWithDataSet);
-                }
+                accountsWithDataSetsToDelete.add(knownAccountWithDataSet);
             }
 
             if (!accountsWithDataSetsToDelete.isEmpty()) {
-                accountsChanged = true;
                 for (AccountWithDataSet accountWithDataSet : accountsWithDataSetsToDelete) {
                     Log.d(TAG, "removing data for removed account " + accountWithDataSet);
-                    String[] accountParams = new String[] {
-                            accountWithDataSet.getAccountName(),
-                            accountWithDataSet.getAccountType()
-                    };
-                    String[] accountWithDataSetParams = accountWithDataSet.getDataSet() == null
-                            ? accountParams
-                            : new String[] {
-                                    accountWithDataSet.getAccountName(),
-                                    accountWithDataSet.getAccountType(),
-                                    accountWithDataSet.getDataSet()
-                            };
-                    String groupsDataSetClause = " AND " + Groups.DATA_SET
-                            + (accountWithDataSet.getDataSet() == null ? " IS NULL" : " = ?");
-                    String rawContactsDataSetClause = " AND " + RawContacts.DATA_SET
-                            + (accountWithDataSet.getDataSet() == null ? " IS NULL" : " = ?");
-                    String settingsDataSetClause = " AND " + Settings.DATA_SET
-                            + (accountWithDataSet.getDataSet() == null ? " IS NULL" : " = ?");
+                    final Long accountIdOrNull = dbHelper.getAccountIdOrNull(accountWithDataSet);
 
-                    db.execSQL(
-                            "DELETE FROM " + Tables.GROUPS +
-                            " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
-                                    " AND " + Groups.ACCOUNT_TYPE + " = ?" +
-                                    groupsDataSetClause, accountWithDataSetParams);
-                    db.execSQL(
-                            "DELETE FROM " + Tables.PRESENCE +
-                            " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
-                                    "SELECT " + RawContacts._ID +
-                                    " FROM " + Tables.RAW_CONTACTS +
-                                    " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
-                                    " AND " + RawContacts.ACCOUNT_TYPE + " = ?" +
-                                    rawContactsDataSetClause + ")", accountWithDataSetParams);
-                    db.execSQL(
-                            "DELETE FROM " + Tables.STREAM_ITEM_PHOTOS +
-                            " WHERE " + StreamItemPhotos.STREAM_ITEM_ID + " IN (" +
-                                    "SELECT " + StreamItems._ID +
-                                    " FROM " + Tables.STREAM_ITEMS +
-                                    " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
-                                            "SELECT " + RawContacts._ID +
-                                            " FROM " + Tables.RAW_CONTACTS +
-                                            " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
-                                            " AND " + RawContacts.ACCOUNT_TYPE + " = ?" +
-                                            rawContactsDataSetClause + "))",
-                            accountWithDataSetParams);
-                    db.execSQL(
-                            "DELETE FROM " + Tables.STREAM_ITEMS +
-                            " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
-                                    "SELECT " + RawContacts._ID +
-                                    " FROM " + Tables.RAW_CONTACTS +
-                                    " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
-                                    " AND " + RawContacts.ACCOUNT_TYPE + " = ?" +
-                                    rawContactsDataSetClause + ")",
-                            accountWithDataSetParams);
-                    db.execSQL(
-                            "DELETE FROM " + Tables.RAW_CONTACTS +
-                            " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
-                            " AND " + RawContacts.ACCOUNT_TYPE + " = ?" +
-                            rawContactsDataSetClause, accountWithDataSetParams);
-                    db.execSQL(
-                            "DELETE FROM " + Tables.SETTINGS +
-                            " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
-                            " AND " + Settings.ACCOUNT_TYPE + " = ?" +
-                            settingsDataSetClause, accountWithDataSetParams);
-                    db.execSQL(
-                            "DELETE FROM " + Tables.ACCOUNTS +
-                            " WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
-                            " AND " + RawContacts.ACCOUNT_TYPE + "=?" +
-                            rawContactsDataSetClause, accountWithDataSetParams);
-                    db.execSQL(
-                            "DELETE FROM " + Tables.DIRECTORIES +
-                            " WHERE " + Directory.ACCOUNT_NAME + "=?" +
-                            " AND " + Directory.ACCOUNT_TYPE + "=?", accountParams);
-                    resetDirectoryCache();
+                    // getAccountIdOrNull() really shouldn't return null here, but just in case...
+                    if (accountIdOrNull != null) {
+                        final String[] accountIdParams =
+                                new String[] {Long.toString(accountIdOrNull)};
+                        db.execSQL(
+                                "DELETE FROM " + Tables.GROUPS +
+                                " WHERE " + GroupsColumns.ACCOUNT_ID + " = ?",
+                                accountIdParams);
+                        db.execSQL(
+                                "DELETE FROM " + Tables.PRESENCE +
+                                " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
+                                        "SELECT " + RawContacts._ID +
+                                        " FROM " + Tables.RAW_CONTACTS +
+                                        " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
+                                        accountIdParams);
+                        db.execSQL(
+                                "DELETE FROM " + Tables.STREAM_ITEM_PHOTOS +
+                                " WHERE " + StreamItemPhotos.STREAM_ITEM_ID + " IN (" +
+                                        "SELECT " + StreamItems._ID +
+                                        " FROM " + Tables.STREAM_ITEMS +
+                                        " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
+                                                "SELECT " + RawContacts._ID +
+                                                " FROM " + Tables.RAW_CONTACTS +
+                                                " WHERE " + RawContactsColumns.ACCOUNT_ID + "=?))",
+                                                accountIdParams);
+                        db.execSQL(
+                                "DELETE FROM " + Tables.STREAM_ITEMS +
+                                " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
+                                        "SELECT " + RawContacts._ID +
+                                        " FROM " + Tables.RAW_CONTACTS +
+                                        " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
+                                        accountIdParams);
+                        db.execSQL(
+                                "DELETE FROM " + Tables.RAW_CONTACTS +
+                                " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?",
+                                accountIdParams);
+                        db.execSQL(
+                                "DELETE FROM " + Tables.ACCOUNTS +
+                                " WHERE " + AccountsColumns._ID + "=?",
+                                accountIdParams);
+                    }
                 }
 
                 // Find all aggregated contacts that used to contain the raw contacts
@@ -4672,55 +4567,43 @@
                 for (Long contactId : orphanContactIds) {
                     mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId);
                 }
-                mDbHelper.get().updateAllVisible();
+                dbHelper.updateAllVisible();
 
                 // Don't bother updating the search index if we're in profile mode - there is no
                 // search index for the profile DB, and updating it for the contacts DB in this case
                 // makes no sense and risks a deadlock.
                 if (!inProfileMode()) {
+                    // TODO Fix it.  It only updates index for contacts/raw_contacts that the
+                    // current transaction context knows updated, but here in this method we don't
+                    // update that information, so effectively it's no-op.
+                    // We can probably just schedule BACKGROUND_TASK_UPDATE_SEARCH_INDEX.
+                    // (But make sure it's not scheduled yet. We schedule this task in initialize()
+                    // too.)
                     updateSearchIndexInTransaction();
                 }
             }
 
-            // Now that we've done the account-based additions and subtractions from the Accounts
-            // table, check for raw contacts that have been added with a data set and add Accounts
-            // entries for those if necessary.
-            existingAccountsWithDataSets = findValidAccountsWithDataSets(Tables.ACCOUNTS);
-            Set<AccountWithDataSet> rawContactAccountsWithDataSets =
-                    findValidAccountsWithDataSets(Tables.RAW_CONTACTS);
-            rawContactAccountsWithDataSets.removeAll(existingAccountsWithDataSets);
+            // Second, remove stale rows from Tables.SETTINGS and Tables.DIRECTORIES
+            removeStaleAccountRows(Tables.SETTINGS, Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE,
+                    systemAccounts);
+            removeStaleAccountRows(Tables.DIRECTORIES, Directory.ACCOUNT_NAME,
+                    Directory.ACCOUNT_TYPE, systemAccounts);
 
-            // Any remaining raw contact sub-accounts need to be added to the Accounts table.
-            for (AccountWithDataSet accountWithDataSet : rawContactAccountsWithDataSets) {
-                accountsChanged = true;
+            // Third, remaining tasks that must be done in a transaction.
+            // TODO: Should sync state take data set into consideration?
+            dbHelper.getSyncState().onAccountsChanged(db, systemAccounts);
 
-                // Add an account entry to match the raw contact.
-                db.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
-                        + ", " + RawContacts.ACCOUNT_TYPE + ", " + RawContacts.DATA_SET
-                        + ") VALUES (?, ?, ?)",
-                        new String[] {
-                                accountWithDataSet.getAccountName(),
-                                accountWithDataSet.getAccountType(),
-                                accountWithDataSet.getDataSet()
-                        });
-            }
+            saveAccounts(systemAccounts);
 
-            if (accountsChanged) {
-                // TODO: Should sync state take data set into consideration?
-                mDbHelper.get().getSyncState().onAccountsChanged(db, accounts);
-            }
             db.setTransactionSuccessful();
         } finally {
             db.endTransaction();
         }
         mAccountWritability.clear();
 
-        if (accountsChanged) {
-            updateContactsAccountCount(accounts);
-            updateProviderStatus();
-        }
-
-        return accountsChanged;
+        updateContactsAccountCount(systemAccounts);
+        updateProviderStatus();
+        return true;
     }
 
     private void updateContactsAccountCount(Account[] accounts) {
@@ -4747,31 +4630,47 @@
         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_DIRECTORIES, packageName);
     }
 
-    /**
-     * Finds all distinct account types and data sets present in the specified table.
-     */
-    private Set<AccountWithDataSet> findValidAccountsWithDataSets(String table) {
-        Set<AccountWithDataSet> accountsWithDataSets = new HashSet<AccountWithDataSet>();
-        Cursor c = mActiveDb.get().rawQuery(
-                "SELECT DISTINCT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE +
-                "," + RawContacts.DATA_SET +
+    public void removeStaleAccountRows(String table, String accountNameColumn,
+            String accountTypeColumn, Account[] systemAccounts) {
+        final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
+        final Cursor c = db.rawQuery(
+                "SELECT DISTINCT " + accountNameColumn +
+                "," + accountTypeColumn +
                 " FROM " + table, null);
         try {
+            c.moveToPosition(-1);
             while (c.moveToNext()) {
-                if (!c.isNull(0) && !c.isNull(1)) {
-                    accountsWithDataSets.add(
-                            new AccountWithDataSet(c.getString(0), c.getString(1), c.getString(2)));
+                final AccountWithDataSet accountWithDataSet = AccountWithDataSet.get(
+                        c.getString(0), c.getString(1), null);
+                if (accountWithDataSet.isLocalAccount()
+                        || accountWithDataSet.inSystemAccounts(systemAccounts)) {
+                    // Account still exists.
+                    continue;
                 }
+
+                db.execSQL("DELETE FROM " + table +
+                        " WHERE " + accountNameColumn + "=? AND " +
+                        accountTypeColumn + "=?",
+                        new String[] {accountWithDataSet.getAccountName(),
+                                accountWithDataSet.getAccountType()});
             }
         } finally {
             c.close();
         }
-        return accountsWithDataSets;
     }
 
     @Override
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
+        return query(uri, projection, selection, selectionArgs, sortOrder, null);
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder, CancellationSignal cancellationSignal) {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "query uri=" + uri + " selection=" + selection + " order=" + sortOrder);
+        }
 
         waitForAccess(mReadAccessLatch);
 
@@ -4781,7 +4680,8 @@
         // Query the profile DB if appropriate.
         if (mapsToProfileDb(uri)) {
             switchToProfileMode();
-            return mProfileProvider.query(uri, projection, selection, selectionArgs, sortOrder);
+            return mProfileProvider.query(uri, projection, selection, selectionArgs, sortOrder,
+                    cancellationSignal);
         }
 
         // Otherwise proceed with a normal query against the contacts DB.
@@ -4790,15 +4690,16 @@
         String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY);
         if (directory == null) {
             return addSnippetExtrasToCursor(uri,
-                    queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1));
+                    queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1,
+                    cancellationSignal));
         } else if (directory.equals("0")) {
             return addSnippetExtrasToCursor(uri,
                     queryLocal(uri, projection, selection, selectionArgs, sortOrder,
-                            Directory.DEFAULT));
+                    Directory.DEFAULT, cancellationSignal));
         } else if (directory.equals("1")) {
             return addSnippetExtrasToCursor(uri,
                     queryLocal(uri, projection, selection, selectionArgs, sortOrder,
-                            Directory.LOCAL_INVISIBLE));
+                    Directory.LOCAL_INVISIBLE, cancellationSignal));
         }
 
         DirectoryInfo directoryInfo = getDirectoryAuthority(directory);
@@ -4836,11 +4737,14 @@
             return null;
         }
 
-        CrossProcessCursor crossProcessCursor = getCrossProcessCursor(cursor);
-        if (crossProcessCursor != null) {
-            return addSnippetExtrasToCursor(uri, cursor);
-        } else {
-            return matrixCursorFromCursor(addSnippetExtrasToCursor(uri, cursor));
+        // Load the cursor contents into a memory cursor (backed by a cursor window) and close the
+        // underlying cursor.
+        try {
+            MemoryCursor memCursor = new MemoryCursor(null, cursor.getColumnNames());
+            memCursor.fillFromCursor(cursor);
+            return memCursor;
+        } finally {
+            cursor.close();
         }
     }
 
@@ -4851,23 +4755,7 @@
             return cursor;
         }
 
-        // Parse out snippet arguments for use when snippets are retrieved from the cursor.
-        String[] args = null;
-        String snippetArgs =
-                getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY);
-        if (snippetArgs != null) {
-            args = snippetArgs.split(",");
-        }
-
         String query = uri.getLastPathSegment();
-        String startMatch = args != null && args.length > 0 ? args[0]
-                : DEFAULT_SNIPPET_ARG_START_MATCH;
-        String endMatch = args != null && args.length > 1 ? args[1]
-                : DEFAULT_SNIPPET_ARG_END_MATCH;
-        String ellipsis = args != null && args.length > 2 ? args[2]
-                : DEFAULT_SNIPPET_ARG_ELLIPSIS;
-        int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3])
-                : DEFAULT_SNIPPET_ARG_MAX_TOKENS;
 
         // Snippet data is needed for the snippeting on the client side, so store it in the cursor
         if (cursor instanceof AbstractCursor && deferredSnippetingRequested(uri)){
@@ -4896,31 +4784,6 @@
         return cursor;
     }
 
-    private CrossProcessCursor getCrossProcessCursor(Cursor cursor) {
-        Cursor c = cursor;
-        if (c instanceof CrossProcessCursor) {
-            return (CrossProcessCursor) c;
-        } else if (c instanceof CursorWindow) {
-            return getCrossProcessCursor(((CursorWrapper) c).getWrappedCursor());
-        } else {
-            return null;
-        }
-    }
-
-    public MatrixCursor matrixCursorFromCursor(Cursor cursor) {
-        MatrixCursor newCursor = new MatrixCursor(cursor.getColumnNames());
-        int numColumns = cursor.getColumnCount();
-        String data[] = new String[numColumns];
-        cursor.moveToPosition(-1);
-        while (cursor.moveToNext()) {
-            for (int i = 0; i < numColumns; i++) {
-                data[i] = cursor.getString(i);
-            }
-            newCursor.addRow(data);
-        }
-        return newCursor;
-    }
-
     private static final class DirectoryQuery {
         public static final String[] COLUMNS = new String[] {
                 Directory._ID,
@@ -4971,22 +4834,9 @@
         }
     }
 
-    private boolean hasColumn(String[] projection, String column) {
-        if (projection == null) {
-            return true; // Null projection means "all columns".
-        }
-
-        for (int i = 0; i < projection.length; i++) {
-            if (column.equalsIgnoreCase(projection[i])) return true;
-        }
-        return false;
-    }
-
-    protected Cursor queryLocal(Uri uri, String[] projection, String selection,
-            String[] selectionArgs, String sortOrder, long directoryId) {
-        if (VERBOSE_LOGGING) {
-            Log.v(TAG, "query: " + uri);
-        }
+    protected Cursor queryLocal(final Uri uri, final String[] projection, String selection,
+            String[] selectionArgs, String sortOrder, final long directoryId,
+            final CancellationSignal cancellationSignal) {
 
         // Default active DB to the contacts DB if none has been set.
         if (mActiveDb.get() == null) {
@@ -5010,7 +4860,7 @@
 
             case CONTACTS: {
                 setTablesAndProjectionMapForContacts(qb, uri, projection);
-                appendLocalDirectorySelectionIfNeeded(qb, directoryId);
+                appendLocalDirectoryAndAccountSelectionIfNeeded(qb, directoryId, uri);
                 break;
             }
 
@@ -5039,7 +4889,8 @@
 
                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
-                            Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey);
+                            Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey,
+                            cancellationSignal);
                     if (c != null) {
                         return c;
                     }
@@ -5068,12 +4919,13 @@
                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
                     setTablesAndProjectionMapForData(lookupQb, uri, projection, false);
                     if (match == CONTACTS_LOOKUP_PHOTO || match == CONTACTS_LOOKUP_ID_PHOTO) {
-                        qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
+                        lookupQb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
                     }
                     lookupQb.appendWhere(" AND ");
                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
-                            Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey);
+                            Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey,
+                            cancellationSignal);
                     if (c != null) {
                         return c;
                     }
@@ -5116,7 +4968,8 @@
                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
                             StreamItems.CONTACT_ID, contactId,
-                            StreamItems.CONTACT_LOOKUP_KEY, lookupKey);
+                            StreamItems.CONTACT_LOOKUP_KEY, lookupKey,
+                            cancellationSignal);
                     if (c != null) {
                         return c;
                     }
@@ -5156,11 +5009,14 @@
                 if (uri.getPathSegments().size() > 2) {
                     filterParam = uri.getLastPathSegment();
                 }
+
+                // If the query consists of a single word, we can do snippetizing after-the-fact for
+                // a performance boost.  Otherwise, we can't defer.
+                snippetDeferred = isSingleWordQuery(filterParam)
+                    && deferredSnipRequested && snippetNeeded(projection);
                 setTablesAndProjectionMapForContactsWithSnippet(
                         qb, uri, projection, filterParam, directoryId,
-                        deferredSnipRequested);
-                snippetDeferred = isSingleWordQuery(filterParam) &&
-                        deferredSnipRequested && snippetNeeded(projection);
+                        snippetDeferred);
                 break;
             }
 
@@ -5370,7 +5226,8 @@
                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
                             Contacts.Entity.CONTACT_ID, contactId,
-                            Contacts.Entity.LOOKUP_KEY, lookupKey);
+                            Contacts.Entity.LOOKUP_KEY, lookupKey,
+                            cancellationSignal);
                     if (c != null) {
                         return c;
                     }
@@ -5433,10 +5290,19 @@
                 return cursor;
             }
 
-            case PHONES: {
+            case PHONES:
+            case CALLABLES: {
+                final String mimeTypeIsPhoneExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
+                final String mimeTypeIsSipExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
-                qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + "=" +
-                        mDbHelper.get().getMimeTypeIdForPhone());
+                if (match == CALLABLES) {
+                    qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
+                            ") OR (" + mimeTypeIsSipExpression + "))");
+                } else {
+                    qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
+                }
 
                 final boolean removeDuplicates = readBooleanQueryParameter(
                         uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
@@ -5456,31 +5322,50 @@
                 break;
             }
 
-            case PHONES_ID: {
+            case PHONES_ID:
+            case CALLABLES_ID: {
+                final String mimeTypeIsPhoneExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
+                final String mimeTypeIsSipExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
-                qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
-                        + mDbHelper.get().getMimeTypeIdForPhone());
+                if (match == CALLABLES_ID) {
+                    qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
+                            ") OR (" + mimeTypeIsSipExpression + "))");
+                } else {
+                    qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
+                }
                 qb.appendWhere(" AND " + Data._ID + "=?");
                 break;
             }
 
-            case PHONES_FILTER: {
+            case PHONES_FILTER:
+            case CALLABLES_FILTER: {
+                final String mimeTypeIsPhoneExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
+                final String mimeTypeIsSipExpression =
+                        DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
+
                 String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
                 Integer typeInt = sDataUsageTypeMap.get(typeParam);
                 if (typeInt == null) {
                     typeInt = DataUsageStatColumns.USAGE_TYPE_INT_CALL;
                 }
                 setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt);
-                qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
-                        + mDbHelper.get().getMimeTypeIdForPhone());
+                if (match == CALLABLES_FILTER) {
+                    qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
+                            ") OR (" + mimeTypeIsSipExpression + "))");
+                } else {
+                    qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
+                }
+
                 if (uri.getPathSegments().size() > 2) {
                     String filterParam = uri.getLastPathSegment();
                     StringBuilder sb = new StringBuilder();
                     sb.append(" AND (");
 
                     boolean hasCondition = false;
-                    boolean orNeeded = false;
                     final String ftsMatchQuery = SearchIndexManager.getFtsMatchQuery(
                             filterParam, FtsQueryBuilder.UNSCOPED_NORMALIZING);
                     if (ftsMatchQuery.length() > 0) {
@@ -5493,13 +5378,12 @@
                                 " WHERE " + SearchIndexColumns.NAME + " MATCH '");
                         sb.append(ftsMatchQuery);
                         sb.append("')");
-                        orNeeded = true;
                         hasCondition = true;
                     }
 
                     String number = PhoneNumberUtils.normalizeNumber(filterParam);
                     if (!TextUtils.isEmpty(number)) {
-                        if (orNeeded) {
+                        if (hasCondition) {
                             sb.append(" OR ");
                         }
                         sb.append(Data._ID +
@@ -5511,6 +5395,23 @@
                         hasCondition = true;
                     }
 
+                    if (!TextUtils.isEmpty(filterParam) && match == CALLABLES_FILTER) {
+                        // If the request is via Callable uri, Sip addresses matching the filter
+                        // parameter should be returned.
+                        if (hasCondition) {
+                            sb.append(" OR ");
+                        }
+                        sb.append("(");
+                        sb.append(mimeTypeIsSipExpression);
+                        sb.append(" AND ((" + Data.DATA1 + " LIKE ");
+                        DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
+                        sb.append(") OR (" + Data.DATA1 + " LIKE ");
+                        // Users may want SIP URIs starting from "sip:"
+                        DatabaseUtils.appendEscapedSQLString(sb, "sip:"+ filterParam + '%');
+                        sb.append(")))");
+                        hasCondition = true;
+                    }
+
                     if (!hasCondition) {
                         // If it is neither a phone number nor a name, the query should return
                         // an empty cursor.  Let's ensure that.
@@ -5519,9 +5420,21 @@
                     sb.append(")");
                     qb.appendWhere(sb);
                 }
-                groupBy = "(CASE WHEN " + PhoneColumns.NORMALIZED_NUMBER
-                        + " IS NOT NULL THEN " + PhoneColumns.NORMALIZED_NUMBER
+                if (match == CALLABLES_FILTER) {
+                    // If the row is for a phone number that has a normalized form, we should use
+                    // the normalized one as PHONES_FILTER does, while we shouldn't do that
+                    // if the row is for a sip address.
+                    String isPhoneAndHasNormalized = "("
+                        + mimeTypeIsPhoneExpression + " AND "
+                        + Phone.NORMALIZED_NUMBER + " IS NOT NULL)";
+                    groupBy = "(CASE WHEN " + isPhoneAndHasNormalized
+                        + " THEN " + Phone.NORMALIZED_NUMBER
                         + " ELSE " + Phone.NUMBER + " END), " + RawContacts.CONTACT_ID;
+                } else {
+                    groupBy = "(CASE WHEN " + Phone.NORMALIZED_NUMBER
+                        + " IS NOT NULL THEN " + Phone.NORMALIZED_NUMBER
+                        + " ELSE " + Phone.NUMBER + " END), " + RawContacts.CONTACT_ID;
+                }
                 if (sortOrder == null) {
                     final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri);
                     if (!TextUtils.isEmpty(accountPromotionSortOrder)) {
@@ -5680,9 +5593,9 @@
                 break;
             }
 
-            case RAW_CONTACTS_DATA:
+            case RAW_CONTACTS_ID_DATA:
             case PROFILE_RAW_CONTACTS_ID_DATA: {
-                int segment = match == RAW_CONTACTS_DATA ? 1 : 2;
+                int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2;
                 long rawContactId = Long.parseLong(uri.getPathSegments().get(segment));
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
@@ -5777,7 +5690,7 @@
                     qb.setStrict(true);
                     boolean foundResult = false;
                     Cursor cursor = query(mActiveDb.get(), qb, projection, selection, selectionArgs,
-                            sortOrder, groupBy, limit);
+                            sortOrder, groupBy, limit, cancellationSignal);
                     try {
                         if (cursor.getCount() > 0) {
                             foundResult = true;
@@ -5801,7 +5714,7 @@
             case GROUPS: {
                 qb.setTables(Views.GROUPS);
                 qb.setProjectionMap(sGroupsProjectionMap);
-                appendAccountFromParameter(qb, uri);
+                appendAccountIdFromParameter(qb, uri);
                 break;
             }
 
@@ -5814,18 +5727,19 @@
             }
 
             case GROUPS_SUMMARY: {
-                final boolean returnGroupCountPerAccount =
-                        readBooleanQueryParameter(uri, Groups.PARAM_RETURN_GROUP_COUNT_PER_ACCOUNT,
-                                false);
                 String tables = Views.GROUPS + " AS " + Tables.GROUPS;
-                if (hasColumn(projection, Groups.SUMMARY_COUNT)) {
+                if (ContactsDatabaseHelper.isInProjection(projection, Groups.SUMMARY_COUNT)) {
                     tables = tables + Joins.GROUP_MEMBER_COUNT;
                 }
+                if (ContactsDatabaseHelper.isInProjection(projection,
+                        Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT)) {
+                    // TODO Add join for this column too (and update the projection map)
+                    // TODO Also remove Groups.PARAM_RETURN_GROUP_COUNT_PER_ACCOUNT when it works.
+                    Log.w(TAG, Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT + " is not supported yet");
+                }
                 qb.setTables(tables);
-                qb.setProjectionMap(returnGroupCountPerAccount ?
-                        sGroupsSummaryProjectionMapWithGroupCountPerAccount
-                        : sGroupsSummaryProjectionMap);
-                appendAccountFromParameter(qb, uri);
+                qb.setProjectionMap(sGroupsSummaryProjectionMap);
+                appendAccountIdFromParameter(qb, uri);
                 groupBy = GroupsColumns.CONCRETE_ID;
                 break;
             }
@@ -5925,7 +5839,7 @@
                 break;
             }
 
-            case RAW_CONTACT_ENTITY_ID: {
+            case RAW_CONTACT_ID_ENTITY: {
                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
                 setTablesAndProjectionMapForRawEntities(qb, uri);
                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
@@ -5965,26 +5879,29 @@
 
         Cursor cursor =
                 query(mActiveDb.get(), qb, projection, selection, selectionArgs, sortOrder, groupBy,
-                        limit);
+                limit, cancellationSignal);
+
         if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) {
-            cursor = bundleLetterCountExtras(cursor, mActiveDb.get(), qb, selection,
-                    selectionArgs, sortOrder, addressBookIndexerCountExpression);
+            bundleFastScrollingIndexExtras(cursor, uri, mActiveDb.get(), qb, selection,
+                    selectionArgs, sortOrder, addressBookIndexerCountExpression,
+                    cancellationSignal);
         }
         if (snippetDeferred) {
             cursor = addDeferredSnippetingExtra(cursor);
         }
+
         return cursor;
     }
 
     private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
             String selection, String[] selectionArgs, String sortOrder, String groupBy,
-            String limit) {
+            String limit, CancellationSignal cancellationSignal) {
         if (projection != null && projection.length == 1
                 && BaseColumns._COUNT.equals(projection[0])) {
             qb.setProjectionMap(sCountProjectionMap);
         }
         final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
-                sortOrder, limit);
+                sortOrder, limit, cancellationSignal);
         if (c != null) {
             c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
         }
@@ -6011,12 +5928,14 @@
      * Runs the query with the supplied contact ID and lookup ID.  If the query succeeds,
      * it returns the resulting cursor, otherwise it returns null and the calling
      * method needs to resolve the lookup key and rerun the query.
+     * @param cancellationSignal
      */
     private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb,
             SQLiteDatabase db, Uri uri,
             String[] projection, String selection, String[] selectionArgs,
             String sortOrder, String groupBy, String limit,
-            String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey) {
+            String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey,
+            CancellationSignal cancellationSignal) {
         String[] args;
         if (selectionArgs == null) {
             args = new String[2];
@@ -6028,7 +5947,7 @@
         args[1] = Uri.encode(lookupKey);
         lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?");
         Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
-                groupBy, limit);
+                groupBy, limit, cancellationSignal);
         if (c.getCount() != 0) {
             return c;
         }
@@ -6037,6 +5956,71 @@
         return null;
     }
 
+    private void invalidateFastScrollingIndexCache() {
+        if (VERBOSE_LOGGING) {
+            Log.v(TAG, "invalidatemFastScrollingIndexCache");
+        }
+
+        // FastScrollingIndexCache is thread-safe, no need to synchronize here.
+        mFastScrollingIndexCache.invalidate();
+    }
+
+    /**
+     * Add the "fast scrolling index" bundle, generated by {@link #getFastScrollingIndexExtras},
+     * to a cursor as extras.  It first checks {@link FastScrollingIndexCache} to see if we
+     * already have a cached result.
+     */
+    private void bundleFastScrollingIndexExtras(Cursor cursor, Uri queryUri,
+            final SQLiteDatabase db, SQLiteQueryBuilder qb, String selection,
+            String[] selectionArgs, String sortOrder, String countExpression,
+            CancellationSignal cancellationSignal) {
+        if (!(cursor instanceof AbstractCursor)) {
+            Log.w(TAG, "Unable to bundle extras.  Cursor is not AbstractCursor.");
+            return;
+        }
+        Bundle b;
+        // Note even though FastScrollingIndexCache is thread-safe, we really need to put the
+        // put-get pair in a single synchronized block, so that even if multiple-threads request the
+        // same index at the same time (which actually happens on the phone app) we only execute
+        // the query once.
+        //
+        // This doesn't cause deadlock, because only reader threads get here but not writer
+        // threads.  (Writer threads may call invalidateFastScrollingIndexCache(), but it doesn't
+        // synchronize on mFastScrollingIndexCache)
+        //
+        // All reader and writer threads share the single lock object internally in
+        // FastScrollingIndexCache, but the lock scope is limited within each put(), get() and
+        // invalidate() call, so it won't deadlock.
+
+        // Synchronizing on a non-static field is generally not a good idea, but nobody should
+        // modify mFastScrollingIndexCache once initialized, and it shouldn't be null at this point.
+        synchronized (mFastScrollingIndexCache) {
+            // First, try the cache.
+            mFastScrollingIndexCacheRequestCount++;
+            b = mFastScrollingIndexCache.get(queryUri, selection, selectionArgs, sortOrder,
+                    countExpression);
+
+            if (b == null) {
+                mFastScrollingIndexCacheMissCount++;
+                // Not in the cache.  Generate and put.
+                final long start = System.currentTimeMillis();
+
+                b = getFastScrollingIndexExtras(queryUri, db, qb, selection, selectionArgs,
+                        sortOrder, countExpression, cancellationSignal, getLocale());
+
+                final long end = System.currentTimeMillis();
+                final int time = (int) (end - start);
+                mTotalTimeFastScrollingIndexGenerate += time;
+                if (VERBOSE_LOGGING) {
+                    Log.v(TAG, "getLetterCountExtraBundle took " + time + "ms");
+                }
+                mFastScrollingIndexCache.put(queryUri, selection, selectionArgs, sortOrder,
+                        countExpression, b);
+            }
+        }
+        ((AbstractCursor) cursor).setExtras(b);
+    }
+
     private static final class AddressBookIndexQuery {
         public static final String LETTER = "letter";
         public static final String TITLE = "title";
@@ -6057,16 +6041,13 @@
     }
 
     /**
-     * Computes counts by the address book index titles and adds the resulting tally
-     * to the returned cursor as a bundle of extras.
+     * Computes counts by the address book index titles and returns it as {@link Bundle} which
+     * will be appended to a {@link Cursor} as extras.
      */
-    private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db,
-            SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder,
-            String countExpression) {
-        if (!(cursor instanceof AbstractCursor)) {
-            Log.w(TAG, "Unable to bundle extras.  Cursor is not AbstractCursor.");
-            return cursor;
-        }
+    private static Bundle getFastScrollingIndexExtras(final Uri queryUri, final SQLiteDatabase db,
+            final SQLiteQueryBuilder qb, final String selection, final String[] selectionArgs,
+            final String sortOrder, String countExpression,
+            final CancellationSignal cancellationSignal, final Locale currentLocale) {
         String sortKey;
 
         // The sort order suffix could be something like "DESC".
@@ -6085,7 +6066,6 @@
             sortKey = Contacts.SORT_KEY_PRIMARY;
         }
 
-        String locale = getLocale().toString();
         HashMap<String, String> projectionMap = Maps.newHashMap();
         String sectionHeading = String.format(Locale.US, AddressBookIndexQuery.SECTION_HEADING,
                 sortKey);
@@ -6105,7 +6085,7 @@
          * than Katakana.
          */
         projectionMap.put(AddressBookIndexQuery.TITLE,
-                "GET_PHONEBOOK_INDEX(" + sectionHeading + ",'" + locale + "')"
+                "GET_PHONEBOOK_INDEX(" + sectionHeading + ",'" + currentLocale.toString() + "')"
                         + " AS " + AddressBookIndexQuery.TITLE);
         projectionMap.put(AddressBookIndexQuery.COUNT,
                 "COUNT(" + countExpression + ") AS " + AddressBookIndexQuery.COUNT);
@@ -6113,7 +6093,8 @@
 
         Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs,
                 AddressBookIndexQuery.ORDER_BY, null /* having */,
-                AddressBookIndexQuery.ORDER_BY + sortOrderSuffix);
+                AddressBookIndexQuery.ORDER_BY + sortOrderSuffix,
+                null, cancellationSignal);
 
         try {
             int groupCount = indexCursor.getCount();
@@ -6128,6 +6109,9 @@
             for (int i = 0; i < groupCount; i++) {
                 indexCursor.moveToNext();
                 String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE);
+                if (title == null) {
+                    title = "";
+                }
                 int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
                 if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) {
                     titles[indexCount] = currentTitle = title;
@@ -6147,13 +6131,7 @@
                 System.arraycopy(counts, 0, newCounts, 0, indexCount);
                 counts = newCounts;
             }
-
-            final Bundle bundle = new Bundle();
-            bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles);
-            bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts);
-
-            ((AbstractCursor) cursor).setExtras(bundle);
-            return cursor;
+            return FastScrollingIndexCache.buildExtraBundle(titles, counts);
         } finally {
             indexCursor.close();
         }
@@ -6398,10 +6376,6 @@
         return false;
     }
 
-    public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
-        mAggregator.get().updateLookupKeyForRawContact(db, rawContactId);
-    }
-
     /**
      * Returns the contact ID that is mentioned the highest number of times.
      */
@@ -6477,7 +6451,7 @@
      * contact and joins that with other contacts tables.
      */
     private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri,
-            String[] projection, String filter, long directoryId, boolean deferredSnippeting) {
+            String[] projection, String filter, long directoryId, boolean deferSnippeting) {
 
         StringBuilder sb = new StringBuilder();
         sb.append(Views.CONTACTS);
@@ -6489,7 +6463,7 @@
         if (TextUtils.isEmpty(filter) || (directoryId != -1 && directoryId != Directory.DEFAULT)) {
             sb.append(" JOIN (SELECT NULL AS " + SearchSnippetColumns.SNIPPET + " WHERE 0)");
         } else {
-            appendSearchIndexJoin(sb, uri, projection, filter, deferredSnippeting);
+            appendSearchIndexJoin(sb, uri, projection, filter, deferSnippeting);
         }
         appendContactPresenceJoin(sb, projection, Contacts._ID);
         appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
@@ -6499,7 +6473,7 @@
 
     private void appendSearchIndexJoin(
             StringBuilder sb, Uri uri, String[] projection, String filter,
-            boolean  deferredSnippeting) {
+            boolean  deferSnippeting) {
 
         if (snippetNeeded(projection)) {
             String[] args = null;
@@ -6520,7 +6494,7 @@
 
             appendSearchIndexJoin(
                     sb, filter, true, startMatch, endMatch, ellipsis, maxTokens,
-                    deferredSnippeting);
+                    deferSnippeting);
         } else {
             appendSearchIndexJoin(sb, filter, false, null, null, null, 0, false);
         }
@@ -6528,16 +6502,13 @@
 
     public void appendSearchIndexJoin(StringBuilder sb, String filter,
             boolean snippetNeeded, String startMatch, String endMatch, String ellipsis,
-            int maxTokens, boolean deferredSnippeting) {
+            int maxTokens, boolean deferSnippeting) {
         boolean isEmailAddress = false;
         String emailAddress = null;
         boolean isPhoneNumber = false;
         String phoneNumber = null;
         String numberE164 = null;
 
-        // If the query consists of a single word, we can do snippetizing after-the-fact for a
-        // performance boost.
-        boolean singleTokenSearch = isSingleWordQuery(filter);
 
         if (filter.indexOf('@') != -1) {
             emailAddress = mDbHelper.get().extractAddressFromEmailAddress(filter);
@@ -6557,18 +6528,24 @@
             sb.append(", ");
             if (isEmailAddress) {
                 sb.append("ifnull(");
-                DatabaseUtils.appendEscapedSQLString(sb, startMatch);
-                sb.append("||(SELECT MIN(" + Email.ADDRESS + ")");
+                if (!deferSnippeting) {
+                    // Add the snippet marker only when we're really creating snippet.
+                    DatabaseUtils.appendEscapedSQLString(sb, startMatch);
+                    sb.append("||");
+                }
+                sb.append("(SELECT MIN(" + Email.ADDRESS + ")");
                 sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS);
                 sb.append(" WHERE  " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
                 sb.append("=" + RawContacts.CONTACT_ID + " AND " + Email.ADDRESS + " LIKE ");
                 DatabaseUtils.appendEscapedSQLString(sb, filter + "%");
-                sb.append(")||");
-                DatabaseUtils.appendEscapedSQLString(sb, endMatch);
+                sb.append(")");
+                if (!deferSnippeting) {
+                    sb.append("||");
+                    DatabaseUtils.appendEscapedSQLString(sb, endMatch);
+                }
                 sb.append(",");
 
-                // Optimization for single-token search (do only if requested).
-                if (singleTokenSearch && deferredSnippeting) {
+                if (deferSnippeting) {
                     sb.append(SearchIndexColumns.CONTENT);
                 } else {
                     appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
@@ -6576,8 +6553,12 @@
                 sb.append(")");
             } else if (isPhoneNumber) {
                 sb.append("ifnull(");
-                DatabaseUtils.appendEscapedSQLString(sb, startMatch);
-                sb.append("||(SELECT MIN(" + Phone.NUMBER + ")");
+                if (!deferSnippeting) {
+                    // Add the snippet marker only when we're really creating snippet.
+                    DatabaseUtils.appendEscapedSQLString(sb, startMatch);
+                    sb.append("||");
+                }
+                sb.append("(SELECT MIN(" + Phone.NUMBER + ")");
                 sb.append(" FROM " +
                         Tables.DATA_JOIN_RAW_CONTACTS + " JOIN " + Tables.PHONE_LOOKUP);
                 sb.append(" ON " + DataColumns.CONCRETE_ID);
@@ -6592,12 +6573,14 @@
                     sb.append(numberE164);
                     sb.append("%'");
                 }
-                sb.append(")||");
-                DatabaseUtils.appendEscapedSQLString(sb, endMatch);
+                sb.append(")");
+                if (! deferSnippeting) {
+                    sb.append("||");
+                    DatabaseUtils.appendEscapedSQLString(sb, endMatch);
+                }
                 sb.append(",");
 
-                // Optimization for single-token search (do only if requested).
-                if (singleTokenSearch && deferredSnippeting) {
+                if (deferSnippeting) {
                     sb.append(SearchIndexColumns.CONTENT);
                 } else {
                     appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
@@ -6606,8 +6589,7 @@
             } else {
                 final String normalizedFilter = NameNormalizer.normalize(filter);
                 if (!TextUtils.isEmpty(normalizedFilter)) {
-                    // Optimization for single-token search (do only if requested)..
-                    if (singleTokenSearch && deferredSnippeting) {
+                    if (deferSnippeting) {
                         sb.append(SearchIndexColumns.CONTENT);
                     } else {
                         sb.append("(CASE WHEN EXISTS (SELECT 1 FROM ");
@@ -6696,13 +6678,13 @@
         sb.append(Views.RAW_CONTACTS);
         qb.setTables(sb.toString());
         qb.setProjectionMap(sRawContactsProjectionMap);
-        appendAccountFromParameter(qb, uri);
+        appendAccountIdFromParameter(qb, uri);
     }
 
     private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) {
         qb.setTables(Views.RAW_ENTITIES);
         qb.setProjectionMap(sRawEntityProjectionMap);
-        appendAccountFromParameter(qb, uri);
+        appendAccountIdFromParameter(qb, uri);
     }
 
     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
@@ -6754,7 +6736,7 @@
         }
 
         qb.setProjectionMap(projectionMap);
-        appendAccountFromParameter(qb, uri);
+        appendAccountIdFromParameter(qb, uri);
     }
 
     private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
@@ -6801,7 +6783,7 @@
 
         qb.setTables(sb.toString());
         qb.setProjectionMap(sEntityProjectionMap);
-        appendAccountFromParameter(qb, uri);
+        appendAccountIdFromParameter(qb, uri);
     }
 
     private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection,
@@ -6857,50 +6839,83 @@
         }
     }
 
-    private boolean appendLocalDirectorySelectionIfNeeded(SQLiteQueryBuilder qb, long directoryId) {
+    private void appendLocalDirectoryAndAccountSelectionIfNeeded(SQLiteQueryBuilder qb,
+            long directoryId, Uri uri) {
+        final StringBuilder sb = new StringBuilder();
         if (directoryId == Directory.DEFAULT) {
-            qb.appendWhere(Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY);
-            return true;
+            sb.append("(" + Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY + ")");
         } else if (directoryId == Directory.LOCAL_INVISIBLE){
-            qb.appendWhere(Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY);
-            return true;
+            sb.append("(" + Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY + ")");
+        } else {
+            sb.append("(1)");
         }
-        return false;
+
+        final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri);
+        // Accounts are valid by only checking one parameter, since we've
+        // already ruled out partial accounts.
+        final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName());
+        if (validAccount) {
+            final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet);
+            if (accountId == null) {
+                // No such account.
+                sb.setLength(0);
+                sb.append("(1=2)");
+            } else {
+                sb.append(
+                        " AND (" + Contacts._ID + " IN (" +
+                        "SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS +
+                        " WHERE " + RawContactsColumns.ACCOUNT_ID + "=" + accountId.toString() +
+                        "))");
+            }
+        }
+        qb.appendWhere(sb.toString());
     }
 
     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
-        final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
-        final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
-        final String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
-
-        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
-        if (partialUri) {
-            // Throw when either account is incomplete
-            throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
-                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
-        }
+        final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri);
 
         // Accounts are valid by only checking one parameter, since we've
         // already ruled out partial accounts.
-        final boolean validAccount = !TextUtils.isEmpty(accountName);
+        final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName());
         if (validAccount) {
-            String toAppend = RawContacts.ACCOUNT_NAME + "="
-                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
+            String toAppend = "(" + RawContacts.ACCOUNT_NAME + "="
+                    + DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountName()) + " AND "
                     + RawContacts.ACCOUNT_TYPE + "="
-                    + DatabaseUtils.sqlEscapeString(accountType);
-            if (dataSet == null) {
+                    + DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountType());
+            if (accountWithDataSet.getDataSet() == null) {
                 toAppend += " AND " + RawContacts.DATA_SET + " IS NULL";
             } else {
                 toAppend += " AND " + RawContacts.DATA_SET + "=" +
-                        DatabaseUtils.sqlEscapeString(dataSet);
+                        DatabaseUtils.sqlEscapeString(accountWithDataSet.getDataSet());
             }
+            toAppend += ")";
             qb.appendWhere(toAppend);
         } else {
             qb.appendWhere("1");
         }
     }
 
-    private String appendAccountToSelection(Uri uri, String selection) {
+    private void appendAccountIdFromParameter(SQLiteQueryBuilder qb, Uri uri) {
+        final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri);
+
+        // Accounts are valid by only checking one parameter, since we've
+        // already ruled out partial accounts.
+        final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName());
+        if (validAccount) {
+            final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet);
+            if (accountId == null) {
+                // No such account.
+                qb.appendWhere("(1=2)");
+            } else {
+                qb.appendWhere(
+                        "(" + RawContactsColumns.ACCOUNT_ID + "=" + accountId.toString() + ")");
+            }
+        } else {
+            qb.appendWhere("1");
+        }
+    }
+
+    private AccountWithDataSet getAccountWithDataSetFromUri(Uri uri) {
         final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
         final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
         final String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
@@ -6911,20 +6926,25 @@
             throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
         }
+        return AccountWithDataSet.get(accountName, accountType, dataSet);
+    }
+
+    private String appendAccountToSelection(Uri uri, String selection) {
+        final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri);
 
         // Accounts are valid by only checking one parameter, since we've
         // already ruled out partial accounts.
-        final boolean validAccount = !TextUtils.isEmpty(accountName);
+        final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName());
         if (validAccount) {
-            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
-                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
-                    + RawContacts.ACCOUNT_TYPE + "="
-                    + DatabaseUtils.sqlEscapeString(accountType));
-            if (dataSet == null) {
+            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=");
+            selectionSb.append(DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountName()));
+            selectionSb.append(" AND " + RawContacts.ACCOUNT_TYPE + "=");
+            selectionSb.append(DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountType()));
+            if (accountWithDataSet.getDataSet() == null) {
                 selectionSb.append(" AND " + RawContacts.DATA_SET + " IS NULL");
             } else {
                 selectionSb.append(" AND " + RawContacts.DATA_SET + "=")
-                        .append(DatabaseUtils.sqlEscapeString(dataSet));
+                        .append(DatabaseUtils.sqlEscapeString(accountWithDataSet.getDataSet()));
             }
             if (!TextUtils.isEmpty(selection)) {
                 selectionSb.append(" AND (");
@@ -6937,6 +6957,34 @@
         }
     }
 
+    private String appendAccountIdToSelection(Uri uri, String selection) {
+        final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri);
+
+        // Accounts are valid by only checking one parameter, since we've
+        // already ruled out partial accounts.
+        final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName());
+        if (validAccount) {
+            final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet);
+            if (accountId == null) {
+                // No such account in the accounts table.  This means, there's no rows to be
+                // selected.
+                return "(1=2)";
+            }
+            final StringBuilder selectionSb = new StringBuilder(
+                    RawContactsColumns.ACCOUNT_ID + "=");
+            selectionSb.append(Long.toString(accountId));
+
+            if (!TextUtils.isEmpty(selection)) {
+                selectionSb.append(" AND (");
+                selectionSb.append(selection);
+                selectionSb.append(')');
+            }
+            return selectionSb.toString();
+        } else {
+            return selection;
+        }
+    }
+
     /**
      * Gets the value of the "limit" URI query parameter.
      *
@@ -6964,17 +7012,27 @@
 
     @Override
     public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
-        if (mode.equals("r")) {
-            waitForAccess(mReadAccessLatch);
-        } else {
-            waitForAccess(mWriteAccessLatch);
-        }
-        if (mapsToProfileDb(uri)) {
-            switchToProfileMode();
-            return mProfileProvider.openAssetFile(uri, mode);
-        } else {
-            switchToContactMode();
-            return openAssetFileLocal(uri, mode);
+        boolean success = false;
+        try {
+            if (mode.equals("r")) {
+                waitForAccess(mReadAccessLatch);
+            } else {
+                waitForAccess(mWriteAccessLatch);
+            }
+            final AssetFileDescriptor ret;
+            if (mapsToProfileDb(uri)) {
+                switchToProfileMode();
+                ret = mProfileProvider.openAssetFile(uri, mode);
+            } else {
+                switchToContactMode();
+                ret = openAssetFileLocal(uri, mode);
+            }
+            success = true;
+            return ret;
+        } finally {
+            if (VERBOSE_LOGGING) {
+                Log.v(TAG, "openAssetFile uri=" + uri + " mode=" + mode + " success=" + success);
+            }
         }
     }
 
@@ -7068,7 +7126,7 @@
                     setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
                             projection, null, null, null, null, null,
-                            Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey);
+                            Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey, null);
                     if (c != null) {
                         try {
                             c.moveToFirst();
@@ -7142,7 +7200,7 @@
                 }
             }
 
-            case DISPLAY_PHOTO: {
+            case DISPLAY_PHOTO_ID: {
                 long photoFileId = ContentUris.parseId(uri);
                 if (!mode.equals("r")) {
                     throw new IllegalArgumentException(
@@ -7463,7 +7521,7 @@
             case CONTACTS_LOOKUP_DISPLAY_PHOTO:
             case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO:
             case RAW_CONTACTS_ID_DISPLAY_PHOTO:
-            case DISPLAY_PHOTO:
+            case DISPLAY_PHOTO_ID:
                 return "image/jpeg";
             case RAW_CONTACTS:
             case PROFILE_RAW_CONTACTS:
@@ -7612,19 +7670,35 @@
                     " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))");
     }
 
-    public boolean isPhoneNumber(String filter) {
-        boolean atLeastOneDigit = false;
-        int len = filter.length();
+    public boolean isPhoneNumber(String query) {
+        if (TextUtils.isEmpty(query)) {
+            return false;
+        }
+        // assume a phone number if it has at least 1 digit
+        return countPhoneNumberDigits(query) > 0;
+    }
+
+    /**
+     * Returns the number of digitis in a phone number ignoring special characters such as '-'.
+     * If the string is not a valid phone number, 0 is returned.
+     */
+    public static int countPhoneNumberDigits(String query) {
+        int numDigits = 0;
+        int len = query.length();
         for (int i = 0; i < len; i++) {
-            char c = filter.charAt(i);
-            if (c >= '0' && c <= '9') {
-                atLeastOneDigit = true;
-            } else if (c != '*' && c != '#' && c != '+' && c != 'N' && c != '.' && c != ';'
-                    && c != '-' && c != '(' && c != ')' && c != ' ') {
-                return false;
+            char c = query.charAt(i);
+            if (Character.isDigit(c)) {
+                numDigits ++;
+            } else if (c == '*' || c == '#' || c == 'N' || c == '.' || c == ';'
+                    || c == '-' || c == '(' || c == ')' || c == ' ') {
+                // carry on
+            } else if (c == '+' && numDigits == 0) {
+                // plus before any digits is ok
+            } else {
+                return 0; // not a phone number
             }
         }
-        return atLeastOneDigit;
+        return numDigits;
     }
 
     /**
@@ -7717,7 +7791,7 @@
     /**
      * Returns true if the specified account type and data set is writable.
      */
-    protected boolean isWritableAccountWithDataSet(String accountTypeAndDataSet) {
+    public boolean isWritableAccountWithDataSet(String accountTypeAndDataSet) {
         if (accountTypeAndDataSet == null) {
             return true;
         }
@@ -7836,17 +7910,14 @@
         }
 
         int version = Integer.parseInt(mContactsHelper.getProperty(
-                PROPERTY_AGGREGATION_ALGORITHM, "1"));
+                DbProperties.AGGREGATION_ALGORITHM, "1"));
         return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION;
     }
 
     protected void upgradeAggregationAlgorithmInBackground() {
-        // This upgrade will affect very few contacts, so it can be performed on the
-        // main thread during the initial boot after an OTA
-
         Log.i(TAG, "Upgrading aggregation algorithm");
         int count = 0;
-        long start = SystemClock.currentThreadTimeMillis();
+        final long start = SystemClock.elapsedRealtime();
         SQLiteDatabase db = null;
         try {
             switchToContactMode();
@@ -7858,9 +7929,8 @@
                     new String[]{"r1." + RawContacts._ID},
                     "r1." + RawContacts._ID + "!=r2." + RawContacts._ID +
                     " AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID +
-                    " AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME +
-                    " AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE +
-                    " AND r1." + RawContacts.DATA_SET + "=r2." + RawContacts.DATA_SET,
+                    " AND r1." + RawContactsColumns.ACCOUNT_ID +
+                        "=r2." + RawContactsColumns.ACCOUNT_ID,
                     null, null, null, null, null);
             try {
                 while (cursor.moveToNext()) {
@@ -7875,25 +7945,39 @@
             mContactAggregator.aggregateInTransaction(mTransactionContext.get(), db);
             updateSearchIndexInTransaction();
             db.setTransactionSuccessful();
-            mContactsHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM,
+            mContactsHelper.setProperty(DbProperties.AGGREGATION_ALGORITHM,
                     String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION));
         } finally {
             if (db != null) {
                 db.endTransaction();
             }
-            long end = SystemClock.currentThreadTimeMillis();
+            final long end = SystemClock.elapsedRealtime();
             Log.i(TAG, "Aggregation algorithm upgraded for " + count
                     + " contacts, in " + (end - start) + "ms");
         }
     }
 
-    /* Visible for testing */
+    @VisibleForTesting
     boolean isPhone() {
-        if (!sIsPhoneInitialized) {
-            sIsPhone = new TelephonyManager(getContext()).isVoiceCapable();
-            sIsPhoneInitialized = true;
+        if (!mIsPhoneInitialized) {
+            mIsPhone = new TelephonyManager(getContext()).isVoiceCapable();
+            mIsPhoneInitialized = true;
         }
-        return sIsPhone;
+        return mIsPhone;
+    }
+
+    boolean isVoiceCapable() {
+        // this copied from com.android.phone.PhoneApp.onCreate():
+
+        // "voice capable" flag.
+        // This flag currently comes from a resource (which is
+        // overrideable on a per-product basis):
+        return getContext().getResources()
+                .getBoolean(com.android.internal.R.bool.config_voice_capable);
+        // ...but this might eventually become a PackageManager "system
+        // feature" instead, in which case we'd do something like:
+        // return
+        //   getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_VOICE_CALLS);
     }
 
     private boolean handleDataUsageFeedback(Uri uri) {
@@ -8043,6 +8127,30 @@
      * @return a boolean indicating if a snippet is needed or not.
      */
     private boolean snippetNeeded(String [] projection) {
-        return mDbHelper.get().isInProjection(projection, SearchSnippetColumns.SNIPPET);
+        return ContactsDatabaseHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET);
+    }
+
+    /**
+     * @return the currently active {@link ContactsDatabaseHelper} for the current thread.
+     */
+    @NeededForTesting
+    public ContactsDatabaseHelper getThreadActiveDatabaseHelperForTest() {
+        return mDbHelper.get();
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.print("FastScrollingIndex stats:\n");
+        pw.printf("request=%d  miss=%d (%d%%)  avg time=%dms\n",
+                mFastScrollingIndexCacheRequestCount,
+                mFastScrollingIndexCacheMissCount,
+                safeDiv(mFastScrollingIndexCacheMissCount * 100,
+                        mFastScrollingIndexCacheRequestCount),
+                safeDiv(mTotalTimeFastScrollingIndexGenerate, mFastScrollingIndexCacheMissCount)
+                );
+    }
+
+    private static final long safeDiv(long dividend, long divisor) {
+        return (divisor == 0) ? 0 : dividend / divisor;
     }
 }
diff --git a/src/com/android/providers/contacts/ContactsUpgradeReceiver.java b/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
index 1439c21..e259ffe 100644
--- a/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
+++ b/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
@@ -74,11 +74,9 @@
                     }
                     helper.getWritableDatabase();
                 }
-                helper.close();
 
                 ProfileDatabaseHelper profileHelper = ProfileDatabaseHelper.getInstance(context);
                 profileHelper.getWritableDatabase();
-                helper.close();
 
                 // Log the total time taken for the receiver to perform the operation
                 EventLogTags.writeContactsUpgradeReceiver(System.currentTimeMillis() - startTime);
diff --git a/src/com/android/providers/contacts/CrossProcessCursorWrapper.java b/src/com/android/providers/contacts/CrossProcessCursorWrapper.java
deleted file mode 100644
index 76baa96..0000000
--- a/src/com/android/providers/contacts/CrossProcessCursorWrapper.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.providers.contacts;
-
-import android.database.CrossProcessCursor;
-import android.database.Cursor;
-import android.database.CursorWindow;
-import android.database.CursorWrapper;
-
-/**
- * Cursor wrapper that implements {@link CrossProcessCursor}, but will only behave as such if the
- * cursor it is wrapping is itself a {@link CrossProcessCursor} or another wrapper around the same.
- */
-public class CrossProcessCursorWrapper extends CursorWrapper implements CrossProcessCursor {
-
-    // The cross process cursor.  Only non-null if the wrapped cursor was a cross-process cursor.
-    private final CrossProcessCursor mCrossProcessCursor;
-
-    public CrossProcessCursorWrapper(Cursor cursor) {
-        super(cursor);
-        mCrossProcessCursor = getCrossProcessCursor(cursor);
-    }
-
-    private CrossProcessCursor getCrossProcessCursor(Cursor cursor) {
-        if (cursor instanceof CrossProcessCursor) {
-            return (CrossProcessCursor) cursor;
-        } else if (cursor instanceof CursorWrapper) {
-            return getCrossProcessCursor(((CursorWrapper) cursor).getWrappedCursor());
-        } else {
-            return null;
-        }
-    }
-
-    @Override
-    public void fillWindow(int pos, CursorWindow window) {
-        if (mCrossProcessCursor != null) {
-            mCrossProcessCursor.fillWindow(pos, window);
-        } else {
-            throw new UnsupportedOperationException("Wrapped cursor is not a cross-process cursor");
-        }
-    }
-
-    @Override
-    public CursorWindow getWindow() {
-        if (mCrossProcessCursor != null) {
-            return mCrossProcessCursor.getWindow();
-        } else {
-            throw new UnsupportedOperationException("Wrapped cursor is not a cross-process cursor");
-        }
-    }
-
-    @Override
-    public boolean onMove(int oldPosition, int newPosition) {
-        if (mCrossProcessCursor != null) {
-            return mCrossProcessCursor.onMove(oldPosition, newPosition);
-        } else {
-            throw new UnsupportedOperationException("Wrapped cursor is not a cross-process cursor");
-        }
-    }
-
-}
diff --git a/src/com/android/providers/contacts/DataRowHandler.java b/src/com/android/providers/contacts/DataRowHandler.java
index 32e6954..b98f6c7 100644
--- a/src/com/android/providers/contacts/DataRowHandler.java
+++ b/src/com/android/providers/contacts/DataRowHandler.java
@@ -19,6 +19,7 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 
 import android.content.ContentValues;
 import android.content.Context;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
index ae20309..b717d31 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForCommonDataKind.java
@@ -15,6 +15,8 @@
  */
 package com.android.providers.contacts;
 
+import com.android.providers.contacts.aggregation.ContactAggregator;
+
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java b/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java
index 726516c..0202fd6 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForCustomMimetype.java
@@ -15,6 +15,8 @@
  */
 package com.android.providers.contacts;
 
+import com.android.providers.contacts.aggregation.ContactAggregator;
+
 import android.content.Context;
 
 public class DataRowHandlerForCustomMimetype extends DataRowHandler {
diff --git a/src/com/android/providers/contacts/DataRowHandlerForEmail.java b/src/com/android/providers/contacts/DataRowHandlerForEmail.java
index de4f17a..f1fa941 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForEmail.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForEmail.java
@@ -16,6 +16,7 @@
 package com.android.providers.contacts;
 
 import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 
 import android.content.ContentValues;
 import android.content.Context;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
index 1b7e475..3a4b167 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForGroupMembership.java
@@ -15,12 +15,14 @@
  */
 package com.android.providers.contacts;
 
-import com.android.internal.util.Objects;
 import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Projections;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 import com.android.providers.contacts.ContactsProvider2.GroupIdCacheEntry;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -30,7 +32,6 @@
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
-import android.text.TextUtils;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -45,15 +46,11 @@
 
         String[] COLUMNS = new String[] {
                 RawContacts.DELETED,
-                RawContacts.ACCOUNT_TYPE,
-                RawContacts.ACCOUNT_NAME,
-                RawContacts.DATA_SET,
+                RawContactsColumns.ACCOUNT_ID,
         };
 
         int DELETED = 0;
-        int ACCOUNT_TYPE = 1;
-        int ACCOUNT_NAME = 2;
-        int DATA_SET = 3;
+        int ACCOUNT_ID = 1;
     }
 
     private static final String SELECTION_RAW_CONTACT_ID = RawContacts._ID + "=?";
@@ -170,7 +167,7 @@
         if (containsGroupSourceId) {
             final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
             final long groupId = getOrMakeGroup(db, rawContactId, sourceId,
-                    txContext.getAccountWithDataSetForRawContact(rawContactId));
+                    txContext.getAccountIdOrNullForRawContact(rawContactId));
             values.remove(GroupMembership.GROUP_SOURCE_ID);
             values.put(GroupMembership.GROUP_ROW_ID, groupId);
         }
@@ -178,41 +175,37 @@
 
     /**
      * Returns the group id of the group with sourceId and the same account as rawContactId.
-     * If the group doesn't already exist then it is first created,
+     * If the group doesn't already exist then it is first created.
+     *
      * @param db SQLiteDatabase to use for this operation
-     * @param rawContactId the contact this group is associated with
-     * @param sourceId the sourceIf of the group to query or create
+     * @param rawContactId the raw contact this group is associated with
+     * @param sourceId the source ID of the group to query or create
+     * @param accountIdOrNull the account ID for the raw contact.  If null it'll be queried from
+     *    the raw_contacts table.
      * @return the group id of the existing or created group
      * @throws IllegalArgumentException if the contact is not associated with an account
      * @throws IllegalStateException if a group needs to be created but the creation failed
      */
     private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId,
-            AccountWithDataSet accountWithDataSet) {
+            Long accountIdOrNull) {
 
-        if (accountWithDataSet == null) {
+        if (accountIdOrNull == null) {
             mSelectionArgs1[0] = String.valueOf(rawContactId);
             Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
-                    RawContacts._ID + "=?", mSelectionArgs1, null, null, null);
+                    RawContactsColumns.CONCRETE_ID + "=?", mSelectionArgs1, null, null, null);
             try {
                 if (c.moveToFirst()) {
-                    String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME);
-                    String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
-                    String dataSet = c.getString(RawContactsQuery.DATA_SET);
-                    if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
-                        accountWithDataSet = new AccountWithDataSet(
-                                accountName, accountType, dataSet);
-                    }
+                    accountIdOrNull = c.getLong(RawContactsQuery.ACCOUNT_ID);
                 }
             } finally {
                 c.close();
             }
         }
 
-        if (accountWithDataSet == null) {
-            throw new IllegalArgumentException("if the groupmembership only "
-                    + "has a sourceid the the contact must be associated with "
-                    + "an account");
+        if (accountIdOrNull == null) {
+            throw new IllegalArgumentException("Raw contact not found for _ID=" + rawContactId);
         }
+        final long accountId = accountIdOrNull;
 
         ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId);
         if (entries == null) {
@@ -223,51 +216,30 @@
         int count = entries.size();
         for (int i = 0; i < count; i++) {
             GroupIdCacheEntry entry = entries.get(i);
-            if (entry.accountName.equals(accountWithDataSet.getAccountName())
-                    && entry.accountType.equals(accountWithDataSet.getAccountType())
-                    && Objects.equal(entry.dataSet, accountWithDataSet.getDataSet())) {
+            if (entry.accountId == accountId) {
                 return entry.groupId;
             }
         }
 
         GroupIdCacheEntry entry = new GroupIdCacheEntry();
-        entry.accountName = accountWithDataSet.getAccountName();
-        entry.accountType = accountWithDataSet.getAccountType();
-        entry.dataSet = accountWithDataSet.getDataSet();
+        entry.accountId = accountId;
         entry.sourceId = sourceId;
         entries.add(0, entry);
 
-        // look up the group that contains this sourceId and has the same account name, type, and
-        // data set as the contact refered to by rawContactId
-        Cursor c;
-        if (accountWithDataSet.getDataSet() == null) {
-            c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
+        // look up the group that contains this sourceId and has the same account as the contact
+        // referred to by rawContactId
+        Cursor c = db.query(Tables.GROUPS, Projections.ID,
                 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
-                new String[]{
-                        sourceId,
-                        accountWithDataSet.getAccountName(),
-                        accountWithDataSet.getAccountType()
-                }, null, null, null);
-        } else {
-            c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
-                Clauses.GROUP_HAS_ACCOUNT_AND_DATA_SET_AND_SOURCE_ID,
-                new String[]{
-                        sourceId,
-                        accountWithDataSet.getAccountName(),
-                        accountWithDataSet.getAccountType(),
-                        accountWithDataSet.getDataSet()
-                }, null, null, null);
-        }
+                new String[]{sourceId, Long.toString(accountId)}, null, null, null);
+
         try {
             if (c.moveToFirst()) {
                 entry.groupId = c.getLong(0);
             } else {
                 ContentValues groupValues = new ContentValues();
-                groupValues.put(Groups.ACCOUNT_NAME, accountWithDataSet.getAccountName());
-                groupValues.put(Groups.ACCOUNT_TYPE, accountWithDataSet.getAccountType());
-                groupValues.put(Groups.DATA_SET, accountWithDataSet.getDataSet());
+                groupValues.put(GroupsColumns.ACCOUNT_ID, accountId);
                 groupValues.put(Groups.SOURCE_ID, sourceId);
-                long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
+                long groupId = db.insert(Tables.GROUPS, null, groupValues);
                 if (groupId < 0) {
                     throw new IllegalStateException("unable to create a new group with "
                             + "this sourceid: " + groupValues);
diff --git a/src/com/android/providers/contacts/DataRowHandlerForIm.java b/src/com/android/providers/contacts/DataRowHandlerForIm.java
index 715a26c..009bb89 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForIm.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForIm.java
@@ -16,6 +16,7 @@
 package com.android.providers.contacts;
 
 import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 
 import android.content.ContentValues;
 import android.content.Context;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForNickname.java b/src/com/android/providers/contacts/DataRowHandlerForNickname.java
index efaf3b0..0fec6ee 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForNickname.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForNickname.java
@@ -16,6 +16,7 @@
 package com.android.providers.contacts;
 
 import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 
 import android.content.ContentValues;
 import android.content.Context;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForNote.java b/src/com/android/providers/contacts/DataRowHandlerForNote.java
index 4610cc7..317af1a 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForNote.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForNote.java
@@ -16,6 +16,7 @@
 package com.android.providers.contacts;
 
 import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 
 import android.content.ContentValues;
 import android.content.Context;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
index 0280c3b..7384ccb 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForOrganization.java
@@ -17,6 +17,7 @@
 
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 
 import android.content.ContentValues;
 import android.content.Context;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
index 008d9cf..f6d6d5d 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForPhoneNumber.java
@@ -15,10 +15,10 @@
  */
 package com.android.providers.contacts;
 
-import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -48,7 +48,7 @@
             String numberE164 =
                     PhoneNumberUtils.formatNumberToE164(number, mDbHelper.getCurrentCountryIso());
             if (numberE164 != null) {
-                values.put(PhoneColumns.NORMALIZED_NUMBER, numberE164);
+                values.put(Phone.NORMALIZED_NUMBER, numberE164);
             }
             dataId = super.insert(db, txContext, rawContactId, values);
 
@@ -76,7 +76,7 @@
                         mDbHelper.getCurrentCountryIso());
             }
             if (numberE164 != null) {
-                values.put(PhoneColumns.NORMALIZED_NUMBER, numberE164);
+                values.put(Phone.NORMALIZED_NUMBER, numberE164);
             }
         }
 
diff --git a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
index dcd23cd..bfe1c3d 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForPhoto.java
@@ -15,6 +15,8 @@
  */
 package com.android.providers.contacts;
 
+import com.android.providers.contacts.aggregation.ContactAggregator;
+
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.res.Resources;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
index fd237fd..c84d2e8 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
@@ -16,6 +16,7 @@
 package com.android.providers.contacts;
 
 import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 
 import android.content.ContentValues;
 import android.content.Context;
diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
index d329cb4..6898a43 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredPostal.java
@@ -16,6 +16,7 @@
 package com.android.providers.contacts;
 
 import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 
 import android.content.ContentValues;
 import android.content.Context;
diff --git a/src/com/android/providers/contacts/FastScrollingIndexCache.java b/src/com/android/providers/contacts/FastScrollingIndexCache.java
new file mode 100644
index 0000000..c1c5602
--- /dev/null
+++ b/src/com/android/providers/contacts/FastScrollingIndexCache.java
@@ -0,0 +1,311 @@
+/*
+ * 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.providers.contacts;
+
+import com.google.android.collect.Maps;
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract.ContactCounts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Cache for the "fast scrolling index".
+ *
+ * It's a cache from "keys" and "bundles" (see {@link #mCache} for what they are).  The cache
+ * content is also persisted in the shared preferences, so it'll survive even if the process
+ * is killed or the device reboots.
+ *
+ * All the content will be invalidated when the provider detects an operation that could potentially
+ * change the index.
+ *
+ * There's no maximum number for cached entries.  It's okay because we store keys and values in
+ * a compact form in both the in-memory cache and the preferences.  Also the query in question
+ * (the query for contact lists) has relatively low number of variations.
+ *
+ * This class is thread-safe.
+ */
+public class FastScrollingIndexCache {
+    private static final String TAG = "LetterCountCache";
+
+    @VisibleForTesting
+    static final String PREFERENCE_KEY = "LetterCountCache";
+
+    /**
+     * Separator used for in-memory structure.
+     */
+    private static final String SEPARATOR = "\u0001";
+    private static final Pattern SEPARATOR_PATTERN = Pattern.compile(SEPARATOR);
+
+    /**
+     * Separator used for serializing values for preferences.
+     */
+    private static final String SAVE_SEPARATOR = "\u0002";
+    private static final Pattern SAVE_SEPARATOR_PATTERN = Pattern.compile(SAVE_SEPARATOR);
+
+    private final SharedPreferences mPrefs;
+
+    private boolean mPreferenceLoaded;
+
+    /**
+     * In-memory cache.
+     *
+     * It's essentially a map from keys, which are query parameters passed to {@link #get}, to
+     * values, which are {@link Bundle}s that will be appended to a {@link Cursor} as extras.
+     *
+     * However, in order to save memory, we store stringified keys and values in the cache.
+     * Key strings are generated by {@link #buildCacheKey} and values are generated by
+     * {@link #buildCacheValue}.
+     *
+     * We store those strings joined with {@link #SAVE_SEPARATOR} as the separator when saving
+     * to shared preferences.
+     */
+    private final Map<String, String> mCache = Maps.newHashMap();
+
+    public FastScrollingIndexCache(Context context) {
+        this(PreferenceManager.getDefaultSharedPreferences(context));
+
+        // At this point, the SharedPreferences might just have been generated and may still be
+        // loading from the file, in which case loading from the preferences would be blocked.
+        // To avoid that, we load lazily.
+    }
+
+    @VisibleForTesting
+    FastScrollingIndexCache(SharedPreferences prefs) {
+        mPrefs = prefs;
+    }
+
+    /**
+     * Append a {@link String} to a {@link StringBuilder}.
+     *
+     * Unlike the original {@link StringBuilder#append}, it does *not* append the string "null" if
+     * {@code value} is null.
+     */
+    private static void appendIfNotNull(StringBuilder sb, Object value) {
+        if (value != null) {
+            sb.append(value.toString());
+        }
+    }
+
+    private static String buildCacheKey(Uri queryUri, String selection, String[] selectionArgs,
+            String sortOrder, String countExpression) {
+        final StringBuilder sb = new StringBuilder();
+
+        appendIfNotNull(sb, queryUri);
+        appendIfNotNull(sb, SEPARATOR);
+        appendIfNotNull(sb, selection);
+        appendIfNotNull(sb, SEPARATOR);
+        appendIfNotNull(sb, sortOrder);
+        appendIfNotNull(sb, SEPARATOR);
+        appendIfNotNull(sb, countExpression);
+
+        if (selectionArgs != null) {
+            for (int i = 0; i < selectionArgs.length; i++) {
+                appendIfNotNull(sb, SEPARATOR);
+                appendIfNotNull(sb, selectionArgs[i]);
+            }
+        }
+        return sb.toString();
+    }
+
+    @VisibleForTesting
+    static String buildCacheValue(String[] titles, int[] counts) {
+        final StringBuilder sb = new StringBuilder();
+
+        for (int i = 0; i < titles.length; i++) {
+            if (i > 0) {
+                appendIfNotNull(sb, SEPARATOR);
+            }
+            appendIfNotNull(sb, titles[i]);
+            appendIfNotNull(sb, SEPARATOR);
+            appendIfNotNull(sb, Integer.toString(counts[i]));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Creates and returns a {@link Bundle} that is appended to a {@link Cursor} as extras.
+     */
+    public static final Bundle buildExtraBundle(String[] titles, int[] counts) {
+        Bundle bundle = new Bundle();
+        bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles);
+        bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts);
+        return bundle;
+    }
+
+    @VisibleForTesting
+    static Bundle buildExtraBundleFromValue(String value) {
+        final String[] values;
+        if (TextUtils.isEmpty(value)) {
+            values = new String[0];
+        } else {
+            values = SEPARATOR_PATTERN.split(value);
+        }
+
+        if ((values.length) % 2 != 0) {
+            return null; // malformed
+        }
+
+        try {
+            final int numTitles = values.length / 2;
+            final String[] titles = new String[numTitles];
+            final int[] counts = new int[numTitles];
+
+            for (int i = 0; i < numTitles; i++) {
+                titles[i] = values[i * 2];
+                counts[i] = Integer.parseInt(values[i * 2 + 1]);
+            }
+
+            return buildExtraBundle(titles, counts);
+        } catch (RuntimeException e) {
+            Log.w(TAG, "Failed to parse cached value", e);
+            return null; // malformed
+        }
+    }
+
+    public Bundle get(Uri queryUri, String selection, String[] selectionArgs, String sortOrder,
+            String countExpression) {
+        synchronized (mCache) {
+            ensureLoaded();
+            final String key = buildCacheKey(queryUri, selection, selectionArgs, sortOrder,
+                    countExpression);
+            final String value = mCache.get(key);
+            if (value == null) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "Miss: " + key);
+                }
+                return null;
+            }
+
+            final Bundle b = buildExtraBundleFromValue(value);
+            if (b == null) {
+                // Value was malformed for whatever reason.
+                mCache.remove(key);
+                save();
+            } else {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "Hit:  " + key);
+                }
+            }
+            return b;
+        }
+    }
+
+    /**
+     * Put a {@link Bundle} into the cache.  {@link Bundle} MUST be built with
+     * {@link #buildExtraBundle(String[], int[])}.
+     */
+    public void put(Uri queryUri, String selection, String[] selectionArgs, String sortOrder,
+            String countExpression, Bundle bundle) {
+        synchronized (mCache) {
+            ensureLoaded();
+            final String key = buildCacheKey(queryUri, selection, selectionArgs, sortOrder,
+                    countExpression);
+            mCache.put(key, buildCacheValue(
+                    bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES),
+                    bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)));
+            save();
+
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "Put: " + key);
+            }
+        }
+    }
+
+    public void invalidate() {
+        synchronized (mCache) {
+            mPrefs.edit().remove(PREFERENCE_KEY).apply();
+            mCache.clear();
+            mPreferenceLoaded = true;
+
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "Invalidated");
+            }
+        }
+    }
+
+    /**
+     * Store the cache to the preferences.
+     *
+     * We concatenate all key+value pairs into one string and save it.
+     */
+    private void save() {
+        final StringBuilder sb = new StringBuilder();
+        for (String key : mCache.keySet()) {
+            if (sb.length() > 0) {
+                appendIfNotNull(sb, SAVE_SEPARATOR);
+            }
+            appendIfNotNull(sb, key);
+            appendIfNotNull(sb, SAVE_SEPARATOR);
+            appendIfNotNull(sb, mCache.get(key));
+        }
+        mPrefs.edit().putString(PREFERENCE_KEY, sb.toString()).apply();
+    }
+
+    private void ensureLoaded() {
+        if (mPreferenceLoaded) return;
+
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "Loading...");
+        }
+
+        // Even when we fail to load, don't retry loading again.
+        mPreferenceLoaded = true;
+
+        boolean successfullyLoaded = false;
+        try {
+            final String savedValue = mPrefs.getString(PREFERENCE_KEY, null);
+
+            if (!TextUtils.isEmpty(savedValue)) {
+
+                final String[] keysAndValues = SAVE_SEPARATOR_PATTERN.split(savedValue);
+
+                if ((keysAndValues.length % 2) != 0) {
+                    return; // malformed
+                }
+
+                for (int i = 1; i < keysAndValues.length; i += 2) {
+                    final String key = keysAndValues[i - 1];
+                    final String value = keysAndValues[i];
+
+                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                        Log.v(TAG, "Loaded: " + key);
+                    }
+
+                    mCache.put(key, value);
+                }
+            }
+            successfullyLoaded = true;
+        } catch (RuntimeException e) {
+            Log.w(TAG, "Failed to load from preferences", e);
+            // But don't crash apps!
+        } finally {
+            if (!successfullyLoaded) {
+                invalidate();
+            }
+        }
+    }
+}
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index 5d4d273..70dbd31 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -17,10 +17,13 @@
 package com.android.providers.contacts;
 
 import android.app.SearchManager;
+import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.sqlite.SQLiteDatabase;
+import android.location.Country;
+import android.location.CountryDetector;
 import android.net.Uri;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Email;
@@ -30,40 +33,30 @@
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.SearchSnippetColumns;
 import android.provider.ContactsContract.StatusUpdates;
+import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 
 import com.android.i18n.phonenumbers.PhoneNumberUtil;
-import com.android.i18n.phonenumbers.NumberParseException;
 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 import com.android.providers.contacts.ContactsDatabaseHelper.Views;
 
 import java.util.ArrayList;
-import java.util.Locale;
 
 /**
  * Support for global search integration for Contacts.
  */
 public class GlobalSearchSupport {
 
-    private static final String[] SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS = {
-            "_id",
-            SearchManager.SUGGEST_COLUMN_TEXT_1,
-            SearchManager.SUGGEST_COLUMN_TEXT_2,
-            SearchManager.SUGGEST_COLUMN_ICON_1,
-            SearchManager.SUGGEST_COLUMN_INTENT_DATA,
-            SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
-            SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
-    };
-
-    private static final String[] SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS = {
+    private static final String[] SEARCH_SUGGESTIONS_COLUMNS = {
             "_id",
             SearchManager.SUGGEST_COLUMN_TEXT_1,
             SearchManager.SUGGEST_COLUMN_TEXT_2,
             SearchManager.SUGGEST_COLUMN_ICON_1,
             SearchManager.SUGGEST_COLUMN_ICON_2,
             SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+            SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
             SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
             SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
             SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT,
@@ -113,15 +106,19 @@
         String text2;
         String icon1;
         String icon2;
+        String intentData;
+        String intentAction;
         String filter;
         String lastAccessTime;
 
         @SuppressWarnings({"unchecked"})
         public ArrayList asList(String[] projection) {
-            if (photoUri != null) {
-                icon1 = photoUri.toString();
-            } else {
-                icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture);
+            if (icon1 == null) {
+                if (photoUri != null) {
+                    icon1 = photoUri.toString();
+                } else {
+                    icon1 = String.valueOf(com.android.internal.R.drawable.ic_contact_picture);
+                }
             }
 
             if (presence != -1) {
@@ -130,15 +127,16 @@
 
             ArrayList<Object> list = new ArrayList<Object>();
             if (projection == null) {
-                list.add(contactId);
-                list.add(text1);
-                list.add(text2);
-                list.add(icon1);
-                list.add(icon2);
-                list.add(buildUri());
-                list.add(lookupKey);
-                list.add(filter);
-                list.add(lastAccessTime);
+                list.add(contactId); // _id
+                list.add(text1); // text1
+                list.add(text2); // text2
+                list.add(icon1); // icon1
+                list.add(icon2); // icon2
+                list.add(intentData == null ? buildUri() : intentData); // intent data
+                list.add(intentAction); // intentAction
+                list.add(lookupKey); // shortcut id
+                list.add(filter); // extra data
+                list.add(lastAccessTime); // last access hint
             } else {
                 for (int i = 0; i < projection.length; i++) {
                     addColumnValue(list, projection[i]);
@@ -159,7 +157,7 @@
             } else if (SearchManager.SUGGEST_COLUMN_ICON_2.equals(column)) {
                 list.add(icon2);
             } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA.equals(column)) {
-                list.add(buildUri());
+                list.add(intentData == null ? buildUri() : intentData);
             } else if (SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID.equals(column)) {
                 list.add(lookupKey);
             } else if (SearchManager.SUGGEST_COLUMN_SHORTCUT_ID.equals(column)) {
@@ -176,15 +174,42 @@
         private String buildUri() {
             return Contacts.getLookupUri(contactId, lookupKey).toString();
         }
+
+        public void reset() {
+            contactId = 0;
+            photoUri = null;
+            lookupKey = null;
+            presence = -1;
+            text1 = null;
+            text2 = null;
+            icon1 = null;
+            icon2 = null;
+            intentData = null;
+            intentAction = null;
+            filter = null;
+            lastAccessTime = null;
+        }
     }
 
     private final ContactsProvider2 mContactsProvider;
-    private final PhoneNumberUtil mPhoneNumberUtil;
+    private PhoneNumberUtil mPhoneNumberUtil;
+    private CountryDetector mCountryDetector;
+    private String mSimCountryIso;
 
     @SuppressWarnings("all")
     public GlobalSearchSupport(ContactsProvider2 contactsProvider) {
         mContactsProvider = contactsProvider;
+
         mPhoneNumberUtil = PhoneNumberUtil.getInstance();
+        mCountryDetector = (CountryDetector)
+                mContactsProvider.getContext().getSystemService(Context.COUNTRY_DETECTOR);
+        TelephonyManager telman = (TelephonyManager)
+                mContactsProvider.getContext().getSystemService(Context.TELEPHONY_SERVICE);
+        // assuming here that the SIM never changes while the phone is booted. ok?
+        mSimCountryIso = telman == null ? null : telman.getSimCountryIso();
+        if (mSimCountryIso != null) {
+            mSimCountryIso = mSimCountryIso.toUpperCase();
+        }
 
         // To ensure the data column position. This is dead code if properly configured.
         if (Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
@@ -194,9 +219,39 @@
         }
     }
 
+    private boolean isPossibleByPhoneNumberUtil(String query) {
+        String currentCountry = null;
+        Country current = mCountryDetector.detectCountry();
+        if (current != null) {
+            currentCountry = current.getCountryIso().toUpperCase();
+        }
+        if (currentCountry != null && mPhoneNumberUtil.isPossibleNumber(query, currentCountry)) {
+            return true;
+        }
+        if (mSimCountryIso != null && !TextUtils.equals(currentCountry, mSimCountryIso)) {
+            // use the SIM country if it's different, so we can add contacts for home numbers
+            // while roaming
+            return mPhoneNumberUtil.isPossibleNumber(query, mSimCountryIso);
+        }
+        return false;
+    }
+
+    private boolean isPhoneNumber(String query) {
+        if (TextUtils.isEmpty(query)) {
+            return false;
+        }
+        if (ContactsProvider2.countPhoneNumberDigits(query) > 2) {
+            // 3 or more digits matching the basic pattern
+            return true;
+        }
+        // more advanced check, for 1800-FLOWERS style numbers and the like
+        return isPossibleByPhoneNumberUtil(query);
+    }
+
     public Cursor handleSearchSuggestionsQuery(
             SQLiteDatabase db, Uri uri, String[] projection, String limit) {
         final String searchClause;
+
         final String selection;
         if (uri.getPathSegments().size() <= 1) {
             searchClause = null;
@@ -206,16 +261,13 @@
             selection = null;
         }
 
-        if (mContactsProvider.isPhone() && isPhoneNumber(searchClause)) {
-            return buildCursorForSearchSuggestionsBasedOnPhoneNumber(searchClause);
-        } else {
-            return buildCursorForSearchSuggestionsBasedOnFilter(
-                    db, projection, selection, searchClause, limit);
+        MatrixCursor cursor = new MatrixCursor(
+                projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection);
+        addSearchSuggestionsBasedOnFilter(cursor, db, projection, selection, searchClause, limit);
+        if (isPhoneNumber(searchClause)) {
+            addSearchSuggestionsBasedOnPhoneNumber(cursor, searchClause, projection);
         }
-    }
-
-    private boolean isPhoneNumber(String query) {
-        return mPhoneNumberUtil.isPossibleNumber(query, Locale.getDefault().getCountry());
+        return cursor;
     }
 
     /**
@@ -236,73 +288,61 @@
         } catch (IllegalArgumentException e) {
             contactId = -1L;
         }
-        return buildCursorForSearchSuggestionsBasedOnFilter(
+        MatrixCursor cursor = new MatrixCursor(
+                projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection);
+        return addSearchSuggestionsBasedOnFilter(cursor,
                 db, projection, ContactsColumns.CONCRETE_ID + "=" + contactId, filter, null);
     }
 
-    private boolean isVoiceCapable() {
-        // this copied from com.android.phone.PhoneApp.onCreate():
-
-        // "voice capable" flag.
-        // This flag currently comes from a resource (which is
-        // overrideable on a per-product basis):
-        return mContactsProvider.getContext().getResources()
-                .getBoolean(com.android.internal.R.bool.config_voice_capable);
-        // ...but this might eventually become a PackageManager "system
-        // feature" instead, in which case we'd do something like:
-        // return
-        //   getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_VOICE_CALLS);
-    }
-
-    private Cursor buildCursorForSearchSuggestionsBasedOnPhoneNumber(String searchClause) {
-        MatrixCursor cursor = new MatrixCursor(SEARCH_SUGGESTIONS_BASED_ON_PHONE_NUMBER_COLUMNS);
+    private Cursor addSearchSuggestionsBasedOnPhoneNumber(MatrixCursor cursor,
+            String searchClause, String[] projection) {
         Resources r = mContactsProvider.getContext().getResources();
         String s;
         int i;
 
-        if (isVoiceCapable()) {
-            ArrayList<Object> dialNumber = new ArrayList<Object>();
-            dialNumber.add(0);  // _id
+        if (mContactsProvider.isPhone() && mContactsProvider.isVoiceCapable()) {
+            SearchSuggestion dialNumber = new SearchSuggestion();
+            dialNumber.contactId = -1;
             s = r.getString(com.android.internal.R.string.dial_number_using, searchClause);
             i = s.indexOf('\n');
             if (i < 0) {
-                dialNumber.add(s);
-                dialNumber.add("");
+                dialNumber.text1 = s;
+                dialNumber.text2 = "";
             } else {
-                dialNumber.add(s.substring(0, i));
-                dialNumber.add(s.substring(i + 1));
+                dialNumber.text1 = s.substring(0, i);
+                dialNumber.text2 = s.substring(i + 1);
             }
-            dialNumber.add(String.valueOf(com.android.internal.R.drawable.call_contact));
-            dialNumber.add("tel:" + searchClause);
-            dialNumber.add(ContactsContract.Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
-            dialNumber.add(null);
-            cursor.addRow(dialNumber);
+            dialNumber.icon1 = String.valueOf(com.android.internal.R.drawable.call_contact);
+            dialNumber.intentData = "tel:" + searchClause;
+            dialNumber.intentAction =
+                    ContactsContract.Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED;
+            dialNumber.lookupKey = SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT; // shortcut id
+            cursor.addRow(dialNumber.asList(projection));
         }
 
-        ArrayList<Object> createContact = new ArrayList<Object>();
-        createContact.add(1);  // _id
+        SearchSuggestion createContact = new SearchSuggestion();
+        createContact.contactId = -2;
         s = r.getString(com.android.internal.R.string.create_contact_using, searchClause);
         i = s.indexOf('\n');
         if (i < 0) {
-            createContact.add(s);
-            createContact.add("");
+            createContact.text1 = s;
+            createContact.text2 = "";
         } else {
-            createContact.add(s.substring(0, i));
-            createContact.add(s.substring(i + 1));
+            createContact.text1 = s.substring(0, i);
+            createContact.text2 = s.substring(i + 1);
         }
-        createContact.add(String.valueOf(com.android.internal.R.drawable.create_contact));
-        createContact.add("tel:" + searchClause);
-        createContact.add(ContactsContract.Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
-        createContact.add(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
-        cursor.addRow(createContact);
+        createContact.icon1 = String.valueOf(com.android.internal.R.drawable.create_contact);
+        createContact.intentData = "tel:" + searchClause;
+        createContact.intentAction =
+                ContactsContract.Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED;
+        createContact.lookupKey = SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT; // shortcut id
+        cursor.addRow(createContact.asList(projection));
 
         return cursor;
     }
 
-    private Cursor buildCursorForSearchSuggestionsBasedOnFilter(SQLiteDatabase db,
+    private Cursor addSearchSuggestionsBasedOnFilter(MatrixCursor cursor, SQLiteDatabase db,
             String[] projection, String selection, String filter, String limit) {
-        MatrixCursor cursor = new MatrixCursor(
-                projection != null ? projection : SEARCH_SUGGESTIONS_BASED_ON_NAME_COLUMNS);
         StringBuilder sb = new StringBuilder();
         final boolean haveFilter = !TextUtils.isEmpty(filter);
         sb.append("SELECT "
@@ -345,6 +385,7 @@
                     suggestion.text2 = shortenSnippet(c.getString(6));
                 }
                 cursor.addRow(suggestion.asList(projection));
+                suggestion.reset();
             }
         } finally {
             c.close();
diff --git a/src/com/android/providers/contacts/Hex.java b/src/com/android/providers/contacts/Hex.java
index 991f095..090764a 100644
--- a/src/com/android/providers/contacts/Hex.java
+++ b/src/com/android/providers/contacts/Hex.java
@@ -61,7 +61,7 @@
         int j = 0;
         for (int i = 0; i < array.length; i++) {
             int index = array[i] & 0xFF;
-            if (index == 0 && zeroTerminated) {
+            if (zeroTerminated && index == 0 && i == array.length-1) {
                 break;
             }
 
diff --git a/src/com/android/providers/contacts/InstrumentedCursorWrapper.java b/src/com/android/providers/contacts/InstrumentedCursorWrapper.java
deleted file mode 100644
index c412810..0000000
--- a/src/com/android/providers/contacts/InstrumentedCursorWrapper.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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.providers.contacts;
-
-import com.google.android.collect.Lists;
-
-import android.database.Cursor;
-import android.net.Uri;
-import android.util.Log;
-
-import java.util.List;
-
-/**
- * Cursor wrapper that handles tracking time taken before query result came back and how long
- * the cursor was open before it was closed.
- */
-public class InstrumentedCursorWrapper extends CrossProcessCursorWrapper {
-
-    /**
-     * Static list of active cursors.
-     */
-    private static List<InstrumentedCursorWrapper> mActiveCursors = Lists.newArrayList();
-
-    /**
-     * Time (ms since epoch) when the cursor was created.
-     */
-    private long mCreationTime;
-
-    /**
-     * Milliseconds after creation at which the query completed (triggered by a getCount or
-     * any method that moves the cursor).
-     */
-    private long mTimeToQuery;
-
-    /**
-     * The URI being queried in this cursor.
-     */
-    private Uri mUri;
-
-    /**
-     * Log tag to use.
-     */
-    private String mTag;
-
-    public InstrumentedCursorWrapper(Cursor cursor, Uri uri, String tag) {
-        super(cursor);
-        mCreationTime = System.currentTimeMillis();
-        mUri = uri;
-        mTag = tag;
-        mActiveCursors.add(this);
-    }
-
-    @Override
-    public int getCount() {
-        int count = super.getCount();
-        logQueryTime();
-        return count;
-    }
-
-    @Override
-    public boolean moveToFirst() {
-        boolean result = super.moveToFirst();
-        logQueryTime();
-        return result;
-    }
-
-    @Override
-    public boolean moveToLast() {
-        boolean result = super.moveToLast();
-        logQueryTime();
-        return result;
-    }
-
-    @Override
-    public boolean move(int offset) {
-        boolean result = super.move(offset);
-        logQueryTime();
-        return result;
-    }
-
-    @Override
-    public boolean moveToPosition(int position) {
-        boolean result = super.moveToPosition(position);
-        logQueryTime();
-        return result;
-    }
-
-    @Override
-    public boolean moveToNext() {
-        boolean result = super.moveToNext();
-        logQueryTime();
-        return result;
-    }
-
-    @Override
-    public boolean moveToPrevious() {
-        boolean result = super.moveToPrevious();
-        logQueryTime();
-        return result;
-    }
-
-    @Override
-    public void close() {
-        super.close();
-        long timeToClose = System.currentTimeMillis() - mCreationTime;
-        Log.v(mTag, timeToClose + "ms to close for URI " + mUri
-                + " (" + (timeToClose - mTimeToQuery) + "ms since query complete)");
-        mActiveCursors.remove(this);
-        Log.v(mTag, mActiveCursors.size() + " cursors still open");
-    }
-
-    private void logQueryTime() {
-        if (mTimeToQuery == 0) {
-            mTimeToQuery = System.currentTimeMillis() - mCreationTime;
-            Log.v(mTag, mTimeToQuery + "ms to query URI " + mUri);
-        }
-    }
-}
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
index 0c1cc32..78cbc9d 100644
--- a/src/com/android/providers/contacts/LegacyApiSupport.java
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -15,6 +15,7 @@
  */
 package com.android.providers.contacts;
 
+import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.ExtensionsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
@@ -118,7 +119,9 @@
     private static final int SETTINGS = 44;
 
     private static final String PEOPLE_JOINS =
-            " LEFT OUTER JOIN data name ON (raw_contacts._id = name.raw_contact_id"
+            " JOIN " + Tables.ACCOUNTS + " ON ("
+                + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID + ")"
+            + " LEFT OUTER JOIN data name ON (raw_contacts._id = name.raw_contact_id"
             + " AND (SELECT mimetype FROM mimetypes WHERE mimetypes._id = name.mimetype_id)"
                     + "='" + StructuredName.CONTENT_ITEM_TYPE + "')"
             + " LEFT OUTER JOIN data organization ON (raw_contacts._id = organization.raw_contact_id"
@@ -508,9 +511,9 @@
     private boolean mDefaultAccountKnown;
     private Account mAccount;
 
-    private long mMimetypeEmail;
-    private long mMimetypeIm;
-    private long mMimetypePostal;
+    private final long mMimetypeEmail;
+    private final long mMimetypeIm;
+    private final long mMimetypePostal;
 
 
     public LegacyApiSupport(Context context, ContactsDatabaseHelper contactsDatabaseHelper,
@@ -563,8 +566,8 @@
                         + " AS " + People.PHONETIC_NAME + " , " +
                 "note." + Note.NOTE
                         + " AS " + People.NOTES + ", " +
-                RawContacts.ACCOUNT_NAME + ", " +
-                RawContacts.ACCOUNT_TYPE + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
                 Tables.RAW_CONTACTS + "." + RawContacts.TIMES_CONTACTED
                         + " AS " + People.TIMES_CONTACTED + ", " +
                 Tables.RAW_CONTACTS + "." + RawContacts.LAST_TIME_CONTACTED
@@ -606,8 +609,8 @@
                         + " AS " + android.provider.Contacts.Organizations.PERSON_ID + ", " +
                 Data.IS_PRIMARY
                         + " AS " + android.provider.Contacts.Organizations.ISPRIMARY + ", " +
-                RawContacts.ACCOUNT_NAME + ", " +
-                RawContacts.ACCOUNT_TYPE + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
                 Organization.COMPANY
                         + " AS " + android.provider.Contacts.Organizations.COMPANY + ", " +
                 Organization.TYPE
@@ -680,8 +683,8 @@
                         + " AS " + android.provider.Contacts.Extensions._ID + ", " +
                 DataColumns.CONCRETE_RAW_CONTACT_ID
                         + " AS " + android.provider.Contacts.Extensions.PERSON_ID + ", " +
-                RawContacts.ACCOUNT_NAME + ", " +
-                RawContacts.ACCOUNT_TYPE + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
                 ExtensionsColumns.NAME
                         + " AS " + android.provider.Contacts.Extensions.NAME + ", " +
                 ExtensionsColumns.VALUE
@@ -695,12 +698,14 @@
         db.execSQL("DROP VIEW IF EXISTS " + LegacyTables.GROUPS + ";");
         db.execSQL("CREATE VIEW " + LegacyTables.GROUPS + " AS SELECT " +
                 GroupsColumns.CONCRETE_ID + " AS " + android.provider.Contacts.Groups._ID + ", " +
-                Groups.ACCOUNT_NAME + ", " +
-                Groups.ACCOUNT_TYPE + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
                 Groups.TITLE + " AS " + android.provider.Contacts.Groups.NAME + ", " +
                 Groups.NOTES + " AS " + android.provider.Contacts.Groups.NOTES + " , " +
                 Groups.SYSTEM_ID + " AS " + android.provider.Contacts.Groups.SYSTEM_ID +
                 " FROM " + Tables.GROUPS +
+                " JOIN " + Tables.ACCOUNTS + " ON (" +
+                GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID + ")" +
         ";");
 
         db.execSQL("DROP VIEW IF EXISTS " + LegacyTables.GROUP_MEMBERSHIP + ";");
@@ -709,10 +714,8 @@
                         + " AS " + android.provider.Contacts.GroupMembership._ID + ", " +
                 DataColumns.CONCRETE_RAW_CONTACT_ID
                         + " AS " + android.provider.Contacts.GroupMembership.PERSON_ID + ", " +
-                Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_NAME
-                        + " AS " +  RawContacts.ACCOUNT_NAME + ", " +
-                Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_TYPE
-                        + " AS " +  RawContacts.ACCOUNT_TYPE + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
                 GroupMembership.GROUP_ROW_ID
                         + " AS " + android.provider.Contacts.GroupMembership.GROUP_ID + ", " +
                 Groups.TITLE
@@ -724,10 +727,10 @@
                 GroupsColumns.CONCRETE_SOURCE_ID
                         + " AS "
                         + android.provider.Contacts.GroupMembership.GROUP_SYNC_ID + ", " +
-                GroupsColumns.CONCRETE_ACCOUNT_NAME
+                AccountsColumns.CONCRETE_ACCOUNT_NAME
                         + " AS "
                         + android.provider.Contacts.GroupMembership.GROUP_SYNC_ACCOUNT + ", " +
-                GroupsColumns.CONCRETE_ACCOUNT_TYPE
+                AccountsColumns.CONCRETE_ACCOUNT_TYPE
                         + " AS "
                         + android.provider.Contacts.GroupMembership.GROUP_SYNC_ACCOUNT_TYPE +
                 " FROM " + Tables.DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS +
@@ -742,8 +745,8 @@
                         + " AS " + android.provider.Contacts.Photos._ID + ", " +
                 DataColumns.CONCRETE_RAW_CONTACT_ID
                         + " AS " + android.provider.Contacts.Photos.PERSON_ID + ", " +
-                RawContacts.ACCOUNT_NAME + ", " +
-                RawContacts.ACCOUNT_TYPE + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
+                AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
                 Tables.DATA + "." + Photo.PHOTO
                         + " AS " + android.provider.Contacts.Photos.DATA + ", " +
                 "legacy_photo." + LegacyPhotoData.EXISTS_ON_SERVER
diff --git a/src/com/android/providers/contacts/LegacyContactImporter.java b/src/com/android/providers/contacts/LegacyContactImporter.java
deleted file mode 100644
index 239f08d..0000000
--- a/src/com/android/providers/contacts/LegacyContactImporter.java
+++ /dev/null
@@ -1,1310 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.contacts;
-
-import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteException;
-import android.database.sqlite.SQLiteStatement;
-import android.provider.CallLog.Calls;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Groups;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-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.telephony.PhoneNumberUtils;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.File;
-
-public class LegacyContactImporter {
-
-    public static final String TAG = "LegacyContactImporter";
-
-    private static final int MAX_ATTEMPTS = 5;
-    private static final int DELAY_BETWEEN_ATTEMPTS = 2000;
-
-    public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
-    private static final String DATABASE_NAME = "contacts.db";
-
-    private static final int INSERT_BATCH_SIZE = 200;
-
-    /**
-     * Estimated increase in database size after import.
-     */
-    private static final long DATABASE_SIZE_MULTIPLIER = 4;
-
-    /**
-     * Estimated minimum database size in megabytes.
-     */
-    private static final long DATABASE_MIN_SIZE = 5;
-
-    private final Context mContext;
-    private final ContactsProvider2 mContactsProvider;
-    private final NameLookupBuilder mNameLookupBuilder;
-
-    private ContactsDatabaseHelper mDbHelper;
-    private ContentValues mValues = new ContentValues();
-    private ContentResolver mResolver;
-    private boolean mPhoneticNameAvailable = true;
-
-    private SQLiteDatabase mSourceDb;
-    private SQLiteDatabase mTargetDb;
-
-    private NameSplitter mNameSplitter;
-    private int mBatchCounter;
-
-    private int mContactCount;
-
-    private long mStructuredNameMimetypeId;
-    private long mNoteMimetypeId;
-    private long mOrganizationMimetypeId;
-    private long mPhoneMimetypeId;
-    private long mEmailMimetypeId;
-    private long mImMimetypeId;
-    private long mPostalMimetypeId;
-    private long mPhotoMimetypeId;
-    private long mGroupMembershipMimetypeId;
-
-    private long mEstimatedStorageRequirement = DATABASE_MIN_SIZE;
-
-    public LegacyContactImporter(Context context, ContactsProvider2 contactsProvider) {
-        mContext = context;
-        mContactsProvider = contactsProvider;
-        mResolver = mContactsProvider.getContext().getContentResolver();
-        mNameLookupBuilder = mContactsProvider.getNameLookupBuilder();
-    }
-
-    public boolean importContacts() throws Exception {
-        String path = mContext.getDatabasePath(DATABASE_NAME).getPath();
-        File file = new File(path);
-        if (!file.exists()) {
-            Log.i(TAG, "Legacy contacts database does not exist at " + path);
-            return true;
-        }
-
-        Log.w(TAG, "Importing contacts from " + path);
-
-        for (int i = 0; i < MAX_ATTEMPTS; i++) {
-            try {
-                mSourceDb = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY);
-                importContactsFromLegacyDb();
-                Log.i(TAG, "Imported legacy contacts: " + mContactCount);
-                mContactsProvider.notifyChange();
-                return true;
-
-            } catch (SQLiteException e) {
-                Log.e(TAG, "Database import exception. Will retry in " + DELAY_BETWEEN_ATTEMPTS
-                        + "ms", e);
-
-                // We could get a "database locked" exception here, in which
-                // case we should retry
-                Thread.sleep(DELAY_BETWEEN_ATTEMPTS);
-
-            } finally {
-                if (mSourceDb != null) {
-                    mSourceDb.close();
-                }
-            }
-        }
-
-        long oldDatabaseSize = file.length();
-        mEstimatedStorageRequirement = oldDatabaseSize * DATABASE_SIZE_MULTIPLIER / 1024 / 1024;
-        if (mEstimatedStorageRequirement < DATABASE_MIN_SIZE) {
-            mEstimatedStorageRequirement = DATABASE_MIN_SIZE;
-        }
-
-        return false;
-    }
-
-    public long getEstimatedStorageRequirement() {
-        return mEstimatedStorageRequirement;
-    }
-
-    private void importContactsFromLegacyDb() {
-        int version = mSourceDb.getVersion();
-
-        // Upgrade to version 78 was the latest that wiped out data.  Might as well follow suit
-        // and ignore earlier versions.
-        if (version < 78) {
-            return;
-        }
-
-        if (version < 80) {
-            mPhoneticNameAvailable = false;
-        }
-
-        mDbHelper = (ContactsDatabaseHelper)mContactsProvider.getDatabaseHelper();
-        mTargetDb = mDbHelper.getWritableDatabase();
-
-        mStructuredNameMimetypeId = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
-        mNoteMimetypeId = mDbHelper.getMimeTypeId(Note.CONTENT_ITEM_TYPE);
-        mOrganizationMimetypeId = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
-        mPhoneMimetypeId = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
-        mEmailMimetypeId = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
-        mImMimetypeId = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
-        mPostalMimetypeId = mDbHelper.getMimeTypeId(StructuredPostal.CONTENT_ITEM_TYPE);
-        mPhotoMimetypeId = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
-        mGroupMembershipMimetypeId =
-                mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
-
-        mNameSplitter = mContactsProvider.getNameSplitter();
-
-        mTargetDb.beginTransaction();
-        try {
-            checkForImportFailureTest();
-
-            /*
-             * At this point there should be no data in the contacts provider, but in case
-             * some was inserted by mistake, we should remove it. The main reason for this
-             * is that we will be preserving original contact IDs and don't want to run into
-             * any collisions.
-             */
-            mContactsProvider.wipeData();
-
-            importGroups();
-            importPeople();
-            importOrganizations();
-            importPhones();
-            importContactMethods();
-            importPhotos();
-            importGroupMemberships();
-            updateDisplayNamesAndLookupKeys();
-
-            // Deleted contacts should be inserted after everything else, because
-            // the legacy table does not provide an _ID field - the _ID field
-            // will be autoincremented
-            importDeletedPeople();
-
-            mDbHelper.updateAllVisible();
-
-            mTargetDb.setTransactionSuccessful();
-        } finally {
-            mTargetDb.endTransaction();
-        }
-
-        importCalls();
-    }
-
-    /**
-     * This is used for simulating an import failure. Insert a row into the "settings"
-     * table with key='TEST' and then proceed with the upgrade.  Remove the record
-     * after verifying the failure handling.
-     */
-    private void checkForImportFailureTest() {
-        long isTest = DatabaseUtils.longForQuery(mSourceDb,
-                "SELECT COUNT(*) FROM settings WHERE key='TEST'", null);
-        if (isTest != 0) {
-            throw new SQLiteException("Testing import failure.");
-        }
-    }
-
-    private interface GroupsQuery {
-        String TABLE = "groups";
-
-        String[] COLUMNS = {
-                "_id", "name", "notes", "should_sync", "system_id", "_sync_account", "_sync_id",
-                "_sync_dirty",
-        };
-
-        static int ID = 0;
-        static int NAME = 1;
-        static int NOTES = 2;
-        static int SHOULD_SYNC = 3;            // TODO add this feature to Groups
-        static int SYSTEM_ID = 4;
-
-        static int _SYNC_ACCOUNT = 5;
-        static int _SYNC_ID = 6;
-        static int _SYNC_DIRTY = 7;
-    }
-
-    private interface GroupsInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.GROUPS + "(" +
-                Groups._ID + "," +
-                Groups.TITLE + "," +
-                Groups.NOTES + "," +
-                Groups.SYSTEM_ID + "," +
-                Groups.DIRTY + "," +
-                Groups.GROUP_VISIBLE + "," +
-                Groups.ACCOUNT_NAME + "," +
-                Groups.ACCOUNT_TYPE + "," +
-                Groups.SOURCE_ID +
-        ") VALUES (?,?,?,?,?,?,?,?,?)";
-
-        int ID = 1;
-        int TITLE = 2;
-        int NOTES = 3;
-        int SYSTEM_ID = 4;
-        int DIRTY = 5;
-        int GROUP_VISIBLE = 6;
-        int ACCOUNT_NAME = 7;
-        int ACCOUNT_TYPE = 8;
-        int SOURCE_ID = 9;
-    }
-
-    private void importGroups() {
-        SQLiteStatement insert = mTargetDb.compileStatement(GroupsInsert.INSERT_SQL);
-        Cursor c = mSourceDb.query(GroupsQuery.TABLE, GroupsQuery.COLUMNS, null, null,
-                null, null, null);
-        try {
-            while (c.moveToNext()) {
-                insertGroup(c, insert);
-            }
-        } finally {
-            c.close();
-            insert.close();
-        }
-    }
-
-    private void insertGroup(Cursor c, SQLiteStatement insert) {
-        long id = c.getLong(GroupsQuery.ID);
-
-        insert.bindLong(GroupsInsert.ID, id);
-        bindString(insert, GroupsInsert.TITLE, c.getString(GroupsQuery.NAME));
-        bindString(insert, GroupsInsert.NOTES, c.getString(GroupsQuery.NOTES));
-        bindString(insert, GroupsInsert.SYSTEM_ID, c.getString(GroupsQuery.SYSTEM_ID));
-        insert.bindLong(GroupsInsert.DIRTY, c.getLong(GroupsQuery._SYNC_DIRTY));
-        insert.bindLong(GroupsInsert.GROUP_VISIBLE, 1);
-
-        String account = c.getString(GroupsQuery._SYNC_ACCOUNT);
-        if (!TextUtils.isEmpty(account)) {
-            bindString(insert, GroupsInsert.ACCOUNT_NAME, account);
-            bindString(insert, GroupsInsert.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
-            bindString(insert, GroupsInsert.SOURCE_ID, c.getString(GroupsQuery._SYNC_ID));
-        } else {
-            insert.bindNull(GroupsInsert.ACCOUNT_NAME);
-            insert.bindNull(GroupsInsert.ACCOUNT_TYPE);
-            insert.bindNull(GroupsInsert.SOURCE_ID);
-        }
-        insert(insert);
-    }
-
-    private interface PeopleQuery {
-        String TABLE = "people";
-
-        String NAME_SQL =
-                "(CASE WHEN (name IS NOT NULL AND name != '') "
-                    + "THEN name "
-                + "ELSE "
-                    + "(CASE WHEN primary_organization is NOT NULL THEN "
-                        + "(SELECT company FROM organizations WHERE "
-                            + "organizations._id = primary_organization) "
-                    + "ELSE "
-                        + "(CASE WHEN primary_phone IS NOT NULL THEN "
-                            +"(SELECT number FROM phones WHERE phones._id = primary_phone) "
-                        + "ELSE "
-                            + "(CASE WHEN primary_email IS NOT NULL THEN "
-                                + "(SELECT data FROM contact_methods WHERE "
-                                    + "contact_methods._id = primary_email) "
-                            + "ELSE "
-                                + "null "
-                            + "END) "
-                        + "END) "
-                    + "END) "
-                + "END) ";
-
-
-        String[] COLUMNS_WITH_DISPLAY_NAME_WITHOUT_PHONETIC_NAME = {
-                "_id", NAME_SQL, "notes", "times_contacted", "last_time_contacted", "starred",
-                "primary_phone", "primary_organization", "primary_email", "custom_ringtone",
-                "send_to_voicemail", "_sync_account", "_sync_id", "_sync_time", "_sync_local_id",
-                "_sync_dirty",
-        };
-
-        String[] COLUMNS_WITH_DISPLAY_NAME_WITH_PHONETIC_NAME = {
-                "_id", NAME_SQL, "notes", "times_contacted", "last_time_contacted", "starred",
-                "primary_phone", "primary_organization", "primary_email", "custom_ringtone",
-                "send_to_voicemail", "_sync_account", "_sync_id", "_sync_time", "_sync_local_id",
-                "_sync_dirty", "phonetic_name",
-        };
-
-        String[] COLUMNS_WITHOUT_PHONETIC_NAME = {
-                "_id", "name", "notes", "times_contacted", "last_time_contacted", "starred",
-                "primary_phone", "primary_organization", "primary_email", "custom_ringtone",
-                "send_to_voicemail", "_sync_account", "_sync_id", "_sync_time", "_sync_local_id",
-                "_sync_dirty",
-        };
-
-        String[] COLUMNS_WITH_PHONETIC_NAME = {
-                "_id", "name", "notes", "times_contacted", "last_time_contacted", "starred",
-                "primary_phone", "primary_organization", "primary_email", "custom_ringtone",
-                "send_to_voicemail", "_sync_account", "_sync_id", "_sync_time", "_sync_local_id",
-                "_sync_dirty", "phonetic_name",
-        };
-
-        static int _ID = 0;
-        static int NAME = 1;
-        static int NOTES = 2;
-        static int TIMES_CONTACTED = 3;
-        static int LAST_TIME_CONTACTED = 4;
-        static int STARRED = 5;
-        static int PRIMARY_PHONE = 6;
-        static int PRIMARY_ORGANIZATION = 7;
-        static int PRIMARY_EMAIL = 8;
-        static int CUSTOM_RINGTONE = 9;
-        static int SEND_TO_VOICEMAIL = 10;
-
-        static int _SYNC_ACCOUNT = 11;
-        static int _SYNC_ID = 12;
-        static int _SYNC_TIME = 13;
-        static int _SYNC_LOCAL_ID = 14;
-        static int _SYNC_DIRTY = 15;
-
-        static int PHONETIC_NAME = 16;
-    }
-
-
-    private interface RawContactsInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.RAW_CONTACTS + "(" +
-                RawContacts._ID + "," +
-                RawContacts.CONTACT_ID + "," +
-                RawContacts.CUSTOM_RINGTONE + "," +
-                RawContacts.DIRTY + "," +
-                RawContacts.LAST_TIME_CONTACTED + "," +
-                RawContacts.SEND_TO_VOICEMAIL + "," +
-                RawContacts.STARRED + "," +
-                RawContacts.TIMES_CONTACTED + "," +
-                RawContacts.SYNC1 + "," +
-                RawContacts.SYNC2 + "," +
-                RawContacts.ACCOUNT_NAME + "," +
-                RawContacts.ACCOUNT_TYPE + "," +
-                RawContacts.SOURCE_ID + "," +
-                RawContactsColumns.DISPLAY_NAME +
-         ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
-
-        int ID = 1;
-        int CONTACT_ID = 2;
-        int CUSTOM_RINGTONE = 3;
-        int DIRTY = 4;
-        int LAST_TIME_CONTACTED = 5;
-        int SEND_TO_VOICEMAIL = 6;
-        int STARRED = 7;
-        int TIMES_CONTACTED = 8;
-        int SYNC1 = 9;
-        int SYNC2 = 10;
-        int ACCOUNT_NAME = 11;
-        int ACCOUNT_TYPE = 12;
-        int SOURCE_ID = 13;
-        int DISPLAY_NAME = 14;
-    }
-
-    private interface ContactsInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.CONTACTS + "(" +
-                Contacts._ID + "," +
-                Contacts.CUSTOM_RINGTONE + "," +
-                Contacts.LAST_TIME_CONTACTED + "," +
-                Contacts.SEND_TO_VOICEMAIL + "," +
-                Contacts.STARRED + "," +
-                Contacts.TIMES_CONTACTED + "," +
-                Contacts.NAME_RAW_CONTACT_ID +
-         ") VALUES (?,?,?,?,?,?,?)";
-
-        int ID = 1;
-        int CUSTOM_RINGTONE = 2;
-        int LAST_TIME_CONTACTED = 3;
-        int SEND_TO_VOICEMAIL = 4;
-        int STARRED = 5;
-        int TIMES_CONTACTED = 6;
-        int NAME_RAW_CONTACT_ID = 7;
-    }
-
-    private interface StructuredNameInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.DATA + "(" +
-                Data.RAW_CONTACT_ID + "," +
-                DataColumns.MIMETYPE_ID + "," +
-                StructuredName.DISPLAY_NAME + "," +
-                StructuredName.PREFIX + "," +
-                StructuredName.GIVEN_NAME + "," +
-                StructuredName.MIDDLE_NAME + "," +
-                StructuredName.FAMILY_NAME + "," +
-                StructuredName.SUFFIX + "," +
-                StructuredName.FULL_NAME_STYLE + "," +
-                StructuredName.PHONETIC_FAMILY_NAME + "," +
-                StructuredName.PHONETIC_MIDDLE_NAME + "," +
-                StructuredName.PHONETIC_GIVEN_NAME + "," +
-                StructuredName.PHONETIC_NAME_STYLE +
-         ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)";
-
-        int RAW_CONTACT_ID = 1;
-        int MIMETYPE_ID = 2;
-        int DISPLAY_NAME = 3;
-        int PREFIX = 4;
-        int GIVEN_NAME = 5;
-        int MIDDLE_NAME = 6;
-        int FAMILY_NAME = 7;
-        int SUFFIX = 8;
-        int FULL_NAME_STYLE = 9;
-        int PHONETIC_FAMILY_NAME = 10;
-        int PHONETIC_MIDDLE_NAME = 11;
-        int PHONETIC_GIVEN_NAME = 12;
-        int PHONETIC_NAME_STYLE = 13;
-    }
-
-    private interface NoteInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.DATA + "(" +
-                Data.RAW_CONTACT_ID + "," +
-                DataColumns.MIMETYPE_ID + "," +
-                Note.NOTE +
-         ") VALUES (?,?,?)";
-
-        int RAW_CONTACT_ID = 1;
-        int MIMETYPE_ID = 2;
-        int NOTE = 3;
-    }
-
-    private void importPeople() {
-        SQLiteStatement rawContactInsert = mTargetDb.compileStatement(RawContactsInsert.INSERT_SQL);
-        SQLiteStatement contactInsert = mTargetDb.compileStatement(ContactsInsert.INSERT_SQL);
-        SQLiteStatement structuredNameInsert =
-                mTargetDb.compileStatement(StructuredNameInsert.INSERT_SQL);
-        SQLiteStatement noteInsert = mTargetDb.compileStatement(NoteInsert.INSERT_SQL);
-        try {
-            String[] columns = mPhoneticNameAvailable
-                    ? PeopleQuery.COLUMNS_WITH_DISPLAY_NAME_WITH_PHONETIC_NAME
-                    : PeopleQuery.COLUMNS_WITH_DISPLAY_NAME_WITHOUT_PHONETIC_NAME;
-            Cursor c = mSourceDb.query(PeopleQuery.TABLE, columns, "name IS NULL", null, null,
-                    null, null);
-            try {
-                while (c.moveToNext()) {
-                    insertRawContact(c, rawContactInsert);
-                    insertContact(c, contactInsert);
-                    insertNote(c, noteInsert);
-                    mContactCount++;
-                }
-            } finally {
-                c.close();
-            }
-
-            columns = mPhoneticNameAvailable
-                    ? PeopleQuery.COLUMNS_WITH_PHONETIC_NAME
-                    : PeopleQuery.COLUMNS_WITHOUT_PHONETIC_NAME;
-            c = mSourceDb.query(PeopleQuery.TABLE, columns, "name IS NOT NULL", null, null, null,
-                    null);
-            try {
-                while (c.moveToNext()) {
-                    long id = insertRawContact(c, rawContactInsert);
-                    insertContact(c, contactInsert);
-                    insertStructuredName(c, structuredNameInsert);
-                    insertNote(c, noteInsert);
-                    mContactCount++;
-                }
-            } finally {
-                c.close();
-            }
-        } finally {
-            rawContactInsert.close();
-            contactInsert.close();
-            structuredNameInsert.close();
-            noteInsert.close();
-        }
-    }
-
-    private long insertRawContact(Cursor c, SQLiteStatement insert) {
-        long id = c.getLong(PeopleQuery._ID);
-        insert.bindLong(RawContactsInsert.ID, id);
-        insert.bindLong(RawContactsInsert.CONTACT_ID, id);
-        bindString(insert, RawContactsInsert.CUSTOM_RINGTONE,
-                c.getString(PeopleQuery.CUSTOM_RINGTONE));
-        bindString(insert, RawContactsInsert.DIRTY,
-                c.getString(PeopleQuery._SYNC_DIRTY));
-        insert.bindLong(RawContactsInsert.LAST_TIME_CONTACTED,
-                c.getLong(PeopleQuery.LAST_TIME_CONTACTED));
-        insert.bindLong(RawContactsInsert.SEND_TO_VOICEMAIL,
-                c.getLong(PeopleQuery.SEND_TO_VOICEMAIL));
-        insert.bindLong(RawContactsInsert.STARRED,
-                c.getLong(PeopleQuery.STARRED));
-        insert.bindLong(RawContactsInsert.TIMES_CONTACTED,
-                c.getLong(PeopleQuery.TIMES_CONTACTED));
-        bindString(insert, RawContactsInsert.SYNC1,
-                c.getString(PeopleQuery._SYNC_TIME));
-        bindString(insert, RawContactsInsert.SYNC2,
-                c.getString(PeopleQuery._SYNC_LOCAL_ID));
-        bindString(insert, RawContactsInsert.DISPLAY_NAME,
-                c.getString(PeopleQuery.NAME));
-
-        String account = c.getString(PeopleQuery._SYNC_ACCOUNT);
-        if (!TextUtils.isEmpty(account)) {
-            bindString(insert, RawContactsInsert.ACCOUNT_NAME, account);
-            bindString(insert, RawContactsInsert.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
-            bindString(insert, RawContactsInsert.SOURCE_ID, c.getString(PeopleQuery._SYNC_ID));
-        } else {
-            insert.bindNull(RawContactsInsert.ACCOUNT_NAME);
-            insert.bindNull(RawContactsInsert.ACCOUNT_TYPE);
-            insert.bindNull(RawContactsInsert.SOURCE_ID);
-        }
-        insert(insert);
-        return id;
-    }
-
-    private void insertContact(Cursor c, SQLiteStatement insert) {
-        long id = c.getLong(PeopleQuery._ID);
-        insert.bindLong(ContactsInsert.ID, id);
-        bindString(insert, ContactsInsert.CUSTOM_RINGTONE,
-                c.getString(PeopleQuery.CUSTOM_RINGTONE));
-        insert.bindLong(ContactsInsert.LAST_TIME_CONTACTED,
-                c.getLong(PeopleQuery.LAST_TIME_CONTACTED));
-        insert.bindLong(ContactsInsert.SEND_TO_VOICEMAIL,
-                c.getLong(PeopleQuery.SEND_TO_VOICEMAIL));
-        insert.bindLong(ContactsInsert.STARRED,
-                c.getLong(PeopleQuery.STARRED));
-        insert.bindLong(ContactsInsert.TIMES_CONTACTED,
-                c.getLong(PeopleQuery.TIMES_CONTACTED));
-        insert.bindLong(ContactsInsert.NAME_RAW_CONTACT_ID, id);
-
-        insert(insert);
-    }
-
-    private void insertStructuredName(Cursor c, SQLiteStatement insert) {
-        String name = c.getString(PeopleQuery.NAME);
-        if (TextUtils.isEmpty(name)) {
-            return;
-        }
-
-        long id = c.getLong(PeopleQuery._ID);
-
-        insert.bindLong(StructuredNameInsert.RAW_CONTACT_ID, id);
-        insert.bindLong(StructuredNameInsert.MIMETYPE_ID, mStructuredNameMimetypeId);
-        bindString(insert, StructuredNameInsert.DISPLAY_NAME, name);
-
-        NameSplitter.Name splitName = new NameSplitter.Name();
-        mNameSplitter.split(splitName, name);
-
-        bindString(insert, StructuredNameInsert.PREFIX,
-                splitName.getPrefix());
-        bindString(insert, StructuredNameInsert.GIVEN_NAME,
-                splitName.getGivenNames());
-        bindString(insert, StructuredNameInsert.MIDDLE_NAME,
-                splitName.getMiddleName());
-        bindString(insert, StructuredNameInsert.FAMILY_NAME,
-                splitName.getFamilyName());
-        bindString(insert, StructuredNameInsert.SUFFIX,
-                splitName.getSuffix());
-        final String joined = mNameSplitter.join(splitName, true, true);
-        bindString(insert, StructuredNameInsert.DISPLAY_NAME, joined);
-
-        if (mPhoneticNameAvailable) {
-            String phoneticName = c.getString(PeopleQuery.PHONETIC_NAME);
-            if (phoneticName != null) {
-                int index = phoneticName.indexOf(' ');
-                if (index == -1) {
-                    splitName.phoneticFamilyName = phoneticName;
-                } else {
-                    splitName.phoneticFamilyName = phoneticName.substring(0, index).trim();
-                    splitName.phoneticGivenName = phoneticName.substring(index + 1).trim();
-                }
-            }
-        }
-
-        mNameSplitter.guessNameStyle(splitName);
-
-        int fullNameStyle = splitName.getFullNameStyle();
-        insert.bindLong(StructuredNameInsert.FULL_NAME_STYLE,
-                fullNameStyle);
-        bindString(insert, StructuredNameInsert.PHONETIC_FAMILY_NAME,
-                splitName.phoneticFamilyName);
-        bindString(insert, StructuredNameInsert.PHONETIC_MIDDLE_NAME,
-                splitName.phoneticMiddleName);
-        bindString(insert, StructuredNameInsert.PHONETIC_GIVEN_NAME,
-                splitName.phoneticGivenName);
-        insert.bindLong(StructuredNameInsert.PHONETIC_NAME_STYLE,
-                splitName.phoneticNameStyle);
-
-        long dataId = insert(insert);
-
-        mNameLookupBuilder.insertNameLookup(id, dataId, name,
-                mNameSplitter.getAdjustedFullNameStyle(fullNameStyle));
-
-        if (splitName.phoneticFamilyName != null
-                || splitName.phoneticMiddleName != null
-                || splitName.phoneticGivenName != null) {
-            mDbHelper.insertNameLookupForPhoneticName(id, dataId,
-                    splitName.phoneticFamilyName,
-                    splitName.phoneticMiddleName,
-                    splitName.phoneticGivenName);
-        }
-    }
-
-    private void insertNote(Cursor c, SQLiteStatement insert) {
-        String notes = c.getString(PeopleQuery.NOTES);
-
-        if (TextUtils.isEmpty(notes)) {
-            return;
-        }
-
-        long id = c.getLong(PeopleQuery._ID);
-        insert.bindLong(NoteInsert.RAW_CONTACT_ID, id);
-        insert.bindLong(NoteInsert.MIMETYPE_ID, mNoteMimetypeId);
-        bindString(insert, NoteInsert.NOTE, notes);
-        insert(insert);
-    }
-
-    private interface OrganizationsQuery {
-        String TABLE = "organizations";
-
-        String[] COLUMNS = {
-                "person", "company", "title", "isprimary", "type", "label",
-        };
-
-        static int PERSON = 0;
-        static int COMPANY = 1;
-        static int TITLE = 2;
-        static int ISPRIMARY = 3;
-        static int TYPE = 4;
-        static int LABEL = 5;
-    }
-
-    private interface OrganizationInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.DATA + "(" +
-                Data.RAW_CONTACT_ID + "," +
-                DataColumns.MIMETYPE_ID + "," +
-                Data.IS_PRIMARY + "," +
-                Data.IS_SUPER_PRIMARY + "," +
-                Organization.COMPANY + "," +
-                Organization.TITLE + "," +
-                Organization.TYPE + "," +
-                Organization.LABEL +
-         ") VALUES (?,?,?,?,?,?,?,?)";
-
-        int RAW_CONTACT_ID = 1;
-        int MIMETYPE_ID = 2;
-        int IS_PRIMARY = 3;
-        int IS_SUPER_PRIMARY = 4;
-        int COMPANY = 5;
-        int TITLE = 6;
-        int TYPE = 7;
-        int LABEL = 8;
-    }
-
-    private void importOrganizations() {
-        SQLiteStatement insert = mTargetDb.compileStatement(OrganizationInsert.INSERT_SQL);
-        Cursor c = mSourceDb.query(OrganizationsQuery.TABLE, OrganizationsQuery.COLUMNS, null, null,
-                null, null, null);
-        try {
-            while (c.moveToNext()) {
-                insertOrganization(c, insert);
-            }
-        } finally {
-            c.close();
-            insert.close();
-        }
-    }
-
-    private void insertOrganization(Cursor c, SQLiteStatement insert) {
-        long id = c.getLong(OrganizationsQuery.PERSON);
-        insert.bindLong(OrganizationInsert.RAW_CONTACT_ID, id);
-        insert.bindLong(OrganizationInsert.MIMETYPE_ID, mOrganizationMimetypeId);
-        bindString(insert, OrganizationInsert.IS_PRIMARY, c.getString(OrganizationsQuery.ISPRIMARY));
-        bindString(insert, OrganizationInsert.IS_SUPER_PRIMARY,
-                c.getString(OrganizationsQuery.ISPRIMARY));
-        bindString(insert, OrganizationInsert.COMPANY, c.getString(OrganizationsQuery.COMPANY));
-        bindString(insert, OrganizationInsert.TITLE, c.getString(OrganizationsQuery.TITLE));
-        bindString(insert, OrganizationInsert.TYPE, c.getString(OrganizationsQuery.TYPE));
-        bindString(insert, OrganizationInsert.LABEL, c.getString(OrganizationsQuery.LABEL));
-        insert(insert);
-    }
-
-    private interface ContactMethodsQuery {
-        String TABLE = "contact_methods";
-
-        String[] COLUMNS = {
-                "person", "kind", "data", "aux_data", "type", "label", "isprimary",
-        };
-
-        static int PERSON = 0;
-        static int KIND = 1;
-        static int DATA = 2;
-        static int AUX_DATA = 3;
-        static int TYPE = 4;
-        static int LABEL = 5;
-        static int ISPRIMARY = 6;
-    }
-
-    private interface EmailInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.DATA + "(" +
-                Data.RAW_CONTACT_ID + "," +
-                DataColumns.MIMETYPE_ID + "," +
-                Data.IS_PRIMARY + "," +
-                Data.IS_SUPER_PRIMARY + "," +
-                Email.DATA + "," +
-                Email.TYPE + "," +
-                Email.LABEL + "," +
-                Data.DATA14 +
-         ") VALUES (?,?,?,?,?,?,?,?)";
-
-        int RAW_CONTACT_ID = 1;
-        int MIMETYPE_ID = 2;
-        int IS_PRIMARY = 3;
-        int IS_SUPER_PRIMARY = 4;
-        int DATA = 5;
-        int TYPE = 6;
-        int LABEL = 7;
-        int AUX_DATA = 8;
-    }
-
-    private interface ImInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.DATA + "(" +
-                Data.RAW_CONTACT_ID + "," +
-                DataColumns.MIMETYPE_ID + "," +
-                Data.IS_PRIMARY + "," +
-                Data.IS_SUPER_PRIMARY + "," +
-                Im.DATA + "," +
-                Im.TYPE + "," +
-                Im.LABEL + "," +
-                Data.DATA14 +
-         ") VALUES (?,?,?,?,?,?,?,?)";
-
-        int RAW_CONTACT_ID = 1;
-        int MIMETYPE_ID = 2;
-        int IS_PRIMARY = 3;
-        int IS_SUPER_PRIMARY = 4;
-        int DATA = 5;
-        int TYPE = 6;
-        int LABEL = 7;
-        int AUX_DATA = 8;
-    }
-
-    private interface PostalInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.DATA + "(" +
-                Data.RAW_CONTACT_ID + "," +
-                DataColumns.MIMETYPE_ID + "," +
-                Data.IS_PRIMARY + "," +
-                Data.IS_SUPER_PRIMARY + "," +
-                StructuredPostal.FORMATTED_ADDRESS + "," +
-                StructuredPostal.TYPE + "," +
-                StructuredPostal.LABEL + "," +
-                Data.DATA14 +
-         ") VALUES (?,?,?,?,?,?,?,?)";
-
-        int RAW_CONTACT_ID = 1;
-        int MIMETYPE_ID = 2;
-        int IS_PRIMARY = 3;
-        int IS_SUPER_PRIMARY = 4;
-        int DATA = 5;
-        int TYPE = 6;
-        int LABEL = 7;
-        int AUX_DATA = 8;
-    }
-
-    private void importContactMethods() {
-        SQLiteStatement emailInsert = mTargetDb.compileStatement(EmailInsert.INSERT_SQL);
-        SQLiteStatement imInsert = mTargetDb.compileStatement(ImInsert.INSERT_SQL);
-        SQLiteStatement postalInsert = mTargetDb.compileStatement(PostalInsert.INSERT_SQL);
-        Cursor c = mSourceDb.query(ContactMethodsQuery.TABLE, ContactMethodsQuery.COLUMNS, null,
-                null, null, null, null);
-        try {
-            while (c.moveToNext()) {
-                int kind = c.getInt(ContactMethodsQuery.KIND);
-                switch (kind) {
-                    case android.provider.Contacts.KIND_EMAIL:
-                        insertEmail(c, emailInsert);
-                        break;
-
-                    case android.provider.Contacts.KIND_IM:
-                        insertIm(c, imInsert);
-                        break;
-
-                    case android.provider.Contacts.KIND_POSTAL:
-                        insertPostal(c, postalInsert);
-                        break;
-                }
-            }
-        } finally {
-            c.close();
-            emailInsert.close();
-            imInsert.close();
-            postalInsert.close();
-        }
-
-    }
-
-    private void insertEmail(Cursor c, SQLiteStatement insert) {
-        long personId = c.getLong(ContactMethodsQuery.PERSON);
-        String email = c.getString(ContactMethodsQuery.DATA);
-
-        insert.bindLong(EmailInsert.RAW_CONTACT_ID, personId);
-        insert.bindLong(EmailInsert.MIMETYPE_ID, mEmailMimetypeId);
-        bindString(insert, EmailInsert.IS_PRIMARY, c.getString(ContactMethodsQuery.ISPRIMARY));
-        bindString(insert, EmailInsert.IS_SUPER_PRIMARY, c.getString(ContactMethodsQuery.ISPRIMARY));
-        bindString(insert, EmailInsert.DATA, email);
-        bindString(insert, EmailInsert.AUX_DATA, c.getString(ContactMethodsQuery.AUX_DATA));
-        bindString(insert, EmailInsert.TYPE, c.getString(ContactMethodsQuery.TYPE));
-        bindString(insert, EmailInsert.LABEL, c.getString(ContactMethodsQuery.LABEL));
-
-        long dataId = insert(insert);
-        mDbHelper.insertNameLookupForEmail(personId, dataId, email);
-    }
-
-    private void insertIm(Cursor c, SQLiteStatement insert) {
-        long personId = c.getLong(ContactMethodsQuery.PERSON);
-
-        insert.bindLong(ImInsert.RAW_CONTACT_ID, personId);
-        insert.bindLong(ImInsert.MIMETYPE_ID, mImMimetypeId);
-        bindString(insert, ImInsert.IS_PRIMARY, c.getString(ContactMethodsQuery.ISPRIMARY));
-        bindString(insert, ImInsert.IS_SUPER_PRIMARY, c.getString(ContactMethodsQuery.ISPRIMARY));
-        bindString(insert, ImInsert.DATA, c.getString(ContactMethodsQuery.DATA));
-        bindString(insert, ImInsert.AUX_DATA, c.getString(ContactMethodsQuery.AUX_DATA));
-        bindString(insert, ImInsert.TYPE, c.getString(ContactMethodsQuery.TYPE));
-        bindString(insert, ImInsert.LABEL, c.getString(ContactMethodsQuery.LABEL));
-        insert(insert);
-    }
-
-    private void insertPostal(Cursor c, SQLiteStatement insert) {
-        long personId = c.getLong(ContactMethodsQuery.PERSON);
-
-        insert.bindLong(PostalInsert.RAW_CONTACT_ID, personId);
-        insert.bindLong(PostalInsert.MIMETYPE_ID, mPostalMimetypeId);
-        bindString(insert, PostalInsert.IS_PRIMARY, c.getString(ContactMethodsQuery.ISPRIMARY));
-        bindString(insert, PostalInsert.IS_SUPER_PRIMARY,
-                c.getString(ContactMethodsQuery.ISPRIMARY));
-        bindString(insert, PostalInsert.DATA, c.getString(ContactMethodsQuery.DATA));
-        bindString(insert, PostalInsert.AUX_DATA, c.getString(ContactMethodsQuery.AUX_DATA));
-        bindString(insert, PostalInsert.TYPE, c.getString(ContactMethodsQuery.TYPE));
-        bindString(insert, PostalInsert.LABEL, c.getString(ContactMethodsQuery.LABEL));
-        insert(insert);
-    }
-
-    private interface PhonesQuery {
-        String TABLE = "phones";
-
-        String[] COLUMNS = {
-                "person", "type", "number", "label", "isprimary",
-        };
-
-        static int PERSON = 0;
-        static int TYPE = 1;
-        static int NUMBER = 2;
-        static int LABEL = 3;
-        static int ISPRIMARY = 4;
-    }
-
-    private interface PhoneInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.DATA + "(" +
-                Data.RAW_CONTACT_ID + "," +
-                DataColumns.MIMETYPE_ID + "," +
-                Data.IS_PRIMARY + "," +
-                Data.IS_SUPER_PRIMARY + "," +
-                Phone.NUMBER + "," +
-                Phone.TYPE + "," +
-                Phone.LABEL + "," +
-                PhoneColumns.NORMALIZED_NUMBER +
-         ") VALUES (?,?,?,?,?,?,?,?)";
-
-        int RAW_CONTACT_ID = 1;
-        int MIMETYPE_ID = 2;
-        int IS_PRIMARY = 3;
-        int IS_SUPER_PRIMARY = 4;
-        int NUMBER = 5;
-        int TYPE = 6;
-        int LABEL = 7;
-        int NORMALIZED_NUMBER = 8;
-    }
-
-    private interface PhoneLookupInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.PHONE_LOOKUP + "(" +
-                PhoneLookupColumns.RAW_CONTACT_ID + "," +
-                PhoneLookupColumns.DATA_ID + "," +
-                PhoneLookupColumns.NORMALIZED_NUMBER + "," +
-                PhoneLookupColumns.MIN_MATCH +
-         ") VALUES (?,?,?,?)";
-
-        int RAW_CONTACT_ID = 1;
-        int DATA_ID = 2;
-        int NORMALIZED_NUMBER = 3;
-        int MIN_MATCH = 4;
-    }
-
-    private interface HasPhoneNumberUpdate {
-        String UPDATE_SQL = "UPDATE " + Tables.CONTACTS +
-                " SET " + Contacts.HAS_PHONE_NUMBER + "=1 WHERE " + Contacts._ID + "=?";
-
-        int CONTACT_ID = 1;
-    }
-
-    private void importPhones() {
-        SQLiteStatement phoneInsert = mTargetDb.compileStatement(PhoneInsert.INSERT_SQL);
-        SQLiteStatement phoneLookupInsert =
-                mTargetDb.compileStatement(PhoneLookupInsert.INSERT_SQL);
-        SQLiteStatement hasPhoneUpdate =
-                mTargetDb.compileStatement(HasPhoneNumberUpdate.UPDATE_SQL);
-        Cursor c = mSourceDb.query(PhonesQuery.TABLE, PhonesQuery.COLUMNS, null, null,
-                null, null, null);
-        try {
-            while (c.moveToNext()) {
-                insertPhone(c, phoneInsert, phoneLookupInsert, hasPhoneUpdate);
-            }
-        } finally {
-            c.close();
-            phoneInsert.close();
-            phoneLookupInsert.close();
-            hasPhoneUpdate.close();
-        }
-    }
-
-    private void insertPhone(Cursor c, SQLiteStatement phoneInsert,
-            SQLiteStatement phoneLookupInsert, SQLiteStatement hasPhoneUpdate) {
-        long lastUpdatedContact = -1;
-        long id = c.getLong(PhonesQuery.PERSON);
-        String number = c.getString(PhonesQuery.NUMBER);
-        String normalizedNumber = null;
-        if (number != null) {
-            normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
-        }
-        phoneInsert.bindLong(PhoneInsert.RAW_CONTACT_ID, id);
-        phoneInsert.bindLong(PhoneInsert.MIMETYPE_ID, mPhoneMimetypeId);
-        bindString(phoneInsert, PhoneInsert.IS_PRIMARY, c.getString(PhonesQuery.ISPRIMARY));
-        bindString(phoneInsert, PhoneInsert.IS_SUPER_PRIMARY, c.getString(PhonesQuery.ISPRIMARY));
-        bindString(phoneInsert, PhoneInsert.NUMBER, number);
-        bindString(phoneInsert, PhoneInsert.TYPE, c.getString(PhonesQuery.TYPE));
-        bindString(phoneInsert, PhoneInsert.LABEL, c.getString(PhonesQuery.LABEL));
-        bindString(phoneInsert, PhoneInsert.NORMALIZED_NUMBER, normalizedNumber);
-
-        long dataId = insert(phoneInsert);
-        if (normalizedNumber != null) {
-            phoneLookupInsert.bindLong(PhoneLookupInsert.RAW_CONTACT_ID, id);
-            phoneLookupInsert.bindLong(PhoneLookupInsert.DATA_ID, dataId);
-            phoneLookupInsert.bindString(PhoneLookupInsert.NORMALIZED_NUMBER, normalizedNumber);
-            phoneLookupInsert.bindString(PhoneLookupInsert.MIN_MATCH,
-                    PhoneNumberUtils.toCallerIDMinMatch(number));
-            insert(phoneLookupInsert);
-
-            if (lastUpdatedContact != id) {
-                lastUpdatedContact = id;
-                hasPhoneUpdate.bindLong(HasPhoneNumberUpdate.CONTACT_ID, id);
-                hasPhoneUpdate.execute();
-            }
-        }
-    }
-
-    private interface PhotosQuery {
-        String TABLE = "photos";
-
-        String[] COLUMNS = {
-                "person", "data", "_sync_id", "_sync_account"
-        };
-
-        static int PERSON = 0;
-        static int DATA = 1;
-        static int _SYNC_ID = 2;
-        static int _SYNC_ACCOUNT = 3;
-    }
-
-    private interface PhotoInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.DATA + "(" +
-                Data.RAW_CONTACT_ID + "," +
-                DataColumns.MIMETYPE_ID + "," +
-                Photo.PHOTO + "," +
-                Data.SYNC1 +
-         ") VALUES (?,?,?,?)";
-
-        int RAW_CONTACT_ID = 1;
-        int MIMETYPE_ID = 2;
-        int PHOTO = 3;
-        int SYNC1 = 4;
-    }
-
-    private interface PhotoIdUpdate {
-        String UPDATE_SQL = "UPDATE " + Tables.CONTACTS +
-                " SET " + Contacts.PHOTO_ID + "=? WHERE " + Contacts._ID + "=?";
-
-        int PHOTO_ID = 1;
-        int CONTACT_ID = 2;
-    }
-
-    private void importPhotos() {
-        SQLiteStatement insert = mTargetDb.compileStatement(PhotoInsert.INSERT_SQL);
-        SQLiteStatement photoIdUpdate = mTargetDb.compileStatement(PhotoIdUpdate.UPDATE_SQL);
-        Cursor c = mSourceDb.query(PhotosQuery.TABLE, PhotosQuery.COLUMNS, null, null,
-                null, null, null);
-        try {
-            while (c.moveToNext()) {
-                insertPhoto(c, insert, photoIdUpdate);
-            }
-        } finally {
-            c.close();
-            insert.close();
-            photoIdUpdate.close();
-        }
-    }
-
-    private void insertPhoto(Cursor c, SQLiteStatement insert, SQLiteStatement photoIdUpdate) {
-        if (c.isNull(PhotosQuery.DATA)) {
-            return;
-        }
-
-        long personId = c.getLong(PhotosQuery.PERSON);
-
-        insert.bindLong(PhotoInsert.RAW_CONTACT_ID, personId);
-        insert.bindLong(PhotoInsert.MIMETYPE_ID, mPhotoMimetypeId);
-        insert.bindBlob(PhotoInsert.PHOTO, c.getBlob(PhotosQuery.DATA));
-
-        String account = c.getString(PhotosQuery._SYNC_ACCOUNT);
-        if (!TextUtils.isEmpty(account)) {
-            bindString(insert, PhotoInsert.SYNC1, c.getString(PhotosQuery._SYNC_ID));
-        } else {
-            insert.bindNull(PhotoInsert.SYNC1);
-        }
-
-        long rowId = insert(insert);
-        photoIdUpdate.bindLong(PhotoIdUpdate.PHOTO_ID, rowId);
-        photoIdUpdate.bindLong(PhotoIdUpdate.CONTACT_ID, personId);
-        photoIdUpdate.execute();
-    }
-
-    private interface GroupMembershipQuery {
-        String TABLE = "groupmembership";
-
-        String[] COLUMNS = {
-                "person", "group_id", "group_sync_account", "group_sync_id"
-        };
-
-        static int PERSON_ID = 0;
-        static int GROUP_ID = 1;
-        static int GROUP_SYNC_ACCOUNT = 2;
-        static int GROUP_SYNC_ID = 3;
-    }
-
-    private interface GroupMembershipInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.DATA + "(" +
-                Data.RAW_CONTACT_ID + "," +
-                DataColumns.MIMETYPE_ID + "," +
-                GroupMembership.GROUP_ROW_ID +
-         ") VALUES (?,?,?)";
-
-        int RAW_CONTACT_ID = 1;
-        int MIMETYPE_ID = 2;
-        int GROUP_ROW_ID = 3;
-    }
-
-    private void importGroupMemberships() {
-        SQLiteStatement insert = mTargetDb.compileStatement(GroupMembershipInsert.INSERT_SQL);
-        Cursor c = mSourceDb.query(GroupMembershipQuery.TABLE, GroupMembershipQuery.COLUMNS, null,
-                null, null, null, null);
-        try {
-            while (c.moveToNext()) {
-                insertGroupMembership(c, insert);
-            }
-        } finally {
-            c.close();
-            insert.close();
-        }
-    }
-
-    private void insertGroupMembership(Cursor c, SQLiteStatement insert) {
-        long personId = c.getLong(GroupMembershipQuery.PERSON_ID);
-
-        long groupId = 0;
-        if (c.isNull(GroupMembershipQuery.GROUP_ID)) {
-            String account = c.getString(GroupMembershipQuery.GROUP_SYNC_ACCOUNT);
-            if (!TextUtils.isEmpty(account)) {
-                String syncId = c.getString(GroupMembershipQuery.GROUP_SYNC_ID);
-
-                Cursor cursor = mTargetDb.query(Tables.GROUPS,
-                        new String[]{Groups._ID}, Groups.SOURCE_ID + "=?", new String[]{syncId},
-                        null, null, null);
-                try {
-                    if (cursor.moveToFirst()) {
-                        groupId = cursor.getLong(0);
-                    }
-                } finally {
-                    cursor.close();
-                }
-
-                if (groupId == 0) {
-                    ContentValues values = new ContentValues();
-                    values.put(Groups.ACCOUNT_NAME, account);
-                    values.put(Groups.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
-                    values.put(Groups.GROUP_VISIBLE, true);
-                    values.put(Groups.SOURCE_ID, syncId);
-                    groupId = mTargetDb.insert(Tables.GROUPS, null, values);
-                }
-            }
-        } else {
-            groupId = c.getLong(GroupMembershipQuery.GROUP_ID);
-        }
-
-        insert.bindLong(GroupMembershipInsert.RAW_CONTACT_ID, personId);
-        insert.bindLong(GroupMembershipInsert.MIMETYPE_ID, mGroupMembershipMimetypeId);
-        insert.bindLong(GroupMembershipInsert.GROUP_ROW_ID, groupId);
-        insert(insert);
-    }
-
-    private interface CallsQuery {
-        String TABLE = "calls";
-
-        String[] COLUMNS = {
-                "_id", "number", "date", "duration", "type", "new", "name", "numbertype",
-                "numberlabel"
-        };
-
-        static int ID = 0;
-        static int NUMBER = 1;
-        static int DATE = 2;
-        static int DURATION = 3;
-        static int TYPE = 4;
-        static int NEW = 5;
-        static int NAME = 6;
-        static int NUMBER_TYPE = 7;
-        static int NUMBER_LABEL = 8;
-    }
-
-    private void importCalls() {
-        Cursor c = mSourceDb.query(CallsQuery.TABLE, CallsQuery.COLUMNS, null, null,
-                null, null, null);
-        try {
-            while (c.moveToNext()) {
-                insertCall(c);
-            }
-        } finally {
-            c.close();
-        }
-    }
-
-    private void insertCall(Cursor c) {
-
-        // Cannot use batch operations here, because call log is serviced by a separate provider
-        mValues.clear();
-        mValues.put(Calls._ID, c.getLong(CallsQuery.ID));
-        mValues.put(Calls.NUMBER, c.getString(CallsQuery.NUMBER));
-        mValues.put(Calls.DATE, c.getLong(CallsQuery.DATE));
-        mValues.put(Calls.DURATION, c.getLong(CallsQuery.DURATION));
-        mValues.put(Calls.NEW, c.getLong(CallsQuery.NEW));
-        mValues.put(Calls.TYPE, c.getLong(CallsQuery.TYPE));
-        mValues.put(Calls.CACHED_NAME, c.getString(CallsQuery.NAME));
-        mValues.put(Calls.CACHED_NUMBER_LABEL, c.getString(CallsQuery.NUMBER_LABEL));
-        mValues.put(Calls.CACHED_NUMBER_TYPE, c.getString(CallsQuery.NUMBER_TYPE));
-
-        // TODO: confirm that we can use the CallLogProvider at this point, that it is guaranteed
-        // to have been registered.
-        mResolver.insert(Calls.CONTENT_URI, mValues);
-    }
-
-    private void updateDisplayNamesAndLookupKeys() {
-        // Compute display names, sort keys, lookup key, etc. for all Raw Cont
-        Cursor cursor = mResolver.query(RawContacts.CONTENT_URI,
-                new String[] { RawContacts._ID }, null, null, null);
-        try {
-            while (cursor.moveToNext()) {
-                long rawContactId = cursor.getLong(0);
-                mDbHelper.updateRawContactDisplayName(mTargetDb, rawContactId);
-                mContactsProvider.updateLookupKeyForRawContact(mTargetDb, rawContactId);
-            }
-        } finally {
-            cursor.close();
-        }
-    }
-
-    private interface DeletedPeopleQuery {
-        String TABLE = "_deleted_people";
-
-        String[] COLUMNS = {
-                "_sync_id", "_sync_account"
-        };
-
-        static int _SYNC_ID = 0;
-        static int _SYNC_ACCOUNT = 1;
-    }
-
-    private interface DeletedRawContactInsert {
-        String INSERT_SQL = "INSERT INTO " + Tables.RAW_CONTACTS + "(" +
-                RawContacts.ACCOUNT_NAME + "," +
-                RawContacts.ACCOUNT_TYPE + "," +
-                RawContacts.SOURCE_ID + "," +
-                RawContacts.DELETED + "," +
-                RawContacts.AGGREGATION_MODE +
-         ") VALUES (?,?,?,?,?)";
-
-
-        int ACCOUNT_NAME = 1;
-        int ACCOUNT_TYPE = 2;
-        int SOURCE_ID = 3;
-        int DELETED = 4;
-        int AGGREGATION_MODE = 5;
-    }
-
-    private void importDeletedPeople() {
-        SQLiteStatement insert = mTargetDb.compileStatement(DeletedRawContactInsert.INSERT_SQL);
-        Cursor c = mSourceDb.query(DeletedPeopleQuery.TABLE, DeletedPeopleQuery.COLUMNS, null, null,
-                null, null, null);
-        try {
-            while (c.moveToNext()) {
-                insertDeletedPerson(c, insert);
-            }
-        } finally {
-            c.close();
-            insert.close();
-        }
-    }
-
-    private void insertDeletedPerson(Cursor c, SQLiteStatement insert) {
-        String account = c.getString(DeletedPeopleQuery._SYNC_ACCOUNT);
-        if (account == null) {
-            return;
-        }
-
-        insert.bindString(DeletedRawContactInsert.ACCOUNT_NAME, account);
-        insert.bindString(DeletedRawContactInsert.ACCOUNT_TYPE, DEFAULT_ACCOUNT_TYPE);
-        bindString(insert, DeletedRawContactInsert.SOURCE_ID,
-                c.getString(DeletedPeopleQuery._SYNC_ID));
-        insert.bindLong(DeletedRawContactInsert.DELETED, 1);
-        insert.bindLong(DeletedRawContactInsert.AGGREGATION_MODE,
-                RawContacts.AGGREGATION_MODE_DISABLED);
-        insert(insert);
-    }
-
-    private void bindString(SQLiteStatement insert, int index, String string) {
-        if (string == null) {
-            insert.bindNull(index);
-        } else {
-            insert.bindString(index, string);
-        }
-    }
-
-    private long insert(SQLiteStatement insertStatement) {
-        long rowId = insertStatement.executeInsert();
-        if (rowId == 0) {
-            throw new RuntimeException("Insert failed");
-        }
-
-        mBatchCounter++;
-        if (mBatchCounter >= INSERT_BATCH_SIZE) {
-            mTargetDb.setTransactionSuccessful();
-            mTargetDb.endTransaction();
-            mTargetDb.beginTransaction();
-            mBatchCounter = 0;
-        }
-        return rowId;
-    }
-}
diff --git a/src/com/android/providers/contacts/MemoryCursor.java b/src/com/android/providers/contacts/MemoryCursor.java
new file mode 100644
index 0000000..23402a6
--- /dev/null
+++ b/src/com/android/providers/contacts/MemoryCursor.java
@@ -0,0 +1,48 @@
+/*
+ * 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.providers.contacts;
+
+import android.database.AbstractWindowedCursor;
+import android.database.Cursor;
+import android.database.CursorWindow;
+import android.database.DatabaseUtils;
+
+/**
+ * Implementation of an in-memory cursor backed by a cursor window.
+ */
+public class MemoryCursor extends AbstractWindowedCursor {
+
+    private final String[] mColumnNames;
+
+    public MemoryCursor(String name, String[] columnNames) {
+        setWindow(new CursorWindow(name));
+        mColumnNames = columnNames;
+    }
+
+    public void fillFromCursor(Cursor cursor) {
+        DatabaseUtils.cursorFillWindow(cursor, 0, getWindow());
+    }
+
+    @Override
+    public int getCount() {
+        return getWindow().getNumRows();
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        return mColumnNames;
+    }
+}
diff --git a/src/com/android/providers/contacts/NameSplitter.java b/src/com/android/providers/contacts/NameSplitter.java
index 27e5056..fd5b096 100644
--- a/src/com/android/providers/contacts/NameSplitter.java
+++ b/src/com/android/providers/contacts/NameSplitter.java
@@ -15,6 +15,8 @@
  */
 package com.android.providers.contacts;
 
+import com.android.providers.contacts.util.NeededForTesting;
+
 import android.content.ContentValues;
 import android.provider.ContactsContract.FullNameStyle;
 import android.provider.ContactsContract.PhoneticNameStyle;
@@ -106,6 +108,7 @@
             this.suffix = suffix;
         }
 
+        @NeededForTesting
         public String getPrefix() {
             return prefix;
         }
@@ -122,6 +125,7 @@
             return familyName;
         }
 
+        @NeededForTesting
         public String getSuffix() {
             return suffix;
         }
diff --git a/src/com/android/providers/contacts/ProfileDatabaseHelper.java b/src/com/android/providers/contacts/ProfileDatabaseHelper.java
index e2e5c90..9b707a3 100644
--- a/src/com/android/providers/contacts/ProfileDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ProfileDatabaseHelper.java
@@ -16,6 +16,8 @@
 
 package com.android.providers.contacts;
 
+import com.android.providers.contacts.util.NeededForTesting;
+
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.sqlite.SQLiteDatabase;
@@ -37,11 +39,11 @@
     private static ProfileDatabaseHelper sSingleton = null;
 
     /**
-     * Private constructor, callers except unit tests should obtain an instance through
-     * {@link #getInstance(android.content.Context)} instead.
+     * Returns a new instance for unit tests.
      */
-    ProfileDatabaseHelper(Context context) {
-        this(context, null, false);
+    @NeededForTesting
+    public static ProfileDatabaseHelper getNewInstanceForTest(Context context) {
+        return new ProfileDatabaseHelper(context, null, false);
     }
 
     private ProfileDatabaseHelper(
diff --git a/src/com/android/providers/contacts/ProfileProvider.java b/src/com/android/providers/contacts/ProfileProvider.java
index d40f1fd..8aa653d 100644
--- a/src/com/android/providers/contacts/ProfileProvider.java
+++ b/src/com/android/providers/contacts/ProfileProvider.java
@@ -15,6 +15,7 @@
  */
 package com.android.providers.contacts;
 
+import android.content.CancellationSignal;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
@@ -73,9 +74,16 @@
     @Override
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
+        return query(uri, projection, selection, selectionArgs, sortOrder, null);
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder, CancellationSignal cancellationSignal) {
         enforceReadPermission(uri);
         mDelegate.substituteDb(getDatabaseHelper().getReadableDatabase());
-        return mDelegate.queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1);
+        return mDelegate.queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1,
+                cancellationSignal);
     }
 
     @Override
diff --git a/src/com/android/providers/contacts/ProjectionMap.java b/src/com/android/providers/contacts/ProjectionMap.java
deleted file mode 100644
index f4c76d6..0000000
--- a/src/com/android/providers/contacts/ProjectionMap.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.providers.contacts;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A convenience wrapper for a projection map.  Makes it easier to create and use projection maps.
- */
-public class ProjectionMap extends HashMap<String, String> {
-
-    public static class Builder {
-
-        private ProjectionMap mMap = new ProjectionMap();
-
-        public Builder add(String column) {
-            mMap.putColumn(column, column);
-            return this;
-        }
-
-        public Builder add(String alias, String expression) {
-            mMap.putColumn(alias, expression + " AS " + alias);
-            return this;
-        }
-
-        public Builder addAll(ProjectionMap map) {
-            for (Map.Entry<String, String> entry : map.entrySet()) {
-                mMap.putColumn(entry.getKey(), entry.getValue());
-            }
-            return this;
-        }
-
-        public ProjectionMap build() {
-            String[] columns = new String[mMap.size()];
-            mMap.keySet().toArray(columns);
-            Arrays.sort(columns);
-            mMap.mColumns = columns;
-            return mMap;
-        }
-
-    }
-
-    private String[] mColumns;
-
-    public static Builder builder() {
-        return new Builder();
-    }
-
-    /**
-     * Returns a sorted array of all column names in the projection map.
-     */
-    public String[] getColumnNames() {
-        return mColumns;
-    }
-
-    private void putColumn(String alias, String column) {
-        super.put(alias, column);
-    }
-
-    @Override
-    public String put(String key, String value) {
-        throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void putAll(Map<? extends String, ? extends String> map) {
-        throw new UnsupportedOperationException();
-    }
-}
diff --git a/src/com/android/providers/contacts/SearchIndexManager.java b/src/com/android/providers/contacts/SearchIndexManager.java
index c989fdc..f1b7338 100644
--- a/src/com/android/providers/contacts/SearchIndexManager.java
+++ b/src/com/android/providers/contacts/SearchIndexManager.java
@@ -17,6 +17,7 @@
 
 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 
@@ -30,6 +31,7 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.ProviderStatus;
+import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -236,15 +238,15 @@
 
     private void rebuildIndex(SQLiteDatabase db) {
         mContactsProvider.setProviderStatus(ProviderStatus.STATUS_UPGRADING);
-        long start = SystemClock.currentThreadTimeMillis();
+        final long start = SystemClock.elapsedRealtime();
         int count = 0;
         try {
             mDbHelper.createSearchIndexTable(db);
-            count = buildIndex(db, null, false);
+            count = buildAndInsertIndex(db, null);
         } finally {
             mContactsProvider.setProviderStatus(ProviderStatus.STATUS_NORMAL);
 
-            long end = SystemClock.currentThreadTimeMillis();
+            final long end = SystemClock.elapsedRealtime();
             Log.i(TAG, "Rebuild contact search index in " + (end - start) + "ms, "
                     + count + " contacts");
         }
@@ -254,7 +256,7 @@
         mSb.setLength(0);
         mSb.append("(");
         if (!contactIds.isEmpty()) {
-            mSb.append(Data.CONTACT_ID + " IN (");
+            mSb.append(RawContacts.CONTACT_ID + " IN (");
             for (Long contactId : contactIds) {
                 mSb.append(contactId).append(",");
             }
@@ -266,7 +268,7 @@
             if (!contactIds.isEmpty()) {
                 mSb.append(" OR ");
             }
-            mSb.append(Data.RAW_CONTACT_ID + " IN (");
+            mSb.append(RawContactsColumns.CONCRETE_ID + " IN (");
             for (Long rawContactId : rawContactIds) {
                 mSb.append(rawContactId).append(",");
             }
@@ -275,10 +277,25 @@
         }
 
         mSb.append(")");
-        buildIndex(mDbHelper.getWritableDatabase(), mSb.toString(), true);
+
+        // The selection to select raw_contacts.
+        final String rawContactsSelection = mSb.toString();
+
+        // Remove affected search_index rows.
+        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        final int deleted = db.delete(Tables.SEARCH_INDEX,
+                SearchIndexColumns.CONTACT_ID + " IN (SELECT " +
+                    RawContacts.CONTACT_ID +
+                    " FROM " + Tables.RAW_CONTACTS +
+                    " WHERE " + rawContactsSelection +
+                    ")"
+                , null);
+
+        // Then rebuild index for them.
+        buildAndInsertIndex(db, rawContactsSelection);
     }
 
-    private int buildIndex(SQLiteDatabase db, String selection, boolean replace) {
+    private int buildAndInsertIndex(SQLiteDatabase db, String selection) {
         mSb.setLength(0);
         mSb.append(Data.CONTACT_ID + ", ");
         mSb.append("(CASE WHEN " + DataColumns.MIMETYPE_ID + "=");
@@ -307,7 +324,7 @@
                 long contactId = cursor.getLong(0);
                 if (contactId != currentContactId) {
                     if (currentContactId != -1) {
-                        saveContactIndex(db, currentContactId, mIndexBuilder, replace);
+                        insertIndexRow(db, currentContactId, mIndexBuilder);
                         count++;
                     }
                     currentContactId = contactId;
@@ -321,7 +338,7 @@
                 }
             }
             if (currentContactId != -1) {
-                saveContactIndex(db, currentContactId, mIndexBuilder, replace);
+                insertIndexRow(db, currentContactId, mIndexBuilder);
                 count++;
             }
         } finally {
@@ -330,24 +347,13 @@
         return count;
     }
 
-    private void saveContactIndex(
-            SQLiteDatabase db, long contactId, IndexBuilder builder, boolean replace) {
+    private void insertIndexRow(SQLiteDatabase db, long contactId, IndexBuilder builder) {
         mValues.clear();
         mValues.put(SearchIndexColumns.CONTENT, builder.getContent());
         mValues.put(SearchIndexColumns.NAME, builder.getName());
         mValues.put(SearchIndexColumns.TOKENS, builder.getTokens());
-        if (replace) {
-            mSelectionArgs1[0] = String.valueOf(contactId);
-            int count = db.update(Tables.SEARCH_INDEX, mValues,
-                    SearchIndexColumns.CONTACT_ID + "=CAST(? AS int)", mSelectionArgs1);
-            if (count == 0) {
-                mValues.put(SearchIndexColumns.CONTACT_ID, contactId);
-                db.insert(Tables.SEARCH_INDEX, null, mValues);
-            }
-        } else {
-            mValues.put(SearchIndexColumns.CONTACT_ID, contactId);
-            db.insert(Tables.SEARCH_INDEX, null, mValues);
-        }
+        mValues.put(SearchIndexColumns.CONTACT_ID, contactId);
+        db.insert(Tables.SEARCH_INDEX, null, mValues);
     }
     private int getSearchIndexVersion() {
         return Integer.parseInt(mDbHelper.getProperty(PROPERTY_SEARCH_INDEX_VERSION, "0"));
diff --git a/src/com/android/providers/contacts/TransactionContext.java b/src/com/android/providers/contacts/TransactionContext.java
index aee2fe2..4f880f7 100644
--- a/src/com/android/providers/contacts/TransactionContext.java
+++ b/src/com/android/providers/contacts/TransactionContext.java
@@ -32,7 +32,8 @@
 public class TransactionContext  {
 
     private final boolean mForProfile;
-    private HashMap<Long, AccountWithDataSet> mInsertedRawContacts = Maps.newHashMap();
+    /** Map from raw contact id to account Id */
+    private HashMap<Long, Long> mInsertedRawContactsAccounts = Maps.newHashMap();
     private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet();
     private HashSet<Long> mDirtyRawContacts = Sets.newHashSet();
     private HashSet<Long> mStaleSearchIndexRawContacts = Sets.newHashSet();
@@ -47,8 +48,8 @@
         return mForProfile;
     }
 
-    public void rawContactInserted(long rawContactId, AccountWithDataSet accountWithDataSet) {
-        mInsertedRawContacts.put(rawContactId, accountWithDataSet);
+    public void rawContactInserted(long rawContactId, long accountId) {
+        mInsertedRawContactsAccounts.put(rawContactId, accountId);
     }
 
     public void rawContactUpdated(long rawContactId) {
@@ -72,7 +73,7 @@
     }
 
     public Set<Long> getInsertedRawContactIds() {
-        return mInsertedRawContacts.keySet();
+        return mInsertedRawContactsAccounts.keySet();
     }
 
     public Set<Long> getUpdatedRawContactIds() {
@@ -95,16 +96,16 @@
         return mUpdatedSyncStates.entrySet();
     }
 
-    public AccountWithDataSet getAccountWithDataSetForRawContact(long rawContactId) {
-        return mInsertedRawContacts.get(rawContactId);
+    public Long getAccountIdOrNullForRawContact(long rawContactId) {
+        return mInsertedRawContactsAccounts.get(rawContactId);
     }
 
     public boolean isNewRawContact(long rawContactId) {
-        return mInsertedRawContacts.containsKey(rawContactId);
+        return mInsertedRawContactsAccounts.containsKey(rawContactId);
     }
 
     public void clear() {
-        mInsertedRawContacts.clear();
+        mInsertedRawContactsAccounts.clear();
         mUpdatedRawContacts.clear();
         mUpdatedSyncStates.clear();
         mDirtyRawContacts.clear();
diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java
index ec1fa38..79d8f92 100644
--- a/src/com/android/providers/contacts/VoicemailContentProvider.java
+++ b/src/com/android/providers/contacts/VoicemailContentProvider.java
@@ -49,8 +49,6 @@
  */
 public class VoicemailContentProvider extends ContentProvider
         implements VoicemailTable.DelegateHelper {
-    private static final String TAG = "VoicemailContentProvider";
-
     private VoicemailPermissions mVoicemailPermissions;
     private VoicemailTable.Delegate mVoicemailContentTable;
     private VoicemailTable.Delegate mVoicemailStatusTable;
diff --git a/src/com/android/providers/contacts/VoicemailContentTable.java b/src/com/android/providers/contacts/VoicemailContentTable.java
index 4d23531..3b72653 100644
--- a/src/com/android/providers/contacts/VoicemailContentTable.java
+++ b/src/com/android/providers/contacts/VoicemailContentTable.java
@@ -19,6 +19,7 @@
 import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
 import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
 
+import com.android.common.content.ProjectionMap;
 import com.android.providers.contacts.VoicemailContentProvider.UriData;
 import com.android.providers.contacts.util.CloseUtils;
 
@@ -245,7 +246,7 @@
 
     /** Creates a clause to restrict the selection to only voicemail call type.*/
     private String getCallTypeClause() {
-        return getEqualityClause(Calls.TYPE, String.valueOf(Calls.VOICEMAIL_TYPE));
+        return getEqualityClause(Calls.TYPE, Calls.VOICEMAIL_TYPE);
     }
 
     private DatabaseModifier getDatabaseModifier(SQLiteDatabase db) {
diff --git a/src/com/android/providers/contacts/VoicemailStatusTable.java b/src/com/android/providers/contacts/VoicemailStatusTable.java
index 24714ed..a0a61ba 100644
--- a/src/com/android/providers/contacts/VoicemailStatusTable.java
+++ b/src/com/android/providers/contacts/VoicemailStatusTable.java
@@ -18,6 +18,7 @@
 
 import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
 
+import com.android.common.content.ProjectionMap;
 import com.android.providers.contacts.VoicemailContentProvider.UriData;
 
 import android.content.ContentUris;
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/aggregation/ContactAggregator.java
similarity index 95%
rename from src/com/android/providers/contacts/ContactAggregator.java
rename to src/com/android/providers/contacts/aggregation/ContactAggregator.java
index cad5d50..a235ce2 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/aggregation/ContactAggregator.java
@@ -14,9 +14,14 @@
  * limitations under the License
  */
 
-package com.android.providers.contacts;
+package com.android.providers.contacts.aggregation;
 
+import com.android.providers.contacts.CommonNicknameCache;
+import com.android.providers.contacts.ContactLookupKey;
+import com.android.providers.contacts.ContactMatcher;
 import com.android.providers.contacts.ContactMatcher.MatchScore;
+import com.android.providers.contacts.ContactsDatabaseHelper;
+import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
@@ -28,6 +33,13 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
 import com.android.providers.contacts.ContactsDatabaseHelper.Views;
+import com.android.providers.contacts.ContactsProvider2;
+import com.android.providers.contacts.NameLookupBuilder;
+import com.android.providers.contacts.NameNormalizer;
+import com.android.providers.contacts.NameSplitter;
+import com.android.providers.contacts.PhotoPriorityResolver;
+import com.android.providers.contacts.ReorderingCursorWrapper;
+import com.android.providers.contacts.TransactionContext;
 
 import android.database.Cursor;
 import android.database.DatabaseUtils;
@@ -372,16 +384,13 @@
     private interface AggregationQuery {
         String SQL =
                 "SELECT " + RawContacts._ID + "," + RawContacts.CONTACT_ID +
-                        ", " + RawContacts.ACCOUNT_TYPE + "," + RawContacts.ACCOUNT_NAME +
-                        ", " + RawContacts.DATA_SET +
+                        ", " + RawContactsColumns.ACCOUNT_ID +
                 " FROM " + Tables.RAW_CONTACTS +
                 " WHERE " + RawContacts._ID + " IN(";
 
         int _ID = 0;
         int CONTACT_ID = 1;
-        int ACCOUNT_TYPE = 2;
-        int ACCOUNT_NAME = 3;
-        int DATA_SET = 4;
+        int ACCOUNT_ID = 2;
     }
 
     /**
@@ -418,9 +427,7 @@
 
         long rawContactIds[] = new long[count];
         long contactIds[] = new long[count];
-        String accountTypes[] = new String[count];
-        String accountNames[] = new String[count];
-        String dataSets[] = new String[count];
+        long accountIds[] = new long[count];
         Cursor c = db.rawQuery(mSb.toString(), selectionArgs);
         try {
             count = c.getCount();
@@ -428,9 +435,7 @@
             while (c.moveToNext()) {
                 rawContactIds[index] = c.getLong(AggregationQuery._ID);
                 contactIds[index] = c.getLong(AggregationQuery.CONTACT_ID);
-                accountTypes[index] = c.getString(AggregationQuery.ACCOUNT_TYPE);
-                accountNames[index] = c.getString(AggregationQuery.ACCOUNT_NAME);
-                dataSets[index] = c.getString(AggregationQuery.DATA_SET);
+                accountIds[index] = c.getLong(AggregationQuery.ACCOUNT_ID);
                 index++;
             }
         } finally {
@@ -438,8 +443,8 @@
         }
 
         for (int i = 0; i < count; i++) {
-            aggregateContact(txContext, db, rawContactIds[i], accountTypes[i], accountNames[i],
-                    dataSets[i], contactIds[i], mCandidates, mMatcher);
+            aggregateContact(txContext, db, rawContactIds[i], accountIds[i], contactIds[i],
+                    mCandidates, mMatcher);
         }
 
         long elapsedTime = System.currentTimeMillis() - start;
@@ -555,7 +560,7 @@
         return contactId;
     }
 
-    public long insertContact(SQLiteDatabase db, long rawContactId) {
+    protected long insertContact(SQLiteDatabase db, long rawContactId) {
         mSelectionArgs1[0] = String.valueOf(rawContactId);
         computeAggregateData(db, mRawContactsQueryByRawContactId, mSelectionArgs1, mContactInsert);
         return mContactInsert.executeInsert();
@@ -565,16 +570,14 @@
         public static final String TABLE = Tables.RAW_CONTACTS;
 
         public static final String[] COLUMNS = {
-                RawContacts.CONTACT_ID, RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_NAME,
-                RawContacts.DATA_SET
+                RawContacts.CONTACT_ID,
+                RawContactsColumns.ACCOUNT_ID
         };
 
         public static final String SELECTION = RawContacts._ID + "=?";
 
         public static final int CONTACT_ID = 0;
-        public static final int ACCOUNT_TYPE = 1;
-        public static final int ACCOUNT_NAME = 2;
-        public static final int DATA_SET = 3;
+        public static final int ACCOUNT_ID = 1;
     }
 
     public void aggregateContact(
@@ -587,9 +590,7 @@
         ContactMatcher matcher = new ContactMatcher();
 
         long contactId = 0;
-        String accountName = null;
-        String accountType = null;
-        String dataSet = null;
+        long accountId = 0;
         mSelectionArgs1[0] = String.valueOf(rawContactId);
         Cursor cursor = db.query(RawContactIdAndAccountQuery.TABLE,
                 RawContactIdAndAccountQuery.COLUMNS, RawContactIdAndAccountQuery.SELECTION,
@@ -597,15 +598,13 @@
         try {
             if (cursor.moveToFirst()) {
                 contactId = cursor.getLong(RawContactIdAndAccountQuery.CONTACT_ID);
-                accountType = cursor.getString(RawContactIdAndAccountQuery.ACCOUNT_TYPE);
-                accountName = cursor.getString(RawContactIdAndAccountQuery.ACCOUNT_NAME);
-                dataSet = cursor.getString(RawContactIdAndAccountQuery.DATA_SET);
+                accountId = cursor.getLong(RawContactIdAndAccountQuery.ACCOUNT_ID);
             }
         } finally {
             cursor.close();
         }
 
-        aggregateContact(txContext, db, rawContactId, accountType, accountName, dataSet, contactId,
+        aggregateContact(txContext, db, rawContactId, accountId, contactId,
                 candidates, matcher);
     }
 
@@ -644,8 +643,8 @@
      * with the highest match score.  If no such contact is found, creates a new contact.
      */
     private synchronized void aggregateContact(TransactionContext txContext, SQLiteDatabase db,
-            long rawContactId, String accountType, String accountName, String dataSet,
-            long currentContactId, MatchCandidateList candidates, ContactMatcher matcher) {
+            long rawContactId, long accountId, long currentContactId, MatchCandidateList candidates,
+            ContactMatcher matcher) {
 
         int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
 
@@ -675,8 +674,7 @@
                 // the same account, not only will we not join it, but also we will split
                 // that other aggregate
                 if (contactId != -1 && contactId != currentContactId &&
-                        containsRawContactsFromAccount(db, contactId, accountType, accountName,
-                                dataSet)) {
+                        containsRawContactsFromAccount(db, contactId, accountId)) {
                     contactIdToSplit = contactId;
                     contactId = -1;
                 }
@@ -739,41 +737,13 @@
      * Returns true if the aggregate contains has any raw contacts from the specified account.
      */
     private boolean containsRawContactsFromAccount(
-            SQLiteDatabase db, long contactId, String accountType, String accountName,
-            String dataSet) {
-        String query;
-        String[] args;
-        if (accountType == null) {
-            query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS +
-                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
-                    " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL " +
-                    " AND " + RawContacts.ACCOUNT_NAME + " IS NULL " +
-                    " AND " + RawContacts.DATA_SET + " IS NULL";
-            args = mSelectionArgs1;
-            args[0] = String.valueOf(contactId);
-        } else if (dataSet == null) {
-            query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS +
-                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
-                    " AND " + RawContacts.ACCOUNT_TYPE + "=?" +
-                    " AND " + RawContacts.ACCOUNT_NAME + "=?" +
-                    " AND " + RawContacts.DATA_SET + " IS NULL";
-            args = mSelectionArgs3;
-            args[0] = String.valueOf(contactId);
-            args[1] = accountType;
-            args[2] = accountName;
-        } else {
-            query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS +
-                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
-                    " AND " + RawContacts.ACCOUNT_TYPE + "=?" +
-                    " AND " + RawContacts.ACCOUNT_NAME + "=?" +
-                    " AND " + RawContacts.DATA_SET + "=?";
-            args = mSelectionArgs4;
-            args[0] = String.valueOf(contactId);
-            args[1] = accountType;
-            args[2] = accountName;
-            args[3] = dataSet;
-        }
-        Cursor cursor = db.rawQuery(query, args);
+            SQLiteDatabase db, long contactId, long accountId) {
+        final String query = "SELECT count(_id) FROM " + Tables.RAW_CONTACTS +
+                " WHERE " + RawContacts.CONTACT_ID + "=?" +
+                " AND " + RawContactsColumns.ACCOUNT_ID + "=?";
+        Cursor cursor = db.rawQuery(query, new String[] {
+                Long.toString(contactId), Long.toString(accountId)
+                });
         try {
             cursor.moveToFirst();
             return cursor.getInt(0) != 0;
@@ -1540,9 +1510,9 @@
                         + RawContactsColumns.CONCRETE_ID + ","
                         + RawContactsColumns.DISPLAY_NAME + ","
                         + RawContactsColumns.DISPLAY_NAME_SOURCE + ","
-                        + RawContacts.ACCOUNT_TYPE + ","
-                        + RawContacts.ACCOUNT_NAME + ","
-                        + RawContacts.DATA_SET + ","
+                        + AccountsColumns.CONCRETE_ACCOUNT_TYPE + ","
+                        + AccountsColumns.CONCRETE_ACCOUNT_NAME + ","
+                        + AccountsColumns.CONCRETE_DATA_SET + ","
                         + RawContacts.SOURCE_ID + ","
                         + RawContacts.CUSTOM_RINGTONE + ","
                         + RawContacts.SEND_TO_VOICEMAIL + ","
@@ -1555,6 +1525,9 @@
                         + Data.IS_SUPER_PRIMARY + ","
                         + Photo.PHOTO_FILE_ID +
                 " FROM " + Tables.RAW_CONTACTS +
+                " JOIN " + Tables.ACCOUNTS + " ON ("
+                    + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
+                    + ")" +
                 " LEFT OUTER JOIN " + Tables.DATA +
                 " ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
                         + " AND ((" + DataColumns.MIMETYPE_ID + "=%d"
@@ -1848,7 +1821,7 @@
 
     private interface PhotoIdQuery {
         final String[] COLUMNS = new String[] {
-            RawContacts.ACCOUNT_TYPE,
+            AccountsColumns.CONCRETE_ACCOUNT_TYPE,
             DataColumns.CONCRETE_ID,
             Data.IS_SUPER_PRIMARY,
             Photo.PHOTO_FILE_ID,
@@ -1873,7 +1846,11 @@
 
         long photoMimeType = mDbHelper.getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
 
-        String tables = Tables.RAW_CONTACTS + " JOIN " + Tables.DATA + " ON("
+        String tables = Tables.RAW_CONTACTS
+                + " JOIN " + Tables.ACCOUNTS + " ON ("
+                    + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
+                    + ")"
+                + " JOIN " + Tables.DATA + " ON("
                 + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
                 + " AND (" + DataColumns.MIMETYPE_ID + "=" + photoMimeType + " AND "
                         + Photo.PHOTO + " NOT NULL))";
@@ -2082,6 +2059,7 @@
     }
 
     private interface LookupKeyQuery {
+        String TABLE = Views.RAW_CONTACTS;
         String[] COLUMNS = new String[] {
             RawContacts._ID,
             RawContactsColumns.DISPLAY_NAME,
@@ -2122,7 +2100,7 @@
     protected String computeLookupKeyForContact(SQLiteDatabase db, long contactId) {
         StringBuilder sb = new StringBuilder();
         mSelectionArgs1[0] = String.valueOf(contactId);
-        final Cursor c = db.query(Views.RAW_CONTACTS, LookupKeyQuery.COLUMNS,
+        final Cursor c = db.query(LookupKeyQuery.TABLE, LookupKeyQuery.COLUMNS,
                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1, null, null, RawContacts._ID);
         try {
             while (c.moveToNext()) {
@@ -2143,7 +2121,7 @@
      * Execute {@link SQLiteStatement} that will update the
      * {@link Contacts#STARRED} flag for the given {@link RawContacts#_ID}.
      */
-    protected void updateStarred(long rawContactId) {
+    public void updateStarred(long rawContactId) {
         long contactId = mDbHelper.getContactId(rawContactId);
         if (contactId == 0) {
             return;
diff --git a/src/com/android/providers/contacts/ProfileAggregator.java b/src/com/android/providers/contacts/aggregation/ProfileAggregator.java
similarity index 87%
rename from src/com/android/providers/contacts/ProfileAggregator.java
rename to src/com/android/providers/contacts/aggregation/ProfileAggregator.java
index e4f8a3a..6126184 100644
--- a/src/com/android/providers/contacts/ProfileAggregator.java
+++ b/src/com/android/providers/contacts/aggregation/ProfileAggregator.java
@@ -13,13 +13,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License
  */
-package com.android.providers.contacts;
+package com.android.providers.contacts.aggregation;
 
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteDoneException;
 import android.database.sqlite.SQLiteStatement;
 import android.provider.ContactsContract.Contacts;
+
+import com.android.providers.contacts.CommonNicknameCache;
+import com.android.providers.contacts.ContactLookupKey;
+import com.android.providers.contacts.ContactsDatabaseHelper;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.ContactsProvider2;
+import com.android.providers.contacts.NameSplitter;
+import com.android.providers.contacts.PhotoPriorityResolver;
+import com.android.providers.contacts.TransactionContext;
 
 /**
  * A version of the ContactAggregator for use against the profile database.
diff --git a/src/com/android/providers/contacts/util/DbQueryUtils.java b/src/com/android/providers/contacts/util/DbQueryUtils.java
index 58c8bb1..c853a96 100644
--- a/src/com/android/providers/contacts/util/DbQueryUtils.java
+++ b/src/com/android/providers/contacts/util/DbQueryUtils.java
@@ -34,8 +34,13 @@
         return getClauseWithOperator(field, "=", value);
     }
 
+    /** Returns a WHERE clause asserting equality of a field to a value. */
+    public static String getEqualityClause(String field, long value) {
+        return getClauseWithOperator(field, "=", value);
+    }
+
     /** Returns a WHERE clause asserting in-equality of a field to a value. */
-    public static String getInequalityClause(String field, String value) {
+    public static String getInequalityClause(String field, long value) {
         return getClauseWithOperator(field, "!=", value);
     }
 
@@ -49,6 +54,16 @@
         return clause.toString();
     }
 
+    private static String getClauseWithOperator(String field, String operator, long value) {
+        StringBuilder clause = new StringBuilder();
+        clause.append("(");
+        clause.append(field);
+        clause.append(" ").append(operator).append(" ");
+        clause.append(value);
+        clause.append(")");
+        return clause.toString();
+    }
+
     /** Concatenates any number of clauses using "AND". */
     public static String concatenateClauses(String... clauses) {
         StringBuilder builder = new StringBuilder();
diff --git a/src/com/android/providers/contacts/util/NeededForTesting.java b/src/com/android/providers/contacts/util/NeededForTesting.java
new file mode 100644
index 0000000..0eb418e
--- /dev/null
+++ b/src/com/android/providers/contacts/util/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.providers.contacts.util;
+
+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/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 33a39ec..b3b3c28 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -41,6 +41,7 @@
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.Contacts;
@@ -337,6 +338,23 @@
         return resultUri;
     }
 
+    protected Uri insertSipAddress(long rawContactId, String sipAddress) {
+        return insertSipAddress(rawContactId, sipAddress, false);
+    }
+
+    protected Uri insertSipAddress(long rawContactId, String sipAddress, boolean primary) {
+        ContentValues values = new ContentValues();
+        values.put(Data.RAW_CONTACT_ID, rawContactId);
+        values.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+        values.put(SipAddress.SIP_ADDRESS, sipAddress);
+        if (primary) {
+            values.put(Data.IS_PRIMARY, 1);
+        }
+
+        Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
+        return resultUri;
+    }
+
     protected Uri insertNickname(long rawContactId, String nickname) {
         ContentValues values = new ContentValues();
         values.put(Data.RAW_CONTACT_ID, rawContactId);
@@ -1055,9 +1073,7 @@
 
     private boolean equalsWithExpectedValues(Cursor cursor, ContentValues expectedValues,
             StringBuilder msgBuffer) {
-        Set<Map.Entry<String, Object>> entries = expectedValues.valueSet();
-        for (Map.Entry<String, Object> entry : entries) {
-            String column = entry.getKey();
+        for (String column : expectedValues.keySet()) {
             int index = cursor.getColumnIndex(column);
             if (index == -1) {
                 msgBuffer.append("No such column: ").append(column);
diff --git a/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java b/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
index 6a45872..bd0e814 100644
--- a/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/BaseVoicemailProviderTest.java
@@ -157,7 +157,7 @@
 
         @Override
         protected ContactsDatabaseHelper getDatabaseHelper(Context context) {
-            return new ContactsDatabaseHelper(context);
+            return ContactsDatabaseHelper.getNewInstanceForTest(context);
         }
 
         @Override
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
index b005d5d..02aebb7 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -337,7 +337,7 @@
         @Override
         protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
             if (mDbHelper == null) {
-                mDbHelper = new ContactsDatabaseHelper(context);
+                mDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context);
             }
             return mDbHelper;
         }
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
deleted file mode 100644
index 013a706..0000000
--- a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.contacts;
-
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.res.Resources;
-import android.os.Debug;
-import android.provider.ContactsContract;
-import android.test.AndroidTestCase;
-import android.test.IsolatedContext;
-import android.test.RenamingDelegatingContext;
-import android.test.mock.MockContentResolver;
-import android.test.mock.MockContext;
-import android.test.suitebuilder.annotation.LargeTest;
-import android.util.Log;
-
-/**
- * Performance test for {@link ContactAggregator}.
- *
- * Run the test like this:
- * <code>
- * adb push <large contacts2.db> \
- *         data/data/com.android.providers.contacts/databases/perf.contacts2.db
- * adb shell am instrument \
- *         -e class com.android.providers.contacts.ContactAggregatorPerformanceTest \
- *         -w com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
- * </code>
- */
-@LargeTest
-public class ContactAggregatorPerformanceTest extends AndroidTestCase {
-
-    private static final String TAG = "ContactAggregatorPerformanceTest";
-    private static final boolean TRACE = false;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        SynchronousContactsProvider2.resetOpenHelper();
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-        SynchronousContactsProvider2.resetOpenHelper();
-    }
-
-    public void testPerformance() {
-        final Context targetContext = getContext();
-        MockContentResolver resolver = new MockContentResolver();
-        MockContext context = new MockContext() {
-            @Override
-            public Resources getResources() {
-                return targetContext.getResources();
-            }
-
-            @Override
-            public String getPackageName() {
-                return "no.package";
-            }
-
-            @Override
-            public ApplicationInfo getApplicationInfo() {
-                ApplicationInfo ai = new ApplicationInfo();
-                ai.packageName = "contactsTestPackage";
-                return ai;
-            }
-        };
-        RenamingDelegatingContext targetContextWrapper =
-                new RenamingDelegatingContext(context, targetContext, "perf.");
-        targetContextWrapper.makeExistingFilesAndDbsAccessible();
-        IsolatedContext providerContext = new IsolatedContext(resolver, targetContextWrapper);
-        SynchronousContactsProvider2 provider = new SynchronousContactsProvider2();
-        provider.setDataWipeEnabled(false);
-        provider.attachInfo(providerContext, null);
-        resolver.addProvider(ContactsContract.AUTHORITY, provider);
-
-        long rawContactCount = provider.getRawContactCount();
-        if (rawContactCount == 0) {
-            Log.w(TAG, "The test has not been set up. Use this command to copy a contact db"
-                    + " to the device:\nadb push <large contacts2.db> "
-                    + "data/data/com.android.providers.contacts/databases/perf.contacts2.db");
-            return;
-        }
-
-        provider.prepareForFullAggregation(500);
-        rawContactCount = provider.getRawContactCount();
-        long start = System.currentTimeMillis();
-        if (TRACE) {
-            Debug.startMethodTracing("aggregation");
-        }
-
-        // TODO
-//        provider.aggregate();
-
-        if (TRACE) {
-            Debug.stopMethodTracing();
-        }
-        long end = System.currentTimeMillis();
-        long contactCount = provider.getContactCount();
-
-        Log.i(TAG, String.format("Aggregated contacts in %d ms.\n" +
-                "Raw contacts: %d\n" +
-                "Aggregated contacts: %d\n" +
-                "Per raw contact: %.3f",
-                end-start,
-                rawContactCount,
-                contactCount,
-                ((double)(end-start)/rawContactCount)));
-
-        provider.getDatabaseHelper().close();
-    }
-}
-
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index e93bb7b..038eb97 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -16,6 +16,7 @@
 
 package com.android.providers.contacts;
 
+import com.android.providers.contacts.util.MockSharedPreferences;
 import com.google.android.collect.Sets;
 
 import android.accounts.Account;
@@ -30,6 +31,7 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
@@ -157,7 +159,8 @@
 
         RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(context,
                 overallContext, FILENAME_PREFIX);
-        mProviderContext = new IsolatedContext(resolver, targetContextWrapper){
+        mProviderContext = new IsolatedContext(resolver, targetContextWrapper) {
+            private final MockSharedPreferences mPrefs = new MockSharedPreferences();
 
             @Override
             public File getFilesDir() {
@@ -175,6 +178,11 @@
                 }
                 return super.getSystemService(name);
             }
+
+            @Override
+            public SharedPreferences getSharedPreferences(String name, int mode) {
+                return mPrefs;
+            }
         };
 
         mMockAccountManager = new MockAccountManager(mProviderContext);
diff --git a/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
new file mode 100644
index 0000000..a9d8a36
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactsDatabaseHelperTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.providers.contacts;
+
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.google.android.collect.Sets;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.Set;
+
+@SmallTest
+public class ContactsDatabaseHelperTest extends BaseContactsProvider2Test {
+    private ContactsDatabaseHelper mDbHelper;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mDbHelper = getContactsProvider().getDatabaseHelper(getContext());
+    }
+
+    public void testGetOrCreateAccountId() {
+        final AccountWithDataSet a1 = null;
+        final AccountWithDataSet a2 = new AccountWithDataSet("a", null, null);
+        final AccountWithDataSet a3 = new AccountWithDataSet(null, "b", null);
+        final AccountWithDataSet a4 = new AccountWithDataSet(null, null, "c");
+        final AccountWithDataSet a5 = new AccountWithDataSet("a", "b", "c");
+
+        // First, there's no accounts.  getAccountIdOrNull() always returns null.
+        assertNull(mDbHelper.getAccountIdOrNull(a1));
+        assertNull(mDbHelper.getAccountIdOrNull(a2));
+        assertNull(mDbHelper.getAccountIdOrNull(a3));
+        assertNull(mDbHelper.getAccountIdOrNull(a4));
+        assertNull(mDbHelper.getAccountIdOrNull(a5));
+
+        // getOrCreateAccountId should create accounts.
+        final long a1id = mDbHelper.getOrCreateAccountIdInTransaction(a1);
+        final long a2id = mDbHelper.getOrCreateAccountIdInTransaction(a2);
+        final long a3id = mDbHelper.getOrCreateAccountIdInTransaction(a3);
+        final long a4id = mDbHelper.getOrCreateAccountIdInTransaction(a4);
+        final long a5id = mDbHelper.getOrCreateAccountIdInTransaction(a5);
+
+        // The IDs should be all positive and unique.
+        assertTrue(a1id > 0);
+        assertTrue(a2id > 0);
+        assertTrue(a3id > 0);
+        assertTrue(a4id > 0);
+        assertTrue(a5id > 0);
+
+        final Set<Long> ids = Sets.newHashSet();
+        ids.add(a1id);
+        ids.add(a2id);
+        ids.add(a3id);
+        ids.add(a4id);
+        ids.add(a5id);
+        assertEquals(5, ids.size());
+
+        // Second call: This time getOrCreateAccountId will return the existing IDs.
+        assertEquals(a1id, mDbHelper.getOrCreateAccountIdInTransaction(a1));
+        assertEquals(a2id, mDbHelper.getOrCreateAccountIdInTransaction(a2));
+        assertEquals(a3id, mDbHelper.getOrCreateAccountIdInTransaction(a3));
+        assertEquals(a4id, mDbHelper.getOrCreateAccountIdInTransaction(a4));
+        assertEquals(a5id, mDbHelper.getOrCreateAccountIdInTransaction(a5));
+
+        // Now getAccountIdOrNull() returns IDs too.
+        assertEquals((Long) a1id, mDbHelper.getAccountIdOrNull(a1));
+        assertEquals((Long) a2id, mDbHelper.getAccountIdOrNull(a2));
+        assertEquals((Long) a3id, mDbHelper.getAccountIdOrNull(a3));
+        assertEquals((Long) a4id, mDbHelper.getAccountIdOrNull(a4));
+        assertEquals((Long) a5id, mDbHelper.getAccountIdOrNull(a5));
+
+        // null and AccountWithDataSet.NULL should be treated as the same thing.
+        assertEquals(a1id, mDbHelper.getOrCreateAccountIdInTransaction(AccountWithDataSet.LOCAL));
+        assertEquals((Long) a1id, mDbHelper.getAccountIdOrNull(AccountWithDataSet.LOCAL));
+
+        // Remove all accounts.
+        mDbHelper.getWritableDatabase().execSQL("delete from " + Tables.ACCOUNTS);
+
+        assertNull(mDbHelper.getAccountIdOrNull(AccountWithDataSet.LOCAL));
+        assertNull(mDbHelper.getAccountIdOrNull(a1));
+        assertNull(mDbHelper.getAccountIdOrNull(a2));
+        assertNull(mDbHelper.getAccountIdOrNull(a3));
+        assertNull(mDbHelper.getAccountIdOrNull(a4));
+        assertNull(mDbHelper.getAccountIdOrNull(a5));
+
+        // Logically same as a5, but physically different object.
+        final AccountWithDataSet a5b = new AccountWithDataSet("a", "b", "c");
+        // a5 and a5b should have the same ID.
+        assertEquals(
+                mDbHelper.getOrCreateAccountIdInTransaction(a5),
+                mDbHelper.getOrCreateAccountIdInTransaction(a5b));
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsMockResources.java b/tests/src/com/android/providers/contacts/ContactsMockResources.java
index d1ec817..248d6da 100644
--- a/tests/src/com/android/providers/contacts/ContactsMockResources.java
+++ b/tests/src/com/android/providers/contacts/ContactsMockResources.java
@@ -16,9 +16,8 @@
 
 package com.android.providers.contacts;
 
-import com.google.common.collect.Maps;
+import com.google.android.collect.Maps;
 
-import android.content.res.Resources.NotFoundException;
 import android.test.mock.MockResources;
 
 import java.util.Map;
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index cbb1c49..bef077c 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -19,9 +19,11 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
 import com.android.providers.contacts.tests.R;
 import com.google.android.collect.Lists;
+import com.google.android.collect.Sets;
 
 import android.accounts.Account;
 import android.content.ContentProviderOperation;
@@ -36,12 +38,14 @@
 import android.os.AsyncTask;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.ContactCounts;
@@ -78,6 +82,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
 
 /**
  * Unit tests for {@link ContactsProvider2}.
@@ -556,6 +561,7 @@
                 Groups.SYNC4,
                 Groups.SUMMARY_COUNT,
                 Groups.SUMMARY_WITH_PHONES,
+                Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT,
         });
     }
 
@@ -619,6 +625,7 @@
 
         values.put(RawContacts.ACCOUNT_NAME, "a");
         values.put(RawContacts.ACCOUNT_TYPE, "b");
+        values.put(RawContacts.DATA_SET, "ds");
         values.put(RawContacts.SOURCE_ID, "c");
         values.put(RawContacts.VERSION, 42);
         values.put(RawContacts.DIRTY, 1);
@@ -917,15 +924,33 @@
     }
 
     public void testPhonesFilterQuery() {
-        long rawContactId1 = createRawContactWithName("Hot", "Tamale", ACCOUNT_1);
+        testPhonesFilterQueryInter(Phone.CONTENT_FILTER_URI);
+    }
+
+    /**
+     * A convenient method for {@link #testPhonesFilterQuery()} and
+     * {@link #testCallablesFilterQuery()}.
+     *
+     * This confirms if both URIs return identical results for phone-only contacts and
+     * appropriately different results for contacts with sip addresses.
+     *
+     * @param baseFilterUri Either {@link Phone#CONTENT_FILTER_URI} or
+     * {@link Callable#CONTENT_FILTER_URI}.
+     */
+    private void testPhonesFilterQueryInter(Uri baseFilterUri) {
+        assertTrue("Unsupported Uri (" + baseFilterUri + ")",
+                Phone.CONTENT_FILTER_URI.equals(baseFilterUri)
+                        || Callable.CONTENT_FILTER_URI.equals(baseFilterUri));
+
+        final long rawContactId1 = createRawContactWithName("Hot", "Tamale", ACCOUNT_1);
         insertPhoneNumber(rawContactId1, "1-800-466-4411");
 
-        long rawContactId2 = createRawContactWithName("Chilled", "Guacamole", ACCOUNT_2);
+        final long rawContactId2 = createRawContactWithName("Chilled", "Guacamole", ACCOUNT_2);
         insertPhoneNumber(rawContactId2, "1-800-466-5432");
         insertPhoneNumber(rawContactId2, "0@example.com", false, Phone.TYPE_PAGER);
         insertPhoneNumber(rawContactId2, "1@example.com", false, Phone.TYPE_PAGER);
 
-        Uri filterUri1 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "tamale");
+        final Uri filterUri1 = Uri.withAppendedPath(baseFilterUri, "tamale");
         ContentValues values = new ContentValues();
         values.put(Contacts.DISPLAY_NAME, "Hot Tamale");
         values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
@@ -934,16 +959,16 @@
         values.putNull(Phone.LABEL);
         assertStoredValuesWithProjection(filterUri1, values);
 
-        Uri filterUri2 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "1-800-GOOG-411");
+        final Uri filterUri2 = Uri.withAppendedPath(baseFilterUri, "1-800-GOOG-411");
         assertStoredValues(filterUri2, values);
 
-        Uri filterUri3 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "18004664");
+        final Uri filterUri3 = Uri.withAppendedPath(baseFilterUri, "18004664");
         assertStoredValues(filterUri3, values);
 
-        Uri filterUri4 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "encilada");
+        final Uri filterUri4 = Uri.withAppendedPath(baseFilterUri, "encilada");
         assertEquals(0, getCount(filterUri4, null, null));
 
-        Uri filterUri5 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "*");
+        final Uri filterUri5 = Uri.withAppendedPath(baseFilterUri, "*");
         assertEquals(0, getCount(filterUri5, null, null));
 
         ContentValues values1 = new ContentValues();
@@ -967,7 +992,42 @@
         values3.put(Phone.TYPE, Phone.TYPE_PAGER);
         values3.putNull(Phone.LABEL);
 
-        Uri filterUri6 = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, "Chilled");
+        final Uri filterUri6 = Uri.withAppendedPath(baseFilterUri, "Chilled");
+        assertStoredValues(filterUri6, new ContentValues[] {values1, values2, values3} );
+
+        // Insert a SIP address. From here, Phone URI and Callable URI may return different results
+        // than each other.
+        insertSipAddress(rawContactId1, "sip_hot_tamale@example.com");
+        insertSipAddress(rawContactId1, "sip:sip_hot@example.com");
+
+        final Uri filterUri7 = Uri.withAppendedPath(baseFilterUri, "sip_hot");
+        final Uri filterUri8 = Uri.withAppendedPath(baseFilterUri, "sip_hot_tamale");
+        if (Callable.CONTENT_FILTER_URI.equals(baseFilterUri)) {
+            ContentValues values4 = new ContentValues();
+            values4.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+            values4.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+            values4.put(SipAddress.SIP_ADDRESS, "sip_hot_tamale@example.com");
+
+            ContentValues values5 = new ContentValues();
+            values5.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+            values5.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+            values5.put(SipAddress.SIP_ADDRESS, "sip:sip_hot@example.com");
+            assertStoredValues(filterUri1, new ContentValues[] {values, values4, values5});
+
+            assertStoredValues(filterUri7, new ContentValues[] {values4, values5});
+            assertStoredValues(filterUri8, values4);
+        } else {
+            // Sip address should not affect Phone URI.
+            assertStoredValuesWithProjection(filterUri1, values);
+            assertEquals(0, getCount(filterUri7, null, null));
+        }
+
+        // Sanity test. Run tests for "Chilled Guacamole" again and see nothing changes
+        // after the Sip address being inserted.
+        assertStoredValues(filterUri2, values);
+        assertStoredValues(filterUri3, values);
+        assertEquals(0, getCount(filterUri4, null, null));
+        assertEquals(0, getCount(filterUri5, null, null));
         assertStoredValues(filterUri6, new ContentValues[] {values1, values2, values3} );
     }
 
@@ -1171,6 +1231,43 @@
         assertNetworkNotified(true);
     }
 
+    /** Tests if {@link Callable#CONTENT_URI} returns both phones and sip addresses. */
+    public void testCallablesQuery() {
+        long rawContactId1 = createRawContactWithName("Meghan", "Knox");
+        long phoneId1 = ContentUris.parseId(insertPhoneNumber(rawContactId1, "18004664411"));
+        long contactId1 = queryContactId(rawContactId1);
+
+        long rawContactId2 = createRawContactWithName("John", "Doe");
+        long sipAddressId2 = ContentUris.parseId(
+                insertSipAddress(rawContactId2, "sip@example.com"));
+        long contactId2 = queryContactId(rawContactId2);
+
+        ContentValues values1 = new ContentValues();
+        values1.put(Data._ID, phoneId1);
+        values1.put(Data.RAW_CONTACT_ID, rawContactId1);
+        values1.put(RawContacts.CONTACT_ID, contactId1);
+        values1.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        values1.put(Phone.NUMBER, "18004664411");
+        values1.put(Phone.TYPE, Phone.TYPE_HOME);
+        values1.putNull(Phone.LABEL);
+        values1.put(Contacts.DISPLAY_NAME, "Meghan Knox");
+
+        ContentValues values2 = new ContentValues();
+        values2.put(Data._ID, sipAddressId2);
+        values2.put(Data.RAW_CONTACT_ID, rawContactId2);
+        values2.put(RawContacts.CONTACT_ID, contactId2);
+        values2.put(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
+        values2.put(SipAddress.SIP_ADDRESS, "sip@example.com");
+        values2.put(Contacts.DISPLAY_NAME, "John Doe");
+
+        assertEquals(2, getCount(Callable.CONTENT_URI, null, null));
+        assertStoredValues(Callable.CONTENT_URI, new ContentValues[] { values1, values2 });
+    }
+
+    public void testCallablesFilterQuery() {
+        testPhonesFilterQueryInter(Callable.CONTENT_FILTER_URI);
+    }
+
     public void testEmailsQuery() {
         ContentValues values = new ContentValues();
         values.put(RawContacts.CUSTOM_RINGTONE, "d");
@@ -2173,6 +2270,7 @@
 
         values.put(Groups.ACCOUNT_NAME, "a");
         values.put(Groups.ACCOUNT_TYPE, "b");
+        values.put(Groups.DATA_SET, "ds");
         values.put(Groups.SOURCE_ID, "c");
         values.put(Groups.VERSION, 42);
         values.put(Groups.GROUP_VISIBLE, 1);
@@ -2229,6 +2327,65 @@
         }
     }
 
+    public void testGroupDelete_byAccountSelection() {
+        final Account account1 = new Account("accountName1", "accountType1");
+        final Account account2 = new Account("accountName2", "accountType2");
+
+        final long groupId1 = createGroup(account1, "sourceId1", "title1");
+        final long groupId2 = createGroup(account2, "sourceId2", "title2");
+        final long groupId3 = createGroup(account2, "sourceId3", "title3");
+
+        final int numDeleted = mResolver.delete(Groups.CONTENT_URI,
+                Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?",
+                new String[]{account2.name, account2.type});
+        assertEquals(2, numDeleted);
+
+        ContentValues v1 = new ContentValues();
+        v1.put(Groups._ID, groupId1);
+        v1.put(Groups.DELETED, 0);
+
+        ContentValues v2 = new ContentValues();
+        v2.put(Groups._ID, groupId2);
+        v2.put(Groups.DELETED, 1);
+
+        ContentValues v3 = new ContentValues();
+        v3.put(Groups._ID, groupId3);
+        v3.put(Groups.DELETED, 1);
+
+        assertStoredValues(Groups.CONTENT_URI, new ContentValues[] { v1, v2, v3 });
+    }
+
+    public void testGroupDelete_byAccountParam() {
+        final Account account1 = new Account("accountName1", "accountType1");
+        final Account account2 = new Account("accountName2", "accountType2");
+
+        final long groupId1 = createGroup(account1, "sourceId1", "title1");
+        final long groupId2 = createGroup(account2, "sourceId2", "title2");
+        final long groupId3 = createGroup(account2, "sourceId3", "title3");
+
+        final int numDeleted = mResolver.delete(
+                Groups.CONTENT_URI.buildUpon()
+                    .appendQueryParameter(Groups.ACCOUNT_NAME, account2.name)
+                    .appendQueryParameter(Groups.ACCOUNT_TYPE, account2.type)
+                    .build(),
+                null, null);
+        assertEquals(2, numDeleted);
+
+        ContentValues v1 = new ContentValues();
+        v1.put(Groups._ID, groupId1);
+        v1.put(Groups.DELETED, 0);
+
+        ContentValues v2 = new ContentValues();
+        v2.put(Groups._ID, groupId2);
+        v2.put(Groups.DELETED, 1);
+
+        ContentValues v3 = new ContentValues();
+        v3.put(Groups._ID, groupId3);
+        v3.put(Groups.DELETED, 1);
+
+        assertStoredValues(Groups.CONTENT_URI, new ContentValues[] { v1, v2, v3 });
+    }
+
     public void testGroupSummaryQuery() {
         final Account account1 = new Account("accountName1", "accountType1");
         final Account account2 = new Account("accountName2", "accountType2");
@@ -2295,19 +2452,29 @@
 
         assertStoredValues(Groups.CONTENT_SUMMARY_URI, new ContentValues[] { v1, v2, v3 });
 
-        final Uri uri = Groups.CONTENT_SUMMARY_URI.buildUpon()
-                .appendQueryParameter(Groups.PARAM_RETURN_GROUP_COUNT_PER_ACCOUNT, "true")
-                .build();
-        v1.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 1);
-        v2.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 2);
-        v3.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 2);
+        final Uri uri = Groups.CONTENT_SUMMARY_URI;
+
+        // TODO Once SUMMARY_GROUP_COUNT_PER_ACCOUNT is supported remove all the if(false).
+        if (false) {
+            v1.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 1);
+            v2.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 2);
+            v3.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 2);
+        } else {
+            v1.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 0);
+            v2.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 0);
+            v3.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 0);
+        }
         assertStoredValues(uri, new ContentValues[] { v1, v2, v3 });
 
         // Introduce another group in account1, testing SUMMARY_GROUP_COUNT_PER_ACCOUNT correctly
         // reflects the change.
         final long groupId4 = createGroup(account1, "sourceId4", "title4");
-        v1.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT,
-                v1.getAsInteger(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT) + 1);
+        if (false) {
+            v1.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT,
+                    v1.getAsInteger(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT) + 1);
+        } else {
+            v1.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 0);
+        }
         ContentValues v4 = new ContentValues();
         v4.put(Groups._ID, groupId4);
         v4.put(Groups.TITLE, "title4");
@@ -2316,8 +2483,12 @@
         v4.put(Groups.ACCOUNT_TYPE, account1.type);
         v4.put(Groups.SUMMARY_COUNT, 0);
         v4.put(Groups.SUMMARY_WITH_PHONES, 0);
-        v4.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT,
-                v1.getAsInteger(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT));
+        if (false) {
+            v4.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT,
+                    v1.getAsInteger(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT));
+        } else {
+            v4.put(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, 0);
+        }
         assertStoredValues(uri, new ContentValues[] { v1, v2, v3, v4 });
 
         // We change the tables dynamically according to the requested projection.
@@ -2763,6 +2934,19 @@
         assertStoredValues(filterUri, values);
     }
 
+    public void testCountPhoneNumberDigits() {
+        assertEquals(10, ContactsProvider2.countPhoneNumberDigits("86 (0) 5-55-12-34"));
+        assertEquals(10, ContactsProvider2.countPhoneNumberDigits("860 555-1234"));
+        assertEquals(3, ContactsProvider2.countPhoneNumberDigits("860"));
+        assertEquals(10, ContactsProvider2.countPhoneNumberDigits("8605551234"));
+        assertEquals(6, ContactsProvider2.countPhoneNumberDigits("860555"));
+        assertEquals(6, ContactsProvider2.countPhoneNumberDigits("860 555"));
+        assertEquals(6, ContactsProvider2.countPhoneNumberDigits("860-555"));
+        assertEquals(12, ContactsProvider2.countPhoneNumberDigits("+441234098765"));
+        assertEquals(0, ContactsProvider2.countPhoneNumberDigits("44+1234098765"));
+        assertEquals(0, ContactsProvider2.countPhoneNumberDigits("+441234098foo"));
+    }
+
     public void testSearchSnippetPhone() throws Exception {
         long rawContactId = createRawContact();
         long contactId = queryContactId(rawContactId);
@@ -3458,17 +3642,6 @@
 
     // Stream item insertion test cases.
 
-    public void testInsertStreamItemIntoOtherAccount() {
-        long rawContactId = createRawContact(mAccount);
-        ContentValues values = buildGenericStreamItemValues();
-        try {
-            insertStreamItem(rawContactId, values, mAccountTwo);
-            fail("Stream insertion was allowed in another account's raw contact.");
-        } catch (SecurityException expected) {
-            // Trying to insert stream items into account one's raw contact is forbidden.
-        }
-    }
-
     public void testInsertStreamItemInProfileRequiresWriteProfileAccess() {
         long profileRawContactId = createBasicProfileContact(new ContentValues());
 
@@ -3637,22 +3810,6 @@
                 RawContacts.StreamItems.CONTENT_DIRECTORY), values);
     }
 
-    public void testUpdateStreamItemFromOtherAccount() {
-        long rawContactId = createRawContact(mAccount);
-        ContentValues values = buildGenericStreamItemValues();
-        Uri resultUri = insertStreamItem(rawContactId, values, mAccount);
-        long streamItemId = ContentUris.parseId(resultUri);
-        values.put(StreamItems._ID, streamItemId);
-        values.put(StreamItems.TEXT, "Goodbye world");
-        try {
-            mResolver.update(maybeAddAccountQueryParameters(StreamItems.CONTENT_URI, mAccountTwo),
-                    values, null, null);
-            fail("Should not be able to update stream items inserted by another account");
-        } catch (SecurityException expected) {
-            // Can't update the stream items from another account.
-        }
-    }
-
     // Stream item photo update test cases.
 
     public void testUpdateStreamItemPhotoById() throws IOException {
@@ -3708,32 +3865,6 @@
                 mResolver.openInputStream(Uri.parse(displayPhotoUri)));
     }
 
-    public void testUpdateStreamItemPhotoFromOtherAccount() {
-        long rawContactId = createRawContact(mAccount);
-        ContentValues values = buildGenericStreamItemValues();
-        Uri resultUri = insertStreamItem(rawContactId, values, mAccount);
-        long streamItemId = ContentUris.parseId(resultUri);
-        ContentValues photoValues = buildGenericStreamItemPhotoValues(1);
-        resultUri = insertStreamItemPhoto(streamItemId, photoValues, mAccount);
-        long streamItemPhotoId = ContentUris.parseId(resultUri);
-
-        photoValues.put(StreamItemPhotos._ID, streamItemPhotoId);
-        photoValues.put(StreamItemPhotos.PHOTO, loadPhotoFromResource(
-                R.drawable.galaxy, PhotoSize.ORIGINAL));
-        Uri photoUri =
-                maybeAddAccountQueryParameters(
-                        Uri.withAppendedPath(
-                                ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
-                                StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
-                        mAccountTwo);
-        try {
-            mResolver.update(photoUri, photoValues, null, null);
-            fail("Should not be able to update stream item photos inserted by another account");
-        } catch (SecurityException expected) {
-            // Can't update a stream item photo inserted by another account.
-        }
-    }
-
     // Stream item deletion test cases.
 
     public void testDeleteStreamItemById() {
@@ -3775,21 +3906,6 @@
                 RawContacts.StreamItems.CONTENT_DIRECTORY), secondValues);
     }
 
-    public void testDeleteStreamItemFromOtherAccount() {
-        long rawContactId = createRawContact(mAccount);
-        long streamItemId = ContentUris.parseId(
-                insertStreamItem(rawContactId, buildGenericStreamItemValues(), mAccount));
-        try {
-            mResolver.delete(
-                    maybeAddAccountQueryParameters(
-                            ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
-                            mAccountTwo), null, null);
-            fail("Should not be able to delete stream item inserted by another account");
-        } catch (SecurityException expected) {
-            // Can't delete a stream item from another account.
-        }
-    }
-
     // Stream item photo deletion test cases.
 
     public void testDeleteStreamItemPhotoById() {
@@ -3833,23 +3949,6 @@
         assertStoredValues(photoUri, firstPhotoValues);
     }
 
-    public void testDeleteStreamItemPhotoFromOtherAccount() {
-        long rawContactId = createRawContact(mAccount);
-        long streamItemId = ContentUris.parseId(
-                insertStreamItem(rawContactId, buildGenericStreamItemValues(), mAccount));
-        insertStreamItemPhoto(streamItemId, buildGenericStreamItemPhotoValues(0), mAccount);
-        try {
-            mResolver.delete(maybeAddAccountQueryParameters(
-                    Uri.withAppendedPath(
-                            ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
-                            StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
-                    mAccountTwo), null, null);
-            fail("Should not be able to delete stream item photo inserted by another account");
-        } catch (SecurityException expected) {
-            // Can't delete a stream item photo from another account.
-        }
-    }
-
     public void testDeleteStreamItemsWhenRawContactDeleted() {
         long rawContactId = createRawContact(mAccount);
         Uri streamItemUri = insertStreamItem(rawContactId,
@@ -4452,7 +4551,7 @@
         assertEquals(1, getCount(Contacts.CONTENT_URI, Contacts._ID + "=" + contactId, null));
     }
 
-    public void testRawContactDeletionWithAccounts() {
+    public void testRawContactDeletion_byAccountParam() {
         long rawContactId = createRawContact(mAccount);
         Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
 
@@ -4471,7 +4570,8 @@
                 .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, mAccountTwo.name)
                 .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, mAccountTwo.type)
                 .build();
-        mResolver.delete(deleteWithWrongAccountUri, null, null);
+        int numDeleted = mResolver.delete(deleteWithWrongAccountUri, null, null);
+        assertEquals(0, numDeleted);
 
         assertStoredValue(uri, RawContacts.DELETED, "0");
 
@@ -4481,11 +4581,130 @@
                 .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, mAccount.name)
                 .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, mAccount.type)
                 .build();
-        mResolver.delete(deleteWithCorrectAccountUri, null, null);
+        numDeleted = mResolver.delete(deleteWithCorrectAccountUri, null, null);
+        assertEquals(1, numDeleted);
 
         assertStoredValue(uri, RawContacts.DELETED, "1");
     }
 
+    public void testRawContactDeletion_byAccountSelection() {
+        long rawContactId = createRawContact(mAccount);
+        Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+
+        // Do not delete if we are deleting with wrong account.
+        int numDeleted = mResolver.delete(RawContacts.CONTENT_URI,
+                RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?",
+                new String[] {mAccountTwo.name, mAccountTwo.type});
+        assertEquals(0, numDeleted);
+
+        assertStoredValue(uri, RawContacts.DELETED, "0");
+
+        // Delete if we are deleting with correct account.
+        numDeleted = mResolver.delete(RawContacts.CONTENT_URI,
+                RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?",
+                new String[] {mAccount.name, mAccount.type});
+        assertEquals(1, numDeleted);
+
+        assertStoredValue(uri, RawContacts.DELETED, "1");
+    }
+
+    /**
+     * Test for {@link ContactsProvider2#stringToAccounts} and
+     * {@link ContactsProvider2#accountsToString}.
+     */
+    public void testAccountsToString() {
+        final Set<Account> EXPECTED_0 = Sets.newHashSet();
+        final Set<Account> EXPECTED_1 = Sets.newHashSet(ACCOUNT_1);
+        final Set<Account> EXPECTED_2 = Sets.newHashSet(ACCOUNT_2);
+        final Set<Account> EXPECTED_1_2 = Sets.newHashSet(ACCOUNT_1, ACCOUNT_2);
+
+        final Set<Account> ACTUAL_0 = Sets.newHashSet();
+        final Set<Account> ACTUAL_1 = Sets.newHashSet(ACCOUNT_1);
+        final Set<Account> ACTUAL_2 = Sets.newHashSet(ACCOUNT_2);
+        final Set<Account> ACTUAL_1_2 = Sets.newHashSet(ACCOUNT_2, ACCOUNT_1);
+
+        assertTrue(EXPECTED_0.equals(accountsToStringToAccounts(ACTUAL_0)));
+        assertFalse(EXPECTED_0.equals(accountsToStringToAccounts(ACTUAL_1)));
+        assertFalse(EXPECTED_0.equals(accountsToStringToAccounts(ACTUAL_2)));
+        assertFalse(EXPECTED_0.equals(accountsToStringToAccounts(ACTUAL_1_2)));
+
+        assertFalse(EXPECTED_1.equals(accountsToStringToAccounts(ACTUAL_0)));
+        assertTrue(EXPECTED_1.equals(accountsToStringToAccounts(ACTUAL_1)));
+        assertFalse(EXPECTED_1.equals(accountsToStringToAccounts(ACTUAL_2)));
+        assertFalse(EXPECTED_1.equals(accountsToStringToAccounts(ACTUAL_1_2)));
+
+        assertFalse(EXPECTED_2.equals(accountsToStringToAccounts(ACTUAL_0)));
+        assertFalse(EXPECTED_2.equals(accountsToStringToAccounts(ACTUAL_1)));
+        assertTrue(EXPECTED_2.equals(accountsToStringToAccounts(ACTUAL_2)));
+        assertFalse(EXPECTED_2.equals(accountsToStringToAccounts(ACTUAL_1_2)));
+
+        assertFalse(EXPECTED_1_2.equals(accountsToStringToAccounts(ACTUAL_0)));
+        assertFalse(EXPECTED_1_2.equals(accountsToStringToAccounts(ACTUAL_1)));
+        assertFalse(EXPECTED_1_2.equals(accountsToStringToAccounts(ACTUAL_2)));
+        assertTrue(EXPECTED_1_2.equals(accountsToStringToAccounts(ACTUAL_1_2)));
+
+        try {
+            ContactsProvider2.stringToAccounts("x");
+            fail("Didn't throw for malformed input");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    private static final Set<Account> accountsToStringToAccounts(Set<Account> accounts) {
+        return ContactsProvider2.stringToAccounts(ContactsProvider2.accountsToString(accounts));
+    }
+
+    /**
+     * Test for {@link ContactsProvider2#haveAccountsChanged} and
+     * {@link ContactsProvider2#saveAccounts}.
+     */
+    public void testHaveAccountsChanged() {
+        final ContactsProvider2 cp = (ContactsProvider2) getProvider();
+
+        final Account[] ACCOUNTS_0 = new Account[] {};
+        final Account[] ACCOUNTS_1 = new Account[] {ACCOUNT_1};
+        final Account[] ACCOUNTS_2 = new Account[] {ACCOUNT_2};
+        final Account[] ACCOUNTS_1_2 = new Account[] {ACCOUNT_1, ACCOUNT_2};
+        final Account[] ACCOUNTS_2_1 = new Account[] {ACCOUNT_2, ACCOUNT_1};
+
+        // Add ACCOUNT_1
+
+        assertTrue(cp.haveAccountsChanged(ACCOUNTS_1));
+        cp.saveAccounts(ACCOUNTS_1);
+        assertFalse(cp.haveAccountsChanged(ACCOUNTS_1));
+
+        // Add ACCOUNT_2
+
+        assertTrue(cp.haveAccountsChanged(ACCOUNTS_1_2));
+        // (try with reverse order)
+        assertTrue(cp.haveAccountsChanged(ACCOUNTS_2_1));
+        cp.saveAccounts(ACCOUNTS_1_2);
+        assertFalse(cp.haveAccountsChanged(ACCOUNTS_1_2));
+        // (try with reverse order)
+        assertFalse(cp.haveAccountsChanged(ACCOUNTS_2_1));
+
+        // Remove ACCOUNT_1
+
+        assertTrue(cp.haveAccountsChanged(ACCOUNTS_2));
+        cp.saveAccounts(ACCOUNTS_2);
+        assertFalse(cp.haveAccountsChanged(ACCOUNTS_2));
+
+        // Remove ACCOUNT_2
+
+        assertTrue(cp.haveAccountsChanged(ACCOUNTS_0));
+        cp.saveAccounts(ACCOUNTS_0);
+        assertFalse(cp.haveAccountsChanged(ACCOUNTS_0));
+
+        // Test with malformed DB property.
+
+        final ContactsDatabaseHelper dbHelper = cp.getThreadActiveDatabaseHelperForTest();
+        dbHelper.setProperty(DbProperties.KNOWN_ACCOUNTS, "x");
+
+        // With malformed property the method always return true.
+        assertTrue(cp.haveAccountsChanged(ACCOUNTS_0));
+        assertTrue(cp.haveAccountsChanged(ACCOUNTS_1));
+    }
+
     public void testAccountsUpdated() {
         // This is to ensure we do not delete contacts with null, null (account name, type)
         // accidentally.
@@ -5527,7 +5746,7 @@
                 new String[]{Contacts.DISPLAY_NAME},
                 null, null, Contacts.SORT_KEY_PRIMARY + " COLLATE LOCALIZED");
 
-        assertFirstLetterValues(cursor, null, "B", "J", "M", "R", "T");
+        assertFirstLetterValues(cursor, "", "B", "J", "M", "R", "T");
         assertFirstLetterCounts(cursor,    1,   1,   1,   2,   2,   1);
         cursor.close();
 
@@ -5535,7 +5754,7 @@
                 new String[]{Contacts.DISPLAY_NAME},
                 null, null, Contacts.SORT_KEY_ALTERNATIVE + " COLLATE LOCALIZED DESC");
 
-        assertFirstLetterValues(cursor, "W", "S", "R", "M", "B", null);
+        assertFirstLetterValues(cursor, "W", "S", "R", "M", "B", "");
         assertFirstLetterCounts(cursor,   1,   2,   1,   1,   2,    1);
         cursor.close();
     }
diff --git a/tests/src/com/android/providers/contacts/FastScrollingIndexCacheTest.java b/tests/src/com/android/providers/contacts/FastScrollingIndexCacheTest.java
new file mode 100644
index 0000000..7ca2a87
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/FastScrollingIndexCacheTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.providers.contacts;
+
+import com.android.providers.contacts.util.MockSharedPreferences;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
+public class FastScrollingIndexCacheTest extends AndroidTestCase {
+    private MockSharedPreferences mPrefs;
+    private FastScrollingIndexCache mCache;
+
+    private static final String[] TITLES_0 = new String[] {};
+    private static final String[] TITLES_1 = new String[] {"a"};
+    private static final String[] TITLES_2 = new String[] {"", "b"};
+    private static final String[] TITLES_3 = new String[] {"", "b", "aaa"};
+
+    private static final int[] COUNTS_0 = new int[] {};
+    private static final int[] COUNTS_1 = new int[] {1};
+    private static final int[] COUNTS_2 = new int[] {2, 3};
+    private static final int[] COUNTS_3 = new int[] {0, -1, 2};
+
+    private static final String[] PROJECTION_0 = new String[] {};
+    private static final String[] PROJECTION_1 = new String[] {"c1"};
+    private static final String[] PROJECTION_2 = new String[] {"c3", "c4"};
+
+    private static final Uri URI_A = Contacts.CONTENT_URI;
+    private static final Uri URI_B = RawContacts.CONTENT_URI;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mPrefs = new MockSharedPreferences();
+        mCache = new FastScrollingIndexCache(mPrefs);
+    }
+
+    private void assertBundle(String[] expectedTitles, int[] expectedCounts, Bundle actual) {
+        assertNotNull(actual);
+        MoreAsserts.assertEquals(expectedTitles,
+                actual.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES));
+        MoreAsserts.assertEquals(expectedCounts,
+                actual.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS));
+    }
+
+    /**
+     * Test for {@link FastScrollingIndexCache#buildExtraBundleFromValue} and
+     * {@link FastScrollingIndexCache#buildCacheValue}.
+     */
+    public void testBuildCacheValue() {
+        assertBundle(TITLES_0, COUNTS_0,
+                FastScrollingIndexCache.buildExtraBundleFromValue(
+                        FastScrollingIndexCache.buildCacheValue(TITLES_0, COUNTS_0)));
+        assertBundle(TITLES_1, COUNTS_1,
+                FastScrollingIndexCache.buildExtraBundleFromValue(
+                        FastScrollingIndexCache.buildCacheValue(TITLES_1, COUNTS_1)));
+        assertBundle(TITLES_2, COUNTS_2,
+                FastScrollingIndexCache.buildExtraBundleFromValue(
+                        FastScrollingIndexCache.buildCacheValue(TITLES_2, COUNTS_2)));
+    }
+
+    private static final Bundle putAndGetBundle(FastScrollingIndexCache cache, Uri queryUri,
+            String selection, String[] selectionArgs, String sortOrder, String countExpression,
+            String[] titles, int[] counts) {
+        Bundle bundle = FastScrollingIndexCache.buildExtraBundle(titles, counts);
+        cache.put(queryUri, selection, selectionArgs, sortOrder, countExpression, bundle);
+        return bundle;
+    }
+
+    public void testPutAndGet() {
+        // Initially the cache is empty
+        assertNull(mCache.get(null, null, null, null, null));
+        assertNull(mCache.get(URI_A, "*s*", PROJECTION_0, "*so*", "*ce*"));
+        assertNull(mCache.get(URI_A, "*s*", PROJECTION_1, "*so*", "*ce*"));
+        assertNull(mCache.get(URI_B, "s", PROJECTION_2, "so", "ce"));
+
+        // Put...
+        Bundle b;
+        b = putAndGetBundle(mCache, null, null, null, null, null, TITLES_0, COUNTS_0);
+        assertBundle(TITLES_0, COUNTS_0, b);
+
+        b = putAndGetBundle(mCache, URI_A, "*s*", PROJECTION_0, "*so*", "*ce*", TITLES_1, COUNTS_1);
+        assertBundle(TITLES_1, COUNTS_1, b);
+
+        b = putAndGetBundle(mCache, URI_A, "*s*", PROJECTION_1, "*so*", "*ce*", TITLES_2, COUNTS_2);
+        assertBundle(TITLES_2, COUNTS_2, b);
+
+        b = putAndGetBundle(mCache, URI_B, "s", PROJECTION_2, "so", "ce", TITLES_3, COUNTS_3);
+        assertBundle(TITLES_3, COUNTS_3, b);
+
+        // Get...
+        assertBundle(TITLES_0, COUNTS_0, mCache.get(null, null, null, null, null));
+        assertBundle(TITLES_1, COUNTS_1, mCache.get(URI_A, "*s*", PROJECTION_0, "*so*", "*ce*"));
+        assertBundle(TITLES_2, COUNTS_2, mCache.get(URI_A, "*s*", PROJECTION_1, "*so*", "*ce*"));
+        assertBundle(TITLES_3, COUNTS_3, mCache.get(URI_B, "s", PROJECTION_2, "so", "ce"));
+
+        // Invalidate...
+        mCache.invalidate();
+
+        // Get again... Nothing shoul be cached...
+        assertNull(mCache.get(null, null, null, null, null));
+        assertNull(mCache.get(URI_A, "*s*", PROJECTION_0, "*so*", "*ce*"));
+        assertNull(mCache.get(URI_A, "*s*", PROJECTION_1, "*so*", "*ce*"));
+        assertNull(mCache.get(URI_B, "s", PROJECTION_2, "so", "ce"));
+
+        // Put again...
+        b = putAndGetBundle(mCache, null, null, null, null, null, TITLES_0, COUNTS_0);
+        assertBundle(TITLES_0, COUNTS_0, b);
+
+        b = putAndGetBundle(mCache, URI_A, "*s*", PROJECTION_0, "*so*", "*ce*", TITLES_1, COUNTS_1);
+        assertBundle(TITLES_1, COUNTS_1, b);
+
+        b = putAndGetBundle(mCache, URI_A, "*s*", PROJECTION_1, "*so*", "*ce*", TITLES_2, COUNTS_2);
+        assertBundle(TITLES_2, COUNTS_2, b);
+
+        b = putAndGetBundle(mCache, URI_B, "s", PROJECTION_2, "so", "ce", TITLES_2, COUNTS_2);
+        assertBundle(TITLES_2, COUNTS_2, b);
+
+        // Now, create a new cache instance (with the same shared preferences)
+        // It should restore the cache content from the preferences...
+
+        FastScrollingIndexCache cache2 = new FastScrollingIndexCache(mPrefs);
+        assertBundle(TITLES_0, COUNTS_0, cache2.get(null, null, null, null, null));
+        assertBundle(TITLES_1, COUNTS_1, cache2.get(URI_A, "*s*", PROJECTION_0, "*so*", "*ce*"));
+        assertBundle(TITLES_2, COUNTS_2, cache2.get(URI_A, "*s*", PROJECTION_1, "*so*", "*ce*"));
+        assertBundle(TITLES_2, COUNTS_2, cache2.get(URI_B, "s", PROJECTION_2, "so", "ce"));
+    }
+
+    public void testMalformedPreferences() {
+        mPrefs.edit().putString(FastScrollingIndexCache.PREFERENCE_KEY, "123");
+        // get() shouldn't crash
+        assertNull(mCache.get(null, null, null, null, null));
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java b/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
index 066c47e..9bfe9d6 100644
--- a/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
+++ b/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
@@ -113,46 +113,60 @@
         GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
                 loadTestPhoto()).phone("1-800-4664-411").build();
         new SuggestionTesterBuilder(contact).query("1800").expectIcon1Uri(true).expectedText1(
-                "Deer Dough").expectedText2("1-800-4664-411").build().test();
+                "Deer Dough").expectedText2("1-800-4664-411").expectedCreateContactNumber("1800")
+                .build().test();
     }
 
-    public void testSearchSuggestionsByPhoneNumberOnPhone() throws Exception {
-        getContactsProvider().setIsPhone(true);
-
+    public void assertCreateContactSuggestion(Cursor c, String number) {
         ContentValues values = new ContentValues();
-
-        Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
-                .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("12345678").build();
-
-        Cursor c = mResolver.query(searchUri, null, null, null, null);
-        assertEquals(2, c.getCount());
-        c.moveToFirst();
-
-        values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Dial number");
-        values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using 12345678");
-        values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
-                String.valueOf(com.android.internal.R.drawable.call_contact));
-        values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
-                Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
-        values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345678");
-        values.putNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
-        assertCursorValues(c, values);
-
-        c.moveToNext();
-        values.clear();
         values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Create contact");
-        values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using 12345678");
+        values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using "+ number);
         values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
                 String.valueOf(com.android.internal.R.drawable.create_contact));
         values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
                 Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED);
-        values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:12345678");
+        values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:" + number);
         values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
                 SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
         assertCursorValues(c, values);
+    }
+
+    public void searchSuggestionsByPhoneNumberOnPhoneTest(String number) {
+        getContactsProvider().setIsPhone(true);
+        getContactsProvider().setIsVoiceCapable(true);
+
+        ContentValues values = new ContentValues();
+
+        Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
+                .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath(number).build();
+
+        Cursor c = mResolver.query(searchUri, null, null, null, null);
+        assertEquals(2, c.getCount());
+        c.moveToFirst();
+        values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, "Dial number");
+        values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, "using " + number);
+        values.put(SearchManager.SUGGEST_COLUMN_ICON_1,
+                String.valueOf(com.android.internal.R.drawable.call_contact));
+        values.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+                Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED);
+        values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, "tel:" + number);
+        values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
+                SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
+        assertCursorValues(c, values);
+
+        c.moveToNext();
+        assertCreateContactSuggestion(c, number);
         c.close();
     }
 
+    public void testSearchSuggestionsByPhoneNumberOnPhone() throws Exception {
+        searchSuggestionsByPhoneNumberOnPhoneTest("12345678");
+    }
+
+    public void testSearchSuggestionsByAlphnumericPhoneNumberOnPhone() throws Exception {
+        searchSuggestionsByPhoneNumberOnPhoneTest("1800-flowers");
+    }
+
     /**
      * Tests that the quick search suggestion returns the expected contact
      * information.
@@ -171,6 +185,8 @@
 
         private final String expectedText2;
 
+        private final String expectedCreateContactNumber;
+
         public SuggestionTester(SuggestionTesterBuilder builder) {
             contact = builder.contact;
             query = builder.query;
@@ -178,6 +194,7 @@
             expectedIcon2 = builder.expectedIcon2;
             expectedText1 = builder.expectedText1;
             expectedText2 = builder.expectedText2;
+            expectedCreateContactNumber = builder.expectedCreateContactNumber;
         }
 
         /**
@@ -205,7 +222,7 @@
                     .appendPath(query).build();
 
             Cursor c = mResolver.query(searchUri, null, null, null, null);
-            assertEquals(1, c.getCount());
+            assertEquals(expectedCreateContactNumber == null ? 1 : 2, c.getCount());
             c.moveToFirst();
 
             String icon1 = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1));
@@ -219,6 +236,12 @@
             // SearchManager does not declare a constant for _id
             ContentValues values = getContactValues();
             assertCursorValues(c, values);
+
+            if (expectedCreateContactNumber != null) {
+                c.moveToNext();
+                assertCreateContactSuggestion(c, expectedCreateContactNumber);
+            }
+
             c.close();
         }
 
@@ -337,6 +360,8 @@
 
         private String expectedText2;
 
+        private String expectedCreateContactNumber;
+
         public SuggestionTesterBuilder(GoldenContact contact) {
             this.contact = contact;
         }
@@ -393,5 +418,10 @@
             expectedText2 = value;
             return this;
         }
+
+        public SuggestionTesterBuilder expectedCreateContactNumber(String number) {
+            expectedCreateContactNumber = number;
+            return this;
+        }
     }
 }
diff --git a/tests/src/com/android/providers/contacts/GroupsTest.java b/tests/src/com/android/providers/contacts/GroupsTest.java
index 56b7100..3d85064 100644
--- a/tests/src/com/android/providers/contacts/GroupsTest.java
+++ b/tests/src/com/android/providers/contacts/GroupsTest.java
@@ -46,6 +46,7 @@
  *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
  * </code>
  */
+@MediumTest
 public class GroupsTest extends BaseContactsProvider2Test {
 
     private static final String GROUP_GREY = "Grey";
@@ -64,7 +65,6 @@
     private static final String PHONE_CHARLIE_1 = "555-4444";
     private static final String PHONE_CHARLIE_2 = "555-5555";
 
-    @LargeTest
     public void testGroupSummary() {
 
         // Clear any existing data before starting
@@ -144,7 +144,6 @@
         cursor.close();
     }
 
-    @MediumTest
     public void testGroupDirtySetOnChange() {
         Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
                 createGroup(mAccount, "gsid1", "title1"));
@@ -153,7 +152,6 @@
         assertDirty(uri, false);
     }
 
-    @MediumTest
     public void testMarkAsDirtyParameter() {
         Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
                 createGroup(mAccount, "gsid1", "title1"));
@@ -166,7 +164,6 @@
         assertDirty(uri, false);
     }
 
-    @MediumTest
     public void testGroupDirtyClearedWhenSetExplicitly() {
         Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
                 createGroup(mAccount, "gsid1", "title1"));
@@ -180,7 +177,6 @@
         assertDirty(uri, false);
     }
 
-    @MediumTest
     public void testGroupDeletion1() {
         long groupId = createGroup(mAccount, "g1", "gt1");
         Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
@@ -195,7 +191,6 @@
         assertEquals(0, getCount(uri, null, null));
     }
 
-    @MediumTest
     public void testGroupDeletion2() {
         long groupId = createGroup(mAccount, "g1", "gt1");
         Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
@@ -206,7 +201,6 @@
         assertEquals(0, getCount(uri, null, null));
     }
 
-    @MediumTest
     public void testGroupVersionUpdates() {
         Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
                 createGroup(mAccount, "gsid1", "title1"));
@@ -254,7 +248,6 @@
                         + (visible ? 1 : 0), null).withExpectedCount(1).build();
     }
 
-    @LargeTest
     public void testDelayVisibleTransaction() throws RemoteException, OperationApplicationException {
         final ContentValues values = new ContentValues();
 
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
deleted file mode 100644
index d78193b..0000000
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.contacts;
-
-import android.content.ContentProvider;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.os.Debug;
-import android.provider.CallLog;
-import android.provider.ContactsContract;
-import android.test.AndroidTestCase;
-import android.test.IsolatedContext;
-import android.test.RenamingDelegatingContext;
-import android.test.mock.MockContentResolver;
-import android.test.mock.MockContext;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.util.Log;
-
-import java.io.File;
-
-/**
- * Performance test for {@link ContactAggregator}. Run the test like this:
- * <code>
- * adb push <large contacts.db> \
- *         data/data/com.android.providers.contacts/databases/perf_imp.contacts.db
- * adb shell am instrument \
- *         -e class com.android.providers.contacts.LegacyContactImporterPerformanceTest \
- *         -w com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
- * </code>
- *
- * Note that this SHOULD be a large test, but had to be bumped down to medium due to a bug in the
- * SQLite cleanup code.
- */
-@MediumTest
-public class LegacyContactImporterPerformanceTest extends AndroidTestCase {
-
-    private static final String TAG = "LegacyContactImporterPerformanceTest";
-
-    private static final boolean TRACE = false;
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        SynchronousContactsProvider2.resetOpenHelper();
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-        SynchronousContactsProvider2.resetOpenHelper();
-    }
-
-    public void testPerformance() {
-        final Context targetContext = getContext();
-        MockContentResolver resolver = new MockContentResolver();
-        MockContext context = new MockContext() {
-            @Override
-            public Resources getResources() {
-                return targetContext.getResources();
-            }
-
-            @Override
-            public String getPackageName() {
-                return "no.package";
-            }
-
-            @Override
-            public ApplicationInfo getApplicationInfo() {
-                ApplicationInfo ai = new ApplicationInfo();
-                ai.packageName = "contactsTestPackage";
-                return ai;
-            }
-
-            @Override
-            public PackageManager getPackageManager() {
-                return new ContactsMockPackageManager();
-            }
-        };
-
-        RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(context,
-                targetContext, "perf_imp.");
-        targetContextWrapper.makeExistingFilesAndDbsAccessible();
-        IsolatedContext providerContext = new IsolatedContext(resolver, targetContextWrapper) {
-            @Override
-            public File getFilesDir() {
-                // TODO: Need to figure out something more graceful than this.
-                return new File("/data/data/com.android.providers.contacts.tests/files");
-            }
-        };
-        SynchronousContactsProvider2 provider = new SynchronousContactsProvider2();
-        provider.setDataWipeEnabled(false);
-        provider.attachInfo(providerContext, null);
-        resolver.addProvider(ContactsContract.AUTHORITY, provider);
-
-        ContentProvider callLogProvider = new TestCallLogProvider();
-        callLogProvider.attachInfo(providerContext, null);
-        resolver.addProvider(CallLog.AUTHORITY, callLogProvider);
-
-        LegacyContactImporter importer = new LegacyContactImporter(providerContext, provider);
-
-        provider.wipeData();
-
-        long start = System.currentTimeMillis();
-        if (TRACE) {
-            Debug.startMethodTracing("import");
-        }
-        provider.importLegacyContacts(importer);
-        if (TRACE) {
-            Debug.stopMethodTracing();
-        }
-        long end = System.currentTimeMillis();
-        long contactCount = provider.getRawContactCount();
-        Log.i(TAG, String.format("Imported contacts in %d ms.\n"
-                + "Contacts: %d\n"
-                + "Per contact: %.3f",
-                end - start,
-                contactCount,
-                ((double)(end - start) / contactCount)));
-    }
-
-    public static class TestCallLogProvider extends CallLogProvider {
-        private static ContactsDatabaseHelper mDbHelper;
-
-        @Override
-        protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
-            if (mDbHelper == null) {
-                mDbHelper = new ContactsDatabaseHelper(context);
-            }
-            return mDbHelper;
-        }
-    }
-}
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
deleted file mode 100644
index 61108af..0000000
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterTest.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.contacts;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-import android.provider.BaseColumns;
-import android.provider.CallLog;
-import android.provider.CallLog.Calls;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Groups;
-import android.provider.ContactsContract.RawContacts;
-import android.test.mock.MockContext;
-import android.test.suitebuilder.annotation.MediumTest;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Unit tests for {@link LegacyContactImporter}.
- *
- * Run the test like this:
- * <code>
- * adb shell am instrument -e class com.android.providers.contacts.LegacyContactImporterTest -w \
- *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
- * </code>
- *
- * Note that this SHOULD be a large test, but had to be bumped down to medium due to a bug in the
- * SQLite cleanup code.
- */
-@MediumTest
-public class LegacyContactImporterTest extends BaseContactsProvider2Test {
-
-    private static class LegacyMockContext extends MockContext {
-
-        private String mFileName;
-
-        public LegacyMockContext(String fileName) {
-            mFileName = fileName;
-        }
-
-        @Override
-        public SQLiteDatabase openOrCreateDatabase(String file, int mode,
-                SQLiteDatabase.CursorFactory factory) {
-            return SQLiteDatabase.openDatabase(mFileName, factory, SQLiteDatabase.OPEN_READONLY);
-        }
-
-        @Override
-        public File getDatabasePath(String name) {
-            return new File(mFileName);
-        }
-    }
-
-    private LegacyMockContext createLegacyMockContext(String folder) throws IOException {
-        Context context = getTestContext();
-        File tempDb = new File(context.getFilesDir(), "legacy_contacts.db");
-        if (tempDb.exists()) {
-            tempDb.delete();
-        }
-        createSQLiteDatabaseFromDumpFile(tempDb.getPath(),
-                new File(folder, "legacy_contacts.sql").getPath());
-        return new LegacyMockContext(tempDb.getPath());
-    }
-
-    private void createSQLiteDatabaseFromDumpFile(String tempDbPath, String dumpFileAssetPath)
-        throws IOException {
-
-        final String[] ignoredTables = new String[] {"android_metadata", "sqlite_sequence"};
-
-        Context context = getTestContext();
-        SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(tempDbPath, null);
-        try {
-            String data = readAssetAsString(dumpFileAssetPath);
-            String[] commands = data.split(";\r|;\n|;\r\n");
-            for (String command : commands) {
-                boolean ignore = false;
-                for (String ignoredTable : ignoredTables) {
-                    if (command.contains(ignoredTable)) {
-                        ignore = true;
-                        break;
-                    }
-                }
-                if (!ignore) {
-                    database.execSQL(command);
-                }
-            }
-
-            assertTrue(
-                    "Database Version not set. Be sure to add " +
-                    "'PRAGMA user_version = <number>;' to the SQL Script",
-                    database.getVersion() != 0);
-        } finally {
-            database.close();
-        }
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        SynchronousContactsProvider2.resetOpenHelper();
-        super.setUp();
-        addProvider(TestCallLogProvider.class, CallLog.AUTHORITY);
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-        SynchronousContactsProvider2.resetOpenHelper();
-    }
-
-    public void testContactUpgrade1() throws Exception {
-        testAssetSet("test1");
-    }
-
-    public void testSyncedContactsUpgrade() throws Exception {
-        testAssetSet("testSynced");
-    }
-
-    public void testUnsyncedContactsUpgrade() throws Exception {
-        testAssetSet("testUnsynced");
-    }
-
-    private void testAssetSet(String folder) throws Exception {
-        ContactsProvider2 provider = (ContactsProvider2)getProvider();
-        LegacyContactImporter importer =
-                new LegacyContactImporter(createLegacyMockContext(folder), provider);
-        provider.importLegacyContacts(importer);
-
-        assertQueryResults(folder + "/expected_groups.txt", Groups.CONTENT_URI, new String[]{
-                Groups._ID,
-                Groups.ACCOUNT_NAME,
-                Groups.ACCOUNT_TYPE,
-                Groups.DIRTY,
-                Groups.GROUP_VISIBLE,
-                Groups.NOTES,
-                Groups.RES_PACKAGE,
-                Groups.SOURCE_ID,
-                Groups.SYSTEM_ID,
-                Groups.TITLE,
-                Groups.VERSION,
-                Groups.SYNC1,
-                Groups.SYNC2,
-                Groups.SYNC3,
-                Groups.SYNC4,
-        });
-
-        assertQueryResults(folder + "/expected_contacts.txt", Contacts.CONTENT_URI, new String[]{
-                Contacts._ID,
-                Contacts.DISPLAY_NAME_PRIMARY,
-                Contacts.SORT_KEY_PRIMARY,
-                Contacts.PHOTO_ID,
-                Contacts.TIMES_CONTACTED,
-                Contacts.LAST_TIME_CONTACTED,
-                Contacts.CUSTOM_RINGTONE,
-                Contacts.SEND_TO_VOICEMAIL,
-                Contacts.STARRED,
-                Contacts.IN_VISIBLE_GROUP,
-                Contacts.HAS_PHONE_NUMBER,
-                Contacts.IS_USER_PROFILE,
-                Contacts.LOOKUP_KEY,
-        });
-
-        assertQueryResults(folder + "/expected_raw_contacts.txt", RawContacts.CONTENT_URI,
-                new String[]{
-                    RawContacts._ID,
-                    RawContacts.ACCOUNT_NAME,
-                    RawContacts.ACCOUNT_TYPE,
-                    RawContacts.DELETED,
-                    RawContacts.DIRTY,
-                    RawContacts.SOURCE_ID,
-                    RawContacts.VERSION,
-                    RawContacts.SYNC1,
-                    RawContacts.SYNC2,
-                    RawContacts.SYNC3,
-                    RawContacts.SYNC4,
-                    RawContacts.DISPLAY_NAME_SOURCE,
-                    RawContacts.DISPLAY_NAME_PRIMARY,
-                    RawContacts.DISPLAY_NAME_ALTERNATIVE,
-                    RawContacts.SORT_KEY_PRIMARY,
-                    RawContacts.SORT_KEY_ALTERNATIVE,
-        });
-
-        assertQueryResults(folder + "/expected_data.txt", Data.CONTENT_URI, new String[]{
-                Data._ID,
-                Data.RAW_CONTACT_ID,
-                Data.MIMETYPE,
-                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.IS_PRIMARY,
-                Data.IS_SUPER_PRIMARY,
-                Data.DATA_VERSION,
-                Data.SYNC1,
-                Data.SYNC2,
-                Data.SYNC3,
-                Data.SYNC4,
-        });
-
-        assertQueryResults(folder + "/expected_calls.txt", Calls.CONTENT_URI, new String[]{
-                Calls._ID,
-                Calls.NUMBER,
-                Calls.DATE,
-                Calls.DURATION,
-                Calls.NEW,
-                Calls.TYPE,
-                Calls.CACHED_NAME,
-                Calls.CACHED_NUMBER_LABEL,
-                Calls.CACHED_NUMBER_TYPE,
-        });
-
-        provider.getDatabaseHelper().close();
-    }
-
-    private void assertQueryResults(String fileName, Uri uri, String[] projection)
-            throws Exception {
-        String expected = readAssetAsString(fileName).trim();
-        String actual = dumpCursorToString(uri, projection).trim();
-        assertEquals("Checking golden file " + fileName, expected, actual);
-    }
-
-    private String readAssetAsString(String fileName) throws IOException {
-        Context context = getTestContext();
-        InputStream input = context.getAssets().open(fileName);
-        ByteArrayOutputStream contents = new ByteArrayOutputStream();
-        int len;
-        byte[] data = new byte[1024];
-        do {
-            len = input.read(data);
-            if (len > 0) contents.write(data, 0, len);
-        } while (len == data.length);
-        return contents.toString();
-    }
-
-    private String dumpCursorToString(Uri uri, String[] projection) {
-        Cursor c = mResolver.query(uri, projection, null, null, BaseColumns._ID);
-        if (c == null) {
-            return "Null cursor";
-        }
-
-        String cursorDump = DatabaseUtils.dumpCursorToString(c);
-        c.close();
-        return insertLineNumbers(cursorDump);
-    }
-
-    private String insertLineNumbers(String multiline) {
-        String[] lines = multiline.split("\n");
-        StringBuilder sb = new StringBuilder();
-
-        // Ignore the first line that is a volatile header and the last line which is "<<<<<"
-        for (int i = 1; i < lines.length - 1; i++) {
-            sb.append(i).append(" ").append(lines[i]).append('\n');
-        }
-        return sb.toString();
-    }
-
-
-    public static class TestCallLogProvider extends CallLogProvider {
-        private static ContactsDatabaseHelper mDbHelper;
-
-        @Override
-        protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
-            if (mDbHelper == null) {
-                mDbHelper = new ContactsDatabaseHelper(context);
-            }
-            return mDbHelper;
-        }
-    }
-}
diff --git a/tests/src/com/android/providers/contacts/PhotoLoadingTestCase.java b/tests/src/com/android/providers/contacts/PhotoLoadingTestCase.java
index 285378c..459fec0 100644
--- a/tests/src/com/android/providers/contacts/PhotoLoadingTestCase.java
+++ b/tests/src/com/android/providers/contacts/PhotoLoadingTestCase.java
@@ -20,6 +20,7 @@
 
 import android.content.res.Resources;
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -29,6 +30,7 @@
 /**
  * Adds support for loading photo files easily from test resources.
  */
+@SmallTest
 public class PhotoLoadingTestCase extends AndroidTestCase {
 
     private Map<Integer, PhotoEntry> photoResourceCache = Maps.newHashMap();
diff --git a/tests/src/com/android/providers/contacts/PhotoStoreTest.java b/tests/src/com/android/providers/contacts/PhotoStoreTest.java
index 9b7c50d..0186741 100644
--- a/tests/src/com/android/providers/contacts/PhotoStoreTest.java
+++ b/tests/src/com/android/providers/contacts/PhotoStoreTest.java
@@ -23,7 +23,6 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.PhotoFiles;
-import android.test.mock.MockContentResolver;
 import android.test.suitebuilder.annotation.LargeTest;
 
 import java.io.File;
@@ -168,7 +167,7 @@
 
         // Run cleanup with the indication that only the large and huge photos are in use, along
         // with a bogus photo file ID that isn't in the photo store.
-        long bogusPhotoFileId = 42;
+        long bogusPhotoFileId = 123456789;
         Set<Long> photoFileIdsInUse = new HashSet<Long>();
         photoFileIdsInUse.add(resourceIdToPhotoMap.get(R.drawable.earth_large));
         photoFileIdsInUse.add(resourceIdToPhotoMap.get(R.drawable.earth_huge));
diff --git a/tests/src/com/android/providers/contacts/SearchIndexManagerTest.java b/tests/src/com/android/providers/contacts/SearchIndexManagerTest.java
index ab20396..ed1c23a 100644
--- a/tests/src/com/android/providers/contacts/SearchIndexManagerTest.java
+++ b/tests/src/com/android/providers/contacts/SearchIndexManagerTest.java
@@ -438,6 +438,35 @@
                 "...doe.com\nthe eighteenth episode of Seinfeld, [650]-[253]-0000");
     }
 
+    /**
+     * Test case for bug 5904515
+     */
+    public void testSearchByPhoneNumber_diferSnippetting() {
+        long rawContactId = createRawContact();
+        insertPhoneNumber(rawContactId, "505-123-4567");
+
+        // The bug happened with the old code only when we use \u0001 as the snippet marker.
+        // But note that the expected result has [ and ] instead of \u0001.  This is because when
+        // we differ snippetizing, the marker passe to the provider will be ignored; instead
+        // assertStoredValue internally do the client-side snippetizing, which done by
+        // getCursorStringValue(), which is hardcoded to use [ and ].
+        assertStoredValue(buildSearchUri("505", "\u0001,\u0001,\u2026,5", true),
+                SearchSnippetColumns.SNIPPET, "[505]-123-4567");
+    }
+
+    /**
+     * Equivalent to {@link #testSearchByPhoneNumber_diferSnippetting} for email addresses, although
+     * the original bug didn't happen with email addresses... (It *did* happen internally, but
+     * there's no visible breakage.)
+     */
+    public void testSearchByEmail_diferSnippetting() {
+        long rawContactId = createRawContact();
+        insertEmail(rawContactId, "john@doe.com");
+
+        assertStoredValue(buildSearchUri("john", "\u0001,\u0001,\u2026,5", true),
+                SearchSnippetColumns.SNIPPET, "[john@doe.com]");
+    }
+
     private Uri buildSearchUri(String filter) {
         return buildSearchUri(filter, false);
     }
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index 49a930d..5dc5cdf 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -35,11 +35,12 @@
     private Account mAccount;
     private boolean mNetworkNotified;
     private boolean mIsPhone = true;
+    private boolean mIsVoiceCapable = true;
 
     @Override
     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
         if (mDbHelper == null) {
-            mDbHelper = new ContactsDatabaseHelper(context);
+            mDbHelper = ContactsDatabaseHelper.getNewInstanceForTest(context);
         }
         return mDbHelper;
     }
@@ -82,6 +83,15 @@
         return mIsPhone;
     }
 
+    public void setIsVoiceCapable(boolean flag) {
+        mIsVoiceCapable = flag;
+    }
+
+    @Override
+    public boolean isVoiceCapable() {
+        return mIsVoiceCapable;
+    }
+
     @Override
     public boolean onCreate() {
         boolean created = super.onCreate();
@@ -97,6 +107,11 @@
     }
 
     @Override
+    protected boolean shouldThrowExceptionForInitializationError() {
+        return true;
+    }
+
+    @Override
     protected void scheduleBackgroundTask(int task) {
         performBackgroundTask(task, null);
     }
@@ -155,7 +170,7 @@
     }
 
     @Override
-    protected boolean isWritableAccountWithDataSet(String accountType) {
+    public boolean isWritableAccountWithDataSet(String accountType) {
         return !READ_ONLY_ACCOUNT_TYPE.equals(accountType);
     }
 
@@ -188,11 +203,4 @@
 
         getContactDirectoryManagerForTest().scanAllPackages();
     }
-
-    @Override
-    protected boolean isLegacyContactImportNeeded() {
-
-        // We have an explicit test for data conversion - no need to do it every time
-        return false;
-    }
 }
diff --git a/tests/src/com/android/providers/contacts/SynchronousProfileProvider.java b/tests/src/com/android/providers/contacts/SynchronousProfileProvider.java
index 6c55f5d..bde6dc9 100644
--- a/tests/src/com/android/providers/contacts/SynchronousProfileProvider.java
+++ b/tests/src/com/android/providers/contacts/SynchronousProfileProvider.java
@@ -35,7 +35,7 @@
     @Override
     protected ProfileDatabaseHelper getDatabaseHelper(final Context context) {
         if (mDbHelper == null) {
-            mDbHelper = new ProfileDatabaseHelper(context);
+            mDbHelper = ProfileDatabaseHelper.getNewInstanceForTest(context);
         }
         return mDbHelper;
     }
diff --git a/tests/src/com/android/providers/contacts/VoicemailCleanupServiceTest.java b/tests/src/com/android/providers/contacts/VoicemailCleanupServiceTest.java
index a2f4617..f8d76ee 100644
--- a/tests/src/com/android/providers/contacts/VoicemailCleanupServiceTest.java
+++ b/tests/src/com/android/providers/contacts/VoicemailCleanupServiceTest.java
@@ -23,10 +23,12 @@
 import android.provider.VoicemailContract;
 import android.provider.VoicemailContract.Status;
 import android.provider.VoicemailContract.Voicemails;
+import android.test.suitebuilder.annotation.SmallTest;
 
 /**
  * Unit tests for {@link VoicemailCleanupService}.
  */
+@SmallTest
 public class VoicemailCleanupServiceTest extends BaseVoicemailProviderTest {
     private static final String TEST_PACKAGE_1 = "package1";
     private static final String TEST_PACKAGE_2 = "package2";
diff --git a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
index a1560dd..74195b5 100644
--- a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
@@ -28,6 +28,7 @@
 import android.provider.VoicemailContract.Status;
 import android.provider.VoicemailContract.Voicemails;
 import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.SmallTest;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -45,6 +46,7 @@
  * </code>
  */
 // TODO: Test that calltype and voicemail_uri are auto populated by the provider.
+@SmallTest
 public class VoicemailProviderTest extends BaseVoicemailProviderTest {
     /** Fields specific to call_log provider that should not be exposed by voicemail provider. */
     private static final String[] CALLLOG_PROVIDER_SPECIFIC_COLUMNS = {
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
similarity index 99%
rename from tests/src/com/android/providers/contacts/ContactAggregatorTest.java
rename to tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
index 5c7c0ff..d2f3a60 100644
--- a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
@@ -14,8 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.providers.contacts;
+package com.android.providers.contacts.aggregation;
 
+import com.android.providers.contacts.BaseContactsProvider2Test;
+import com.android.providers.contacts.aggregation.ContactAggregator;
 import com.android.providers.contacts.tests.R;
 import com.google.android.collect.Lists;
 
diff --git a/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java b/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
index 350e971..43f7c06 100644
--- a/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
+++ b/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
@@ -19,14 +19,13 @@
 import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns;
 import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
 
+import com.android.common.content.ProjectionMap;
+import com.android.providers.contacts.EvenMoreAsserts;
+
 import android.content.ContentValues;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
-import com.android.providers.contacts.EvenMoreAsserts;
-import com.android.providers.contacts.ProjectionMap;
-import com.android.providers.contacts.util.DbQueryUtils;
-
 /**
  * Unit tests for the {@link DbQueryUtils} class.
  * Run the test like this:
@@ -38,10 +37,11 @@
 public class DBQueryUtilsTest extends AndroidTestCase {
     public void testGetEqualityClause() {
         assertEquals("(foo = 'bar')", DbQueryUtils.getEqualityClause("foo", "bar"));
+        assertEquals("(foo = 2)", DbQueryUtils.getEqualityClause("foo", 2));
     }
 
     public void testGetInEqualityClause() {
-        assertEquals("(foo != 'bar')", DbQueryUtils.getInequalityClause("foo", "bar"));
+        assertEquals("(foo != 2)", DbQueryUtils.getInequalityClause("foo", 2));
     }
 
     public void testConcatenateClauses() {
diff --git a/tests/src/com/android/providers/contacts/util/MockSharedPreferences.java b/tests/src/com/android/providers/contacts/util/MockSharedPreferences.java
new file mode 100644
index 0000000..d00e711
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/util/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.providers.contacts.util;
+
+import com.google.android.collect.Maps;
+
+import android.content.SharedPreferences;
+
+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();
+    }
+}