am 0a940aac: Revert "Update list of unrestricted packages"
diff --git a/Android.mk b/Android.mk
index 0224435..bcd6b9e 100644
--- a/Android.mk
+++ b/Android.mk
@@ -10,7 +10,14 @@
 
 LOCAL_JAVA_LIBRARIES := ext
 
-LOCAL_STATIC_JAVA_LIBRARIES += android-common
+LOCAL_STATIC_JAVA_LIBRARIES += android-common com.android.vcard
+
+# The Emma tool analyzes code coverage when running unit tests on the
+# application. This configuration line selects which packages will be analyzed,
+# leaving out code which is tested by other means (e.g. static libraries) that
+# would dilute the coverage results. These options do not affect regular
+# production builds.
+LOCAL_EMMA_COVERAGE_FILTER := +com.android.providers.contacts.*
 
 # The Emma tool analyzes code coverage when running unit tests on the
 # application. This configuration line selects which packages will be analyzed,
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f56fac4..d36dd0c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -12,6 +12,8 @@
     <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.cp" />
     <uses-permission android:name="android.permission.SUBSCRIBED_FEEDS_READ" />
     <uses-permission android:name="android.permission.SUBSCRIBED_FEEDS_WRITE" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 
     <application android:process="android.process.acore"
         android:label="@string/app_label"
@@ -59,5 +61,30 @@
                 <action android:name="android.intent.action.PRE_BOOT_COMPLETED"/>
             </intent-filter>
         </receiver>
+
+        <receiver android:name="PackageIntentReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.PACKAGE_ADDED" />
+                <data android:scheme="package" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.PACKAGE_REPLACED" />
+                <data android:scheme="package" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.PACKAGE_REMOVED" />
+                <data android:scheme="package" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.PACKAGE_CHANGED" />
+                <data android:scheme="package" />
+            </intent-filter>
+        </receiver>
+
+        <receiver android:name="LocaleChangeReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.LOCALE_CHANGED"/>
+            </intent-filter>
+        </receiver>
     </application>
 </manifest>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index fe20126..285513e 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Kontakty"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Jiné"</string>
 </resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 06d13e3..54311f9 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Kontaktpersoner"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Andre"</string>
 </resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 005eb0d..749d9fd 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Kontakte"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Andere"</string>
 </resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index f1cc0f8..e6b59b6 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Επαφές"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Άλλο"</string>
 </resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 73225e5..af8742b 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Contactos"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Otro"</string>
 </resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index eecb1e1..730a3e4 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Contactos"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Otro"</string>
 </resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 7c669b0..6cef179 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Contacts"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Autre"</string>
 </resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index bd749d4..f02e6b6 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Contatti"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Altro"</string>
 </resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 7913b36..38c7d0c 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"連絡先"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"その他"</string>
 </resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 140a996..9927a28 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"주소록"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"기타"</string>
 </resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 327f8d3..b306fee 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Kontakter"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Andre"</string>
 </resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index ccfc34e..859f194 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Contacten"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Overig"</string>
 </resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index f3b4122..c645d4b 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Kontakty"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Inne"</string>
 </resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 08bcae1..5c6c445 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Contactos"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Outro"</string>
 </resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 3186b64..20cbef2 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Contatos"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Outros"</string>
 </resources>
diff --git a/res/values-rm/strings.xml b/res/values-rm/strings.xml
new file mode 100644
index 0000000..057fe15
--- /dev/null
+++ b/res/values-rm/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+  
+          http://www.apache.org/licenses/LICENSE-2.0
+  
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="app_label" msgid="3389954322874982620">"Glista da contacts"</string>
+    <string name="provider_label" msgid="6012150850819899907">"Contacts"</string>
+    <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 default_directory (93961630309570294) -->
+    <skip />
+    <!-- no translation found for local_invisible_directory (705244318477396120) -->
+    <skip />
+</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 6351180..c7353f6 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Контакты"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Другое"</string>
 </resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index d7a6707..bacb6b8 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Kontakter"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Övrigt"</string>
 </resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index 92cea75..2cc4330 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"Kişiler"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"Diğer"</string>
 </resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 17a6003..4471b68 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"联系人"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"其他"</string>
 </resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 9da7cb5..4a2a4d8 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -21,4 +21,6 @@
     <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="default_directory" msgid="93961630309570294">"聯絡人"</string>
+    <string name="local_invisible_directory" msgid="705244318477396120">"其他"</string>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ba8a7de..0e39d61 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -31,5 +31,11 @@
 
     <!-- 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>
+    
+    <!-- The name of the default contact directory -->
+    <string name="default_directory">Contacts</string>
+
+    <!-- The name of the invisible local contact directory -->
+    <string name="local_invisible_directory">Other</string>
 
 </resources>
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index 317e4a8..a574bee 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -65,11 +65,13 @@
         sCallsProjectionMap.put(Calls.CACHED_NAME, Calls.CACHED_NAME);
         sCallsProjectionMap.put(Calls.CACHED_NUMBER_TYPE, Calls.CACHED_NUMBER_TYPE);
         sCallsProjectionMap.put(Calls.CACHED_NUMBER_LABEL, Calls.CACHED_NUMBER_LABEL);
+        sCallsProjectionMap.put(Calls.COUNTRY_ISO, Calls.COUNTRY_ISO);
     }
 
     private ContactsDatabaseHelper mDbHelper;
     private DatabaseUtils.InsertHelper mCallsInserter;
     private boolean mUseStrictPhoneNumberComparation;
+    private CountryMonitor mCountryMonitor;
 
     @Override
     public boolean onCreate() {
@@ -82,7 +84,7 @@
         mUseStrictPhoneNumberComparation =
             context.getResources().getBoolean(
                     com.android.internal.R.bool.config_use_strict_phone_number_comparation);
-
+        mCountryMonitor = CountryMonitor.getInstance(context);
         return true;
     }
 
@@ -150,6 +152,9 @@
 
     @Override
     public Uri insert(Uri uri, ContentValues values) {
+        // Inserted the current country code, so we know the country
+        // the number belongs to.
+        values.put(Calls.COUNTRY_ISO, getCurrentCountryIso());
         long rowId = mCallsInserter.insert(values);
         if (rowId > 0) {
             notifyChange();
@@ -206,4 +211,8 @@
         getContext().getContentResolver().notifyChange(CallLog.CONTENT_URI, null,
                 false /* wake up sync adapters */);
     }
+
+    protected String getCurrentCountryIso() {
+        return mCountryMonitor.getCountryIso();
+    }
 }
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java
index 314ed0a..019f9f1 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -35,14 +35,16 @@
 import android.database.sqlite.SQLiteStatement;
 import android.net.Uri;
 import android.provider.ContactsContract.AggregationExceptions;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.DisplayNameSources;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.StatusUpdates;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
 import android.text.TextUtils;
 import android.util.EventLog;
 import android.util.Log;
@@ -93,6 +95,9 @@
     private final ContactsProvider2 mContactsProvider;
     private final ContactsDatabaseHelper mDbHelper;
     private PhotoPriorityResolver mPhotoPriorityResolver;
+    private final NameSplitter mNameSplitter;
+    private final CommonNicknameCache mCommonNicknameCache;
+
     private boolean mEnabled = true;
 
     /** Precompiled sql statement for setting an aggregated presence */
@@ -130,6 +135,19 @@
     private DisplayNameCandidate mDisplayNameCandidate = new DisplayNameCandidate();
 
     /**
+     * Parameter for the suggestion lookup query.
+     */
+    public static final class AggregationSuggestionParameter {
+        public final String kind;
+        public final String value;
+
+        public AggregationSuggestionParameter(String kind, String value) {
+            this.kind = kind;
+            this.value = value;
+        }
+    }
+
+    /**
      * Captures a potential match for a given name. The matching algorithm
      * constructs a bunch of NameMatchCandidate objects for various potential matches
      * and then executes the search in bulk.
@@ -169,6 +187,10 @@
         public void clear() {
             mCount = 0;
         }
+
+        public boolean isEmpty() {
+            return mCount == 0;
+        }
     }
 
     /**
@@ -200,10 +222,13 @@
      */
     public ContactAggregator(ContactsProvider2 contactsProvider,
             ContactsDatabaseHelper contactsDatabaseHelper,
-            PhotoPriorityResolver photoPriorityResolver) {
+            PhotoPriorityResolver photoPriorityResolver, NameSplitter nameSplitter,
+            CommonNicknameCache commonNicknameCache) {
         mContactsProvider = contactsProvider;
         mDbHelper = contactsDatabaseHelper;
         mPhotoPriorityResolver = photoPriorityResolver;
+        mNameSplitter = nameSplitter;
+        mCommonNicknameCache = commonNicknameCache;
 
         SQLiteDatabase db = mDbHelper.getReadableDatabase();
 
@@ -212,17 +237,17 @@
         final String replaceAggregatePresenceSql =
                 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
                 + AggregatedPresenceColumns.CONTACT_ID + ", "
-                + StatusUpdates.PRESENCE_STATUS + ", "
+                + StatusUpdates.PRESENCE + ", "
                 + StatusUpdates.CHAT_CAPABILITY + ")"
                 + " SELECT " + PresenceColumns.CONTACT_ID + ","
-                + StatusUpdates.PRESENCE_STATUS + ","
+                + StatusUpdates.PRESENCE + ","
                 + StatusUpdates.CHAT_CAPABILITY
                 + " FROM " + Tables.PRESENCE
                 + " WHERE "
-                + " (" + StatusUpdates.PRESENCE_STATUS
+                + " (" + StatusUpdates.PRESENCE
                 +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
                 + " = (SELECT "
-                + "MAX (" + StatusUpdates.PRESENCE_STATUS
+                + "MAX (" + StatusUpdates.PRESENCE
                 +       " * 10 + " + StatusUpdates.CHAT_CAPABILITY + ")"
                 + " FROM " + Tables.PRESENCE
                 + " WHERE " + PresenceColumns.CONTACT_ID
@@ -997,6 +1022,9 @@
         int NAME_TYPE_B = 3;
     }
 
+    /**
+     * Finds contacts with names matching the name of the specified raw contact.
+     */
     private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, long rawContactId,
             ContactMatcher matcher) {
         mSelectionArgs1[0] = String.valueOf(rawContactId);
@@ -1021,6 +1049,101 @@
         }
     }
 
+    private interface NameLookupMatchQueryWithParameter {
+        String TABLE = Tables.NAME_LOOKUP
+                + " JOIN " + Tables.RAW_CONTACTS +
+                " ON (" + NameLookupColumns.RAW_CONTACT_ID + " = "
+                        + Tables.RAW_CONTACTS + "." + RawContacts._ID + ")";
+
+        String[] COLUMNS = new String[] {
+            RawContacts.CONTACT_ID,
+            NameLookupColumns.NORMALIZED_NAME,
+            NameLookupColumns.NAME_TYPE,
+        };
+
+        int CONTACT_ID = 0;
+        int NAME = 1;
+        int NAME_TYPE = 2;
+    }
+
+    private final class NameLookupSelectionBuilder extends NameLookupBuilder {
+
+        private final MatchCandidateList mCandidates;
+
+        private StringBuilder mSelection = new StringBuilder(
+                NameLookupColumns.NORMALIZED_NAME + " IN(");
+
+
+        public NameLookupSelectionBuilder(NameSplitter splitter, MatchCandidateList candidates) {
+            super(splitter);
+            this.mCandidates = candidates;
+        }
+
+        @Override
+        protected String[] getCommonNicknameClusters(String normalizedName) {
+            return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
+        }
+
+        @Override
+        protected void insertNameLookup(
+                long rawContactId, long dataId, int lookupType, String string) {
+            mCandidates.add(string, lookupType);
+            DatabaseUtils.appendEscapedSQLString(mSelection, string);
+            mSelection.append(',');
+        }
+
+        public boolean isEmpty() {
+            return mCandidates.isEmpty();
+        }
+
+        public String getSelection() {
+            mSelection.setLength(mSelection.length() - 1);      // Strip last comma
+            mSelection.append(')');
+            return mSelection.toString();
+        }
+
+        public int getLookupType(String name) {
+            for (int i = 0; i < mCandidates.mCount; i++) {
+                if (mCandidates.mList.get(i).mName.equals(name)) {
+                    return mCandidates.mList.get(i).mLookupType;
+                }
+            }
+            throw new IllegalStateException();
+        }
+    }
+
+    /**
+     * Finds contacts with names matching the specified name.
+     */
+    private void updateMatchScoresBasedOnNameMatches(SQLiteDatabase db, String query,
+            MatchCandidateList candidates, ContactMatcher matcher) {
+        NameLookupSelectionBuilder builder = new NameLookupSelectionBuilder(
+                mNameSplitter, candidates);
+        builder.insertNameLookup(0, 0, query, FullNameStyle.UNDEFINED);
+        if (builder.isEmpty()) {
+            return;
+        }
+
+        Cursor c = db.query(NameLookupMatchQueryWithParameter.TABLE,
+                NameLookupMatchQueryWithParameter.COLUMNS, builder.getSelection(), null, null, null,
+                null, PRIMARY_HIT_LIMIT_STRING);
+        try {
+            while (c.moveToNext()) {
+                long contactId = c.getLong(NameLookupMatchQueryWithParameter.CONTACT_ID);
+                String name = c.getString(NameLookupMatchQueryWithParameter.NAME);
+                int nameTypeA = builder.getLookupType(name);
+                int nameTypeB = c.getInt(NameLookupMatchQueryWithParameter.NAME_TYPE);
+                matcher.matchName(contactId, nameTypeA, name, nameTypeB, name,
+                        ContactMatcher.MATCHING_ALGORITHM_EXACT);
+                if (nameTypeA == NameLookupType.NICKNAME && nameTypeB == NameLookupType.NICKNAME) {
+                    matcher.updateScoreWithNicknameMatch(contactId);
+                }
+            }
+        } finally {
+            c.close();
+        }
+    }
+
     private interface EmailLookupQuery {
         String TABLE = Tables.DATA + " dataA"
                 + " JOIN " + Tables.DATA + " dataB" +
@@ -1251,9 +1374,8 @@
                         + Contacts.STARRED + ", "
                         + Contacts.HAS_PHONE_NUMBER + ", "
                         + ContactsColumns.SINGLE_IS_RESTRICTED + ", "
-                        + Contacts.LOOKUP_KEY + ", "
-                        + Contacts.IN_VISIBLE_GROUP + ") " +
-                " VALUES (?,?,?,?,?,?,?,?,?,?,0)";
+                        + Contacts.LOOKUP_KEY + ") " +
+                " VALUES (?,?,?,?,?,?,?,?,?,?)";
 
         int NAME_RAW_CONTACT_ID = 1;
         int PHOTO_ID = 2;
@@ -1671,10 +1793,11 @@
      * Finds matching contacts and returns a cursor on those.
      */
     public Cursor queryAggregationSuggestions(SQLiteQueryBuilder qb, String[] projection,
-            long contactId, int maxSuggestions, String filter) {
+            long contactId, int maxSuggestions, String filter,
+            ArrayList<AggregationSuggestionParameter> parameters) {
         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
 
-        List<MatchScore> bestMatches = findMatchingContacts(db, contactId);
+        List<MatchScore> bestMatches = findMatchingContacts(db, contactId, parameters);
         return queryMatchingContacts(qb, db, contactId, projection, bestMatches, maxSuggestions,
                 filter);
     }
@@ -1784,8 +1907,10 @@
     /**
      * Finds contacts with data matches and returns a list of {@link MatchScore}'s in the
      * descending order of match score.
+     * @param parameters
      */
-    private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId) {
+    private List<MatchScore> findMatchingContacts(final SQLiteDatabase db, long contactId,
+            ArrayList<AggregationSuggestionParameter> parameters) {
 
         MatchCandidateList candidates = new MatchCandidateList();
         ContactMatcher matcher = new ContactMatcher();
@@ -1793,16 +1918,21 @@
         // Don't aggregate a contact with itself
         matcher.keepOut(contactId);
 
-        final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
-                RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
-        try {
-            while (c.moveToNext()) {
-                long rawContactId = c.getLong(RawContactIdQuery._ID);
-                updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
-                        matcher);
+        if (parameters == null || parameters.size() == 0) {
+            final Cursor c = db.query(RawContactIdQuery.TABLE, RawContactIdQuery.COLUMNS,
+                    RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
+            try {
+                while (c.moveToNext()) {
+                    long rawContactId = c.getLong(RawContactIdQuery._ID);
+                    updateMatchScoresForSuggestionsBasedOnDataMatches(db, rawContactId, candidates,
+                            matcher);
+                }
+            } finally {
+                c.close();
             }
-        } finally {
-            c.close();
+        } else {
+            updateMatchScoresForSuggestionsBasedOnDataMatches(db, candidates,
+                    matcher, parameters);
         }
 
         return matcher.pickBestMatches(ContactMatcher.SCORE_THRESHOLD_SUGGEST);
@@ -1820,4 +1950,16 @@
         loadNameMatchCandidates(db, rawContactId, candidates, false);
         lookupApproximateNameMatches(db, candidates, matcher);
     }
+
+    private void updateMatchScoresForSuggestionsBasedOnDataMatches(SQLiteDatabase db,
+            MatchCandidateList candidates, ContactMatcher matcher,
+            ArrayList<AggregationSuggestionParameter> parameters) {
+        for (AggregationSuggestionParameter parameter : parameters) {
+            if (AggregationSuggestions.PARAMETER_MATCH_NAME.equals(parameter.kind)) {
+                updateMatchScoresBasedOnNameMatches(db, parameter.value, candidates, matcher);
+            }
+
+            // TODO: add support for other parameter kinds
+        }
+    }
 }
diff --git a/src/com/android/providers/contacts/ContactDirectoryManager.java b/src/com/android/providers/contacts/ContactDirectoryManager.java
new file mode 100644
index 0000000..51ad4a4
--- /dev/null
+++ b/src/com/android/providers/contacts/ContactDirectoryManager.java
@@ -0,0 +1,497 @@
+/*
+ * 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 com.android.providers.contacts.ContactsDatabaseHelper.DirectoryColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.google.android.collect.Lists;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ProviderInfo;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages the contents of the {@link Directory} table.
+ */
+public class ContactDirectoryManager extends HandlerThread {
+
+    private static final String TAG = "ContactDirectoryManager";
+
+    private static final int MESSAGE_SCAN_ALL_PROVIDERS = 0;
+    private static final int MESSAGE_SCAN_PACKAGES_BY_UID = 1;
+
+    public static final String PROPERTY_DIRECTORY_SCAN_COMPLETE = "directoryScanComplete";
+    public static final String CONTACT_DIRECTORY_META_DATA = "android.content.ContactDirectory";
+
+    public class DirectoryInfo {
+        long id;
+        String packageName;
+        String authority;
+        String accountName;
+        String accountType;
+        String displayName;
+        int typeResourceId;
+        int exportSupport = Directory.EXPORT_SUPPORT_NONE;
+        int shortcutSupport = Directory.SHORTCUT_SUPPORT_NONE;
+        int photoSupport = Directory.PHOTO_SUPPORT_NONE;
+    }
+
+    private final static class DirectoryQuery {
+        public static final String[] PROJECTION = {
+            Directory.ACCOUNT_NAME,
+            Directory.ACCOUNT_TYPE,
+            Directory.DISPLAY_NAME,
+            Directory.TYPE_RESOURCE_ID,
+            Directory.EXPORT_SUPPORT,
+            Directory.SHORTCUT_SUPPORT,
+            Directory.PHOTO_SUPPORT,
+        };
+
+        public static final int ACCOUNT_NAME = 0;
+        public static final int ACCOUNT_TYPE = 1;
+        public static final int DISPLAY_NAME = 2;
+        public static final int TYPE_RESOURCE_ID = 3;
+        public static final int EXPORT_SUPPORT = 4;
+        public static final int SHORTCUT_SUPPORT = 5;
+        public static final int PHOTO_SUPPORT = 6;
+    }
+
+    private final ContactsProvider2 mContactsProvider;
+    private Context mContext;
+    private Handler mHandler;
+
+    public ContactDirectoryManager(ContactsProvider2 contactsProvider) {
+        super("DirectoryManager", Process.THREAD_PRIORITY_BACKGROUND);
+        this.mContactsProvider = contactsProvider;
+        this.mContext = contactsProvider.getContext();
+    }
+
+    public ContactsDatabaseHelper getDbHelper() {
+        return (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper();
+    }
+
+    /**
+     * Launches an asynchronous scan of all packages.
+     */
+    @Override
+    public void start() {
+        super.start();
+        if (areTypeResourceIdsValid()) {
+            scheduleScanAllPackages(false);
+        } else {
+            getDbHelper().setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
+            scanAllPackagesIfNeeded();
+        }
+    }
+    /**
+     * Launches an asynchronous scan of all packages owned by the current calling UID.
+     */
+    public void scheduleDirectoryUpdateForCaller() {
+        final int callingUid = Binder.getCallingUid();
+        if (isAlive()) {
+            Handler handler = getHandler();
+            handler.sendMessage(handler.obtainMessage(MESSAGE_SCAN_PACKAGES_BY_UID, callingUid, 0));
+        } else {
+            scanPackagesByUid(callingUid);
+        }
+    }
+
+    protected Handler getHandler() {
+        if (mHandler == null) {
+            mHandler = new Handler(getLooper()) {
+                @Override
+                public void handleMessage(Message msg) {
+                    ContactDirectoryManager.this.handleMessage(msg);
+                }
+            };
+        }
+        return mHandler;
+    }
+
+    protected void handleMessage(Message msg) {
+        switch(msg.what) {
+            case MESSAGE_SCAN_ALL_PROVIDERS:
+                scanAllPackagesIfNeeded();
+                break;
+            case MESSAGE_SCAN_PACKAGES_BY_UID:
+                scanPackagesByUid(msg.arg1);
+                break;
+        }
+    }
+
+    /**
+     * Scans all packages owned by the specified calling UID looking for contact
+     * directory providers.
+     */
+    public void scanPackagesByUid(int callingUid) {
+        final PackageManager pm = mContext.getPackageManager();
+        final String[] callerPackages = pm.getPackagesForUid(callingUid);
+        if (callerPackages != null) {
+            for (int i = 0; i < callerPackages.length; i++) {
+                onPackageChanged(callerPackages[i]);
+            }
+        }
+    }
+
+    /**
+     * Scans through existing directories to see if the cached resource IDs still
+     * match their original resource names.  If not - plays it safe by refreshing all directories.
+     *
+     * @return true if all resource IDs were found valid
+     */
+    private boolean areTypeResourceIdsValid() {
+        final PackageManager pm = mContext.getPackageManager();
+        SQLiteDatabase db = getDbHelper().getReadableDatabase();
+
+        Cursor cursor = db.query(Tables.DIRECTORIES,
+                new String[] { Directory.TYPE_RESOURCE_ID, Directory.PACKAGE_NAME,
+                        DirectoryColumns.TYPE_RESOURCE_NAME }, null, null, null, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                int resourceId = cursor.getInt(0);
+                if (resourceId != 0) {
+                    String packageName = cursor.getString(1);
+                    String storedResourceName = cursor.getString(2);
+                    String resourceName = getResourceNameById(pm, packageName, resourceId);
+                    if (!TextUtils.equals(storedResourceName, resourceName)) {
+                        return false;
+                    }
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return true;
+    }
+
+    /**
+     * Given a resource ID, returns the corresponding resource name or null if the package name /
+     * resource ID combination is invalid.
+     */
+    private String getResourceNameById(PackageManager pm, String packageName, int resourceId) {
+        try {
+            Resources resources = pm.getResourcesForApplication(packageName);
+            return resources.getResourceName(resourceId);
+        } catch (NameNotFoundException e) {
+            return null;
+        } catch (NotFoundException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Scans all packages for directory content providers.
+     */
+    private void scanAllPackagesIfNeeded() {
+        String scanComplete = getDbHelper().getProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
+        if (!"0".equals(scanComplete)) {
+            return;
+        }
+
+        long start = SystemClock.currentThreadTimeMillis();
+        int count = scanAllPackages();
+        getDbHelper().setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "1");
+        long end = SystemClock.currentThreadTimeMillis();
+        Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms");
+
+        // Announce the change to listeners of the contacts authority
+        mContactsProvider.notifyChange(false);
+    }
+
+    public void scheduleScanAllPackages(boolean rescan) {
+        if (rescan) {
+            getDbHelper().setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
+        }
+        if (isAlive()) {
+            getHandler().sendEmptyMessage(MESSAGE_SCAN_ALL_PROVIDERS);
+        } else {
+            scanAllPackagesIfNeeded();
+        }
+    }
+
+    /* Visible for testing */
+    int scanAllPackages() {
+        SQLiteDatabase db = getDbHelper().getWritableDatabase();
+        insertDefaultDirectory(db);
+        insertLocalInvisibleDirectory(db);
+
+        int count = 0;
+        PackageManager pm = mContext.getPackageManager();
+        List<PackageInfo> packages = pm.getInstalledPackages(
+                PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA);
+        if (packages != null) {
+            for (PackageInfo packageInfo : packages) {
+                // Check all packages except the one containing ContactsProvider itself
+                if (!packageInfo.packageName.equals(mContext.getPackageName())) {
+                    count += updateDirectoriesForPackage(packageInfo, true);
+                }
+            }
+        }
+        return count;
+    }
+
+    private void insertDefaultDirectory(SQLiteDatabase db) {
+        ContentValues values = new ContentValues();
+        values.put(Directory._ID, Directory.DEFAULT);
+        values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName);
+        values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
+        values.put(Directory.TYPE_RESOURCE_ID, R.string.default_directory);
+        values.put(DirectoryColumns.TYPE_RESOURCE_NAME,
+                mContext.getResources().getResourceName(R.string.default_directory));
+        values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
+        values.put(Directory.SHORTCUT_SUPPORT, Directory.SHORTCUT_SUPPORT_FULL);
+        values.put(Directory.PHOTO_SUPPORT, Directory.PHOTO_SUPPORT_FULL);
+        db.replace(Tables.DIRECTORIES, null, values);
+    }
+
+    private void insertLocalInvisibleDirectory(SQLiteDatabase db) {
+        ContentValues values = new ContentValues();
+        values.put(Directory._ID, Directory.LOCAL_INVISIBLE);
+        values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName);
+        values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
+        values.put(Directory.TYPE_RESOURCE_ID, R.string.local_invisible_directory);
+        values.put(DirectoryColumns.TYPE_RESOURCE_NAME,
+                mContext.getResources().getResourceName(R.string.local_invisible_directory));
+        values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
+        values.put(Directory.SHORTCUT_SUPPORT, Directory.SHORTCUT_SUPPORT_FULL);
+        values.put(Directory.PHOTO_SUPPORT, Directory.PHOTO_SUPPORT_FULL);
+        db.replace(Tables.DIRECTORIES, null, values);
+    }
+
+    /**
+     * Scans the specified package for content directories.  The package may have
+     * already been removed, so packageName does not necessarily correspond to
+     * an installed package.
+     */
+    public void onPackageChanged(String packageName) {
+        PackageManager pm = mContext.getPackageManager();
+        PackageInfo packageInfo = null;
+
+        try {
+            packageInfo = pm.getPackageInfo(packageName,
+                    PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA);
+        } catch (NameNotFoundException e) {
+            // The package got removed
+            packageInfo = new PackageInfo();
+            packageInfo.packageName = packageName;
+        }
+
+        updateDirectoriesForPackage(packageInfo, false);
+    }
+
+    /**
+     * Scans the specified package for content directories and updates the {@link Directory}
+     * table accordingly.
+     */
+    private int updateDirectoriesForPackage(PackageInfo packageInfo, boolean initialScan) {
+        ArrayList<DirectoryInfo> directories = Lists.newArrayList();
+
+        ProviderInfo[] providers = packageInfo.providers;
+        if (providers != null) {
+            for (ProviderInfo provider : providers) {
+                Bundle metaData = provider.metaData;
+                if (metaData != null) {
+                    Object trueFalse = metaData.get(CONTACT_DIRECTORY_META_DATA);
+                    if (trueFalse != null && Boolean.TRUE.equals(trueFalse)) {
+                        queryDirectoriesForAuthority(directories, provider);
+                    }
+                }
+            }
+        }
+
+        if (directories.size() == 0 && initialScan) {
+            return 0;
+        }
+
+        SQLiteDatabase db = getDbHelper().getWritableDatabase();
+        db.beginTransaction();
+        try {
+            updateDirectories(db, directories);
+            // Clear out directories that are no longer present
+            StringBuilder sb = new StringBuilder(Directory.PACKAGE_NAME + "=?");
+            if (!directories.isEmpty()) {
+                sb.append(" AND " + Directory._ID + " NOT IN(");
+                for (DirectoryInfo info: directories) {
+                    sb.append(info.id).append(",");
+                }
+                sb.setLength(sb.length() - 1);  // Remove the extra comma
+                sb.append(")");
+            }
+            db.delete(Tables.DIRECTORIES, sb.toString(), new String[] { packageInfo.packageName });
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+
+        mContactsProvider.resetDirectoryCache();
+        return directories.size();
+    }
+
+    /**
+     * Sends a {@link Directory#CONTENT_URI} request to a specific contact directory
+     * provider and appends all discovered directories to the directoryInfo list.
+     */
+    protected void queryDirectoriesForAuthority(
+            ArrayList<DirectoryInfo> directoryInfo, ProviderInfo provider) {
+        Uri uri = new Uri.Builder().scheme("content")
+                .authority(provider.authority).appendPath("directories").build();
+        Cursor cursor = null;
+        try {
+            cursor = mContext.getContentResolver().query(
+                    uri, DirectoryQuery.PROJECTION, null, null, null);
+            if (cursor == null) {
+                Log.i(TAG, providerDescription(provider) + " returned a NULL cursor.");
+            } else {
+                while (cursor.moveToNext()) {
+                    DirectoryInfo info = new DirectoryInfo();
+                    info.packageName = provider.packageName;
+                    info.authority = provider.authority;
+                    info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
+                    info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
+                    info.displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+                    if (!cursor.isNull(DirectoryQuery.TYPE_RESOURCE_ID)) {
+                        info.typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+                    }
+                    if (!cursor.isNull(DirectoryQuery.EXPORT_SUPPORT)) {
+                        int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
+                        switch (exportSupport) {
+                            case Directory.EXPORT_SUPPORT_NONE:
+                            case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY:
+                            case Directory.EXPORT_SUPPORT_ANY_ACCOUNT:
+                                info.exportSupport = exportSupport;
+                                break;
+                            default:
+                                Log.e(TAG, providerDescription(provider)
+                                        + " - invalid export support flag: " + exportSupport);
+                        }
+                    }
+                    if (!cursor.isNull(DirectoryQuery.SHORTCUT_SUPPORT)) {
+                        int shortcutSupport = cursor.getInt(DirectoryQuery.SHORTCUT_SUPPORT);
+                        switch (shortcutSupport) {
+                            case Directory.SHORTCUT_SUPPORT_NONE:
+                            case Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY:
+                            case Directory.SHORTCUT_SUPPORT_FULL:
+                                info.shortcutSupport = shortcutSupport;
+                                break;
+                            default:
+                                Log.e(TAG, providerDescription(provider)
+                                        + " - invalid shortcut support flag: " + shortcutSupport);
+                        }
+                    }
+                    if (!cursor.isNull(DirectoryQuery.PHOTO_SUPPORT)) {
+                        int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT);
+                        switch (photoSupport) {
+                            case Directory.PHOTO_SUPPORT_NONE:
+                            case Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY:
+                            case Directory.PHOTO_SUPPORT_FULL_SIZE_ONLY:
+                            case Directory.PHOTO_SUPPORT_FULL:
+                                info.photoSupport = photoSupport;
+                                break;
+                            default:
+                                Log.e(TAG, providerDescription(provider)
+                                        + " - invalid photo support flag: " + photoSupport);
+                        }
+                    }
+                    directoryInfo.add(info);
+                }
+            }
+        } catch (Throwable t) {
+            Log.e(TAG, providerDescription(provider) + " exception", t);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    /**
+     * Updates the directories tables in the database to match the info received
+     * from directory providers.
+     */
+    private void updateDirectories(SQLiteDatabase db, ArrayList<DirectoryInfo> directoryInfo) {
+        PackageManager pm = mContext.getPackageManager();
+
+        // Insert or replace existing directories.
+        // This happens so infrequently that we can use a less-then-optimal one-a-time approach
+        for (DirectoryInfo info : directoryInfo) {
+            ContentValues values = new ContentValues();
+            values.put(Directory.PACKAGE_NAME, info.packageName);
+            values.put(Directory.DIRECTORY_AUTHORITY, info.authority);
+            values.put(Directory.ACCOUNT_NAME, info.accountName);
+            values.put(Directory.ACCOUNT_TYPE, info.accountType);
+            values.put(Directory.TYPE_RESOURCE_ID, info.typeResourceId);
+            values.put(Directory.DISPLAY_NAME, info.displayName);
+            values.put(Directory.EXPORT_SUPPORT, info.exportSupport);
+            values.put(Directory.SHORTCUT_SUPPORT, info.shortcutSupport);
+            values.put(Directory.PHOTO_SUPPORT, info.photoSupport);
+
+            if (info.typeResourceId != 0) {
+                String resourceName = getResourceNameById(
+                        pm, info.packageName, info.typeResourceId);
+                values.put(DirectoryColumns.TYPE_RESOURCE_NAME, resourceName);
+            }
+
+            Cursor cursor = db.query(Tables.DIRECTORIES, new String[] { Directory._ID },
+                    Directory.PACKAGE_NAME + "=? AND " + Directory.DIRECTORY_AUTHORITY + "=? AND "
+                            + Directory.ACCOUNT_NAME + "=? AND " + Directory.ACCOUNT_TYPE + "=?",
+                    new String[] {
+                            info.packageName, info.authority, info.accountName, info.accountType },
+                    null, null, null);
+            try {
+                long id;
+                if (cursor.moveToFirst()) {
+                    id = cursor.getLong(0);
+                    db.update(Tables.DIRECTORIES, values, Directory._ID + "=?",
+                            new String[] { String.valueOf(id) });
+                } else {
+                    id = db.insert(Tables.DIRECTORIES, null, values);
+                }
+                info.id = id;
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    protected String providerDescription(ProviderInfo provider) {
+        return "Directory provider " + provider.packageName + "(" + provider.authority + ")";
+    }
+}
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 8f1253a..64976d5 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -16,7 +16,7 @@
 
 package com.android.providers.contacts;
 
-import com.android.internal.content.SyncStateContentProviderHelper;
+import com.android.common.content.SyncStateContentProviderHelper;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -34,28 +34,31 @@
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.database.sqlite.SQLiteStatement;
+import android.location.CountryDetector;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.provider.BaseColumns;
-import android.provider.ContactsContract;
 import android.provider.CallLog.Calls;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.AggregationExceptions;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.DisplayNameSources;
-import android.provider.ContactsContract.FullNameStyle;
-import android.provider.ContactsContract.Groups;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.Settings;
-import android.provider.ContactsContract.StatusUpdates;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Nickname;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.Settings;
+import android.provider.ContactsContract.StatusUpdates;
 import android.provider.SocialContract.Activities;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
@@ -85,7 +88,7 @@
      *   400-499 Honeycomb
      * </pre>
      */
-    static final int DATABASE_VERSION = 353;
+    static final int DATABASE_VERSION = 414;
 
     private static final String DATABASE_NAME = "contacts2.db";
     private static final String DATABASE_PRESENCE = "presence_db";
@@ -105,11 +108,12 @@
         public static final String AGGREGATED_PRESENCE = "agg_presence";
         public static final String NICKNAME_LOOKUP = "nickname_lookup";
         public static final String CALLS = "calls";
-        public static final String CONTACT_ENTITIES = "contact_entities_view";
-        public static final String CONTACT_ENTITIES_RESTRICTED = "contact_entities_view_restricted";
         public static final String STATUS_UPDATES = "status_updates";
         public static final String PROPERTIES = "properties";
         public static final String ACCOUNTS = "accounts";
+        public static final String VISIBLE_CONTACTS = "visible_contacts";
+        public static final String DIRECTORIES = "directories";
+        public static final String DEFAULT_DIRECTORY = "default_directory";
 
         public static final String DATA_JOIN_MIMETYPES = "data "
                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id)";
@@ -184,6 +188,12 @@
         public static final String CONTACTS_ALL = "view_contacts";
         public static final String CONTACTS_RESTRICTED = "view_contacts_restricted";
 
+        public static final String ENTITIES = "view_entities";
+        public static final String ENTITIES_RESTRICTED = "view_entities_restricted";
+
+        public static final String RAW_ENTITIES = "view_raw_entities";
+        public static final String RAW_ENTITIES_RESTRICTED = "view_raw_entities_restricted";
+
         public static final String GROUPS_ALL = "view_groups";
     }
 
@@ -225,6 +235,11 @@
 
         final String GROUP_HAS_ACCOUNT_AND_SOURCE_ID = Groups.SOURCE_ID + "=? AND "
                 + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?";
+
+        public static final String CONTACT_VISIBLE =
+            "EXISTS (SELECT _id FROM " + Tables.VISIBLE_CONTACTS
+                + " WHERE " + Tables.CONTACTS +"." + Contacts._ID
+                        + "=" + Tables.VISIBLE_CONTACTS +"." + Contacts._ID + ")";
     }
 
     public interface ContactsColumns {
@@ -282,7 +297,6 @@
         public static final String DISPLAY_NAME = RawContacts.DISPLAY_NAME_PRIMARY;
         public static final String DISPLAY_NAME_SOURCE = RawContacts.DISPLAY_NAME_SOURCE;
         public static final String AGGREGATION_NEEDED = "aggregation_needed";
-        public static final String CONTACT_IN_VISIBLE_GROUP = "contact_in_visible_group";
 
         public static final String CONCRETE_DISPLAY_NAME =
                 Tables.RAW_CONTACTS + "." + DISPLAY_NAME;
@@ -462,6 +476,10 @@
         String PROPERTY_VALUE = "property_value";
     }
 
+    public static final class DirectoryColumns {
+        public static final String TYPE_RESOURCE_NAME = "typeResourceName";
+    }
+
     /** In-memory cache of previously found MIME-type mappings */
     private final HashMap<String, Long> mMimetypeCache = new HashMap<String, Long>();
     /** In-memory cache of previously found package name mappings */
@@ -481,12 +499,6 @@
     private final Context mContext;
     private final SyncStateContentProviderHelper mSyncState;
 
-
-    /** Compiled statements for updating {@link Contacts#IN_VISIBLE_GROUP}. */
-    private SQLiteStatement mVisibleSpecificUpdate;
-    private SQLiteStatement mVisibleUpdateRawContacts;
-    private SQLiteStatement mVisibleSpecificUpdateRawContacts;
-
     private boolean mReopenDatabase = false;
 
     private static ContactsDatabaseHelper sSingleton = null;
@@ -511,7 +523,6 @@
      */
     ContactsDatabaseHelper(Context context) {
         super(context, DATABASE_NAME, null, DATABASE_VERSION);
-        if (false) Log.i(TAG, "Creating OpenHelper");
         Resources resources = context.getResources();
 
         mContext = context;
@@ -552,34 +563,6 @@
                 + " FROM " + Tables.ACTIVITIES_JOIN_MIMETYPES + " WHERE " + Tables.ACTIVITIES + "."
                 + Activities._ID + "=?");
 
-        // Change visibility of a specific contact
-        mVisibleSpecificUpdate = db.compileStatement(
-                "UPDATE " + Tables.CONTACTS +
-                " SET " + Contacts.IN_VISIBLE_GROUP + "=(" + Clauses.CONTACT_IS_VISIBLE + ")" +
-                " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
-
-        // Return visibility of the aggregate contact joined with the raw contact
-        String contactVisibility =
-                "SELECT " + Contacts.IN_VISIBLE_GROUP +
-                " FROM " + Tables.CONTACTS +
-                " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID;
-
-        // Set visibility of raw contacts to the visibility of corresponding aggregate contacts
-        mVisibleUpdateRawContacts = db.compileStatement(
-                "UPDATE " + Tables.RAW_CONTACTS +
-                " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(CASE WHEN ("
-                        + contactVisibility + ")=1 THEN 1 ELSE 0 END)" +
-                " WHERE " + RawContacts.DELETED + "=0" +
-                " AND " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "!=("
-                        + contactVisibility + ")=1");
-
-        // Set visibility of a raw contact to the visibility of corresponding aggregate contact
-        mVisibleSpecificUpdateRawContacts = db.compileStatement(
-                "UPDATE " + Tables.RAW_CONTACTS +
-                " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=("
-                        + contactVisibility + ")" +
-                " WHERE " + RawContacts.DELETED + "=0 AND " + RawContacts.CONTACT_ID + "=?");
-
         db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
         db.execSQL("CREATE TABLE IF NOT EXISTS " + DATABASE_PRESENCE + "." + Tables.PRESENCE + " ("+
                 StatusUpdates.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
@@ -604,7 +587,7 @@
                 + DATABASE_PRESENCE + "." + Tables.AGGREGATED_PRESENCE + " ("+
                 AggregatedPresenceColumns.CONTACT_ID
                         + " INTEGER PRIMARY KEY REFERENCES contacts(_id)," +
-                StatusUpdates.PRESENCE_STATUS + " INTEGER," +
+                StatusUpdates.PRESENCE + " INTEGER," +
                 StatusUpdates.CHAT_CAPABILITY + " INTEGER NOT NULL DEFAULT 0" +
         ");");
 
@@ -678,17 +661,12 @@
                 Contacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
                 Contacts.LAST_TIME_CONTACTED + " INTEGER," +
                 Contacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
-                Contacts.IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 1," +
                 Contacts.HAS_PHONE_NUMBER + " INTEGER NOT NULL DEFAULT 0," +
                 Contacts.LOOKUP_KEY + " TEXT," +
                 ContactsColumns.LAST_STATUS_UPDATE_ID + " INTEGER REFERENCES data(_id)," +
                 ContactsColumns.SINGLE_IS_RESTRICTED + " INTEGER NOT NULL DEFAULT 0" +
         ");");
 
-        db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
-                Contacts.IN_VISIBLE_GROUP +
-        ");");
-
         db.execSQL("CREATE INDEX contacts_has_phone_index ON " + Tables.CONTACTS + " (" +
                 Contacts.HAS_PHONE_NUMBER +
         ");");
@@ -708,6 +686,7 @@
                 RawContacts.ACCOUNT_NAME + " STRING DEFAULT NULL, " +
                 RawContacts.ACCOUNT_TYPE + " STRING DEFAULT NULL, " +
                 RawContacts.SOURCE_ID + " TEXT," +
+                RawContacts.RAW_CONTACT_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," +
                 RawContacts.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.DELETED + " INTEGER NOT NULL DEFAULT 0," +
@@ -731,7 +710,6 @@
                 RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE " +
                         ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," +
                 RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0," +
-                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.SYNC1 + " TEXT, " +
                 RawContacts.SYNC2 + " TEXT, " +
                 RawContacts.SYNC3 + " TEXT, " +
@@ -776,6 +754,7 @@
                 DataColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
                 DataColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," +
                 Data.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
+                Data.IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
                 Data.IS_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
                 Data.IS_SUPER_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
                 Data.DATA_VERSION + " INTEGER NOT NULL DEFAULT 0," +
@@ -815,7 +794,7 @@
         // Private phone numbers table used for lookup
         db.execSQL("CREATE TABLE " + Tables.PHONE_LOOKUP + " (" +
                 PhoneLookupColumns.DATA_ID
-                        + " INTEGER PRIMARY KEY REFERENCES data(_id) NOT NULL," +
+                        + " INTEGER REFERENCES data(_id) NOT NULL," +
                 PhoneLookupColumns.RAW_CONTACT_ID
                         + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
                 PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," +
@@ -878,6 +857,8 @@
                 Groups.DELETED + " INTEGER NOT NULL DEFAULT 0," +
                 Groups.GROUP_VISIBLE + " INTEGER NOT NULL DEFAULT 0," +
                 Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1," +
+                Groups.AUTO_ADD + " INTEGER NOT NULL DEFAULT 0," +
+                Groups.FAVORITES + " INTEGER NOT NULL DEFAULT 0," +
                 Groups.SYNC1 + " TEXT, " +
                 Groups.SYNC2 + " TEXT, " +
                 Groups.SYNC3 + " TEXT, " +
@@ -920,6 +901,14 @@
                     Settings.ACCOUNT_TYPE + ") ON CONFLICT REPLACE" +
         ");");
 
+        db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" +
+                Contacts._ID + " INTEGER PRIMARY KEY" +
+        ");");
+
+        db.execSQL("CREATE TABLE " + Tables.DEFAULT_DIRECTORY + " (" +
+                Contacts._ID + " INTEGER PRIMARY KEY" +
+        ");");
+
         // The table for recent calls is here so we can do table joins
         // on people, phones, and calls all in one place.
         db.execSQL("CREATE TABLE " + Tables.CALLS + " (" +
@@ -931,8 +920,8 @@
                 Calls.NEW + " INTEGER," +
                 Calls.CACHED_NAME + " TEXT," +
                 Calls.CACHED_NUMBER_TYPE + " INTEGER," +
-                Calls.CACHED_NUMBER_LABEL + " TEXT" +
-        ");");
+                Calls.CACHED_NUMBER_LABEL + " TEXT," +
+                Calls.COUNTRY_ISO + " TEXT" + ");");
 
         // Activities table
         db.execSQL("CREATE TABLE " + Tables.ACTIVITIES + " (" +
@@ -976,9 +965,10 @@
         // is added to the phone.
         db.execSQL("INSERT INTO accounts VALUES(NULL, NULL)");
 
+        createDirectoriesTable(db);
+
         createContactsViews(db);
         createGroupsView(db);
-        createContactEntitiesView(db);
         createContactsTriggers(db);
         createContactsIndexes(db);
 
@@ -1001,6 +991,28 @@
                 ContactsContract.AUTHORITY, new Bundle());
     }
 
+    private void createDirectoriesTable(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE " + Tables.DIRECTORIES + "(" +
+                Directory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                Directory.PACKAGE_NAME + " TEXT NOT NULL," +
+                Directory.DIRECTORY_AUTHORITY + " TEXT NOT NULL," +
+                Directory.TYPE_RESOURCE_ID + " INTEGER," +
+                DirectoryColumns.TYPE_RESOURCE_NAME + " TEXT," +
+                Directory.ACCOUNT_TYPE + " TEXT," +
+                Directory.ACCOUNT_NAME + " TEXT," +
+                Directory.DISPLAY_NAME + " TEXT, " +
+                Directory.EXPORT_SUPPORT + " INTEGER NOT NULL" +
+                        " DEFAULT " + Directory.EXPORT_SUPPORT_NONE + "," +
+                Directory.SHORTCUT_SUPPORT + " INTEGER NOT NULL" +
+                        " DEFAULT " + Directory.SHORTCUT_SUPPORT_NONE + "," +
+                Directory.PHOTO_SUPPORT + " INTEGER NOT NULL" +
+                        " DEFAULT " + Directory.PHOTO_SUPPORT_NONE +
+        ");");
+
+        // Trigger a full scan of directories in the system
+        setProperty(db, ContactDirectoryManager.PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
+    }
+
     private static void createContactsTriggers(SQLiteDatabase db) {
 
         /*
@@ -1018,6 +1030,16 @@
                                 + "=OLD." + RawContacts._ID
                 + "        OR " + AggregationExceptions.RAW_CONTACT_ID2
                                 + "=OLD." + RawContacts._ID + ";"
+                + "   DELETE FROM " + Tables.VISIBLE_CONTACTS
+                + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
+                + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
+                + "            WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID
+                + "           )=1;"
+                + "   DELETE FROM " + Tables.DEFAULT_DIRECTORY
+                + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
+                + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
+                + "            WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID
+                + "           )=1;"
                 + "   DELETE FROM " + Tables.CONTACTS
                 + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
                 + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
@@ -1093,13 +1115,11 @@
 
         db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key1_index");
         db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
-                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
                 RawContacts.SORT_KEY_PRIMARY +
         ");");
 
         db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key2_index");
         db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
-                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
                 RawContacts.SORT_KEY_ALTERNATIVE +
         ");");
     }
@@ -1111,6 +1131,10 @@
         db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_RESTRICTED + ";");
         db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS_ALL + ";");
         db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS_RESTRICTED + ";");
+        db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_ENTITIES + ";");
+        db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_ENTITIES_RESTRICTED + ";");
+        db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES + ";");
+        db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES_RESTRICTED + ";");
 
         String dataColumns =
                 Data.IS_PRIMARY + ", "
@@ -1118,6 +1142,7 @@
                 + Data.DATA_VERSION + ", "
                 + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + ","
                 + MimetypesColumns.MIMETYPE + " AS " + Data.MIMETYPE + ", "
+                + Data.IS_READ_ONLY + ", "
                 + Data.DATA1 + ", "
                 + Data.DATA2 + ", "
                 + Data.DATA3 + ", "
@@ -1150,6 +1175,14 @@
                 + RawContactsColumns.CONCRETE_SYNC3 + " AS " + RawContacts.SYNC3 + ","
                 + RawContactsColumns.CONCRETE_SYNC4 + " AS " + RawContacts.SYNC4;
 
+        String baseContactColumns =
+                Contacts.HAS_PHONE_NUMBER + ", "
+                + Contacts.NAME_RAW_CONTACT_ID + ", "
+                + Contacts.LOOKUP_KEY + ", "
+                + Contacts.PHOTO_ID + ", "
+                + Clauses.CONTACT_VISIBLE + " AS " + Contacts.IN_VISIBLE_GROUP + ", "
+                + ContactsColumns.LAST_STATUS_UPDATE_ID;
+
         String contactOptionColumns =
                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE
                         + " AS " + RawContacts.CUSTOM_RINGTONE + ","
@@ -1176,9 +1209,7 @@
                 + "name_raw_contact." + RawContacts.SORT_KEY_PRIMARY
                         + " AS " + Contacts.SORT_KEY_PRIMARY + ", "
                 + "name_raw_contact." + RawContacts.SORT_KEY_ALTERNATIVE
-                        + " AS " + Contacts.SORT_KEY_ALTERNATIVE + ", "
-                + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
-                        + " AS " + Contacts.IN_VISIBLE_GROUP;
+                        + " AS " + Contacts.SORT_KEY_ALTERNATIVE;
 
         String dataSelect = "SELECT "
                 + DataColumns.CONCRETE_ID + " AS " + Data._ID + ","
@@ -1188,10 +1219,11 @@
                 + dataColumns + ", "
                 + contactOptionColumns + ", "
                 + contactNameColumns + ", "
-                + Contacts.LOOKUP_KEY + ", "
-                + Contacts.PHOTO_ID + ", "
-                + Contacts.NAME_RAW_CONTACT_ID + ","
-                + ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
+                + baseContactColumns + ", "
+                + buildPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
+                        Contacts.PHOTO_URI) + ", "
+                + buildPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
+                        Contacts.PHOTO_THUMBNAIL_URI) + ", "
                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
                 + " FROM " + Tables.DATA
                 + " JOIN " + Tables.MIMETYPES + " ON ("
@@ -1224,6 +1256,7 @@
                 + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + ","
                 + RawContacts.CONTACT_ID + ", "
                 + RawContacts.AGGREGATION_MODE + ", "
+                + RawContacts.RAW_CONTACT_IS_READ_ONLY + ", "
                 + RawContacts.DELETED + ", "
                 + RawContacts.DISPLAY_NAME_SOURCE  + ", "
                 + RawContacts.DISPLAY_NAME_PRIMARY  + ", "
@@ -1244,9 +1277,7 @@
                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE
                         + " AS " + Contacts.CUSTOM_RINGTONE + ", "
                 + contactNameColumns + ", "
-                + Contacts.HAS_PHONE_NUMBER + ", "
-                + Contacts.LOOKUP_KEY + ", "
-                + Contacts.PHOTO_ID + ", "
+                + baseContactColumns + ", "
                 + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
                         + " AS " + Contacts.LAST_TIME_CONTACTED + ", "
                 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
@@ -1254,12 +1285,13 @@
                 + ContactsColumns.CONCRETE_STARRED
                         + " AS " + Contacts.STARRED + ", "
                 + ContactsColumns.CONCRETE_TIMES_CONTACTED
-                        + " AS " + Contacts.TIMES_CONTACTED + ", "
-                + ContactsColumns.LAST_STATUS_UPDATE_ID;
+                        + " AS " + Contacts.TIMES_CONTACTED;
 
         String contactsSelect = "SELECT "
                 + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
-                + contactsColumns
+                + contactsColumns + ", "
+                + buildPhotoUriAlias(ContactsColumns.CONCRETE_ID, Contacts.PHOTO_URI) + ", "
+                + buildPhotoUriAlias(ContactsColumns.CONCRETE_ID, Contacts.PHOTO_THUMBNAIL_URI)
                 + " FROM " + Tables.CONTACTS
                 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
                 +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
@@ -1267,79 +1299,17 @@
         db.execSQL("CREATE VIEW " + Views.CONTACTS_ALL + " AS " + contactsSelect);
         db.execSQL("CREATE VIEW " + Views.CONTACTS_RESTRICTED + " AS " + contactsSelect
                 + " WHERE " + ContactsColumns.SINGLE_IS_RESTRICTED + "=0");
-    }
 
-    private static void createGroupsView(SQLiteDatabase db) {
-        db.execSQL("DROP VIEW IF EXISTS " + Views.GROUPS_ALL + ";");
-        String groupsColumns =
-                Groups.ACCOUNT_NAME + ","
-                + Groups.ACCOUNT_TYPE + ","
-                + Groups.SOURCE_ID + ","
-                + Groups.VERSION + ","
-                + Groups.DIRTY + ","
-                + Groups.TITLE + ","
-                + Groups.TITLE_RES + ","
-                + Groups.NOTES + ","
-                + Groups.SYSTEM_ID + ","
-                + Groups.DELETED + ","
-                + Groups.GROUP_VISIBLE + ","
-                + Groups.SHOULD_SYNC + ","
-                + Groups.SYNC1 + ","
-                + Groups.SYNC2 + ","
-                + Groups.SYNC3 + ","
-                + Groups.SYNC4 + ","
-                + PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE;
-
-        String groupsSelect = "SELECT "
-                + GroupsColumns.CONCRETE_ID + " AS " + Groups._ID + ","
-                + groupsColumns
-                + " FROM " + Tables.GROUPS_JOIN_PACKAGES;
-
-        db.execSQL("CREATE VIEW " + Views.GROUPS_ALL + " AS " + groupsSelect);
-    }
-
-    private static void createContactEntitiesView(SQLiteDatabase db) {
-        db.execSQL("DROP VIEW IF EXISTS " + Tables.CONTACT_ENTITIES + ";");
-        db.execSQL("DROP VIEW IF EXISTS " + Tables.CONTACT_ENTITIES_RESTRICTED + ";");
-
-        String contactEntitiesSelect = "SELECT "
-                + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + ","
-                + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + ","
-                + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
-                + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
-                + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
-                + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
-                + RawContactsColumns.CONCRETE_NAME_VERIFIED + " AS " + RawContacts.NAME_VERIFIED + ","
-                + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + ","
+        String rawEntitiesSelect = "SELECT "
                 + RawContacts.CONTACT_ID + ", "
-                + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ", "
-                + RawContactsColumns.CONCRETE_SYNC2 + " AS " + RawContacts.SYNC2 + ", "
-                + RawContactsColumns.CONCRETE_SYNC3 + " AS " + RawContacts.SYNC3 + ", "
-                + RawContactsColumns.CONCRETE_SYNC4 + " AS " + RawContacts.SYNC4 + ", "
-                + 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 + ", "
+                + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
+                + dataColumns + ", "
+                + syncColumns + ", "
                 + Data.SYNC1 + ", "
                 + Data.SYNC2 + ", "
                 + Data.SYNC3 + ", "
                 + Data.SYNC4 + ", "
                 + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + ", "
-                + Data.IS_PRIMARY + ", "
-                + Data.IS_SUPER_PRIMARY + ", "
-                + Data.DATA_VERSION + ", "
                 + DataColumns.CONCRETE_ID + " AS " + RawContacts.Entity.DATA_ID + ","
                 + RawContactsColumns.CONCRETE_STARRED + " AS " + RawContacts.STARRED + ","
                 + RawContactsColumns.CONCRETE_IS_RESTRICTED + " AS "
@@ -1357,10 +1327,92 @@
                 +   "' AND " + GroupsColumns.CONCRETE_ID + "="
                 + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")";
 
-        db.execSQL("CREATE VIEW " + Tables.CONTACT_ENTITIES + " AS "
-                + contactEntitiesSelect);
-        db.execSQL("CREATE VIEW " + Tables.CONTACT_ENTITIES_RESTRICTED + " AS "
-                + contactEntitiesSelect + " WHERE " + RawContacts.IS_RESTRICTED + "=0");
+        db.execSQL("CREATE VIEW " + Views.RAW_ENTITIES + " AS "
+                + rawEntitiesSelect);
+        db.execSQL("CREATE VIEW " + Views.RAW_ENTITIES_RESTRICTED + " AS "
+                + rawEntitiesSelect + " WHERE " + RawContacts.IS_RESTRICTED + "=0");
+
+        String entitiesSelect = "SELECT "
+                + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + Contacts._ID + ", "
+                + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
+                + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
+                + RawContactsColumns.CONCRETE_IS_RESTRICTED
+                        + " AS " + RawContacts.IS_RESTRICTED + ","
+                + dataColumns + ", "
+                + syncColumns + ", "
+                + contactsColumns + ", "
+                + buildPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
+                        Contacts.PHOTO_URI) + ", "
+                + buildPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
+                        Contacts.PHOTO_THUMBNAIL_URI) + ", "
+                + Data.SYNC1 + ", "
+                + Data.SYNC2 + ", "
+                + Data.SYNC3 + ", "
+                + Data.SYNC4 + ", "
+                + RawContactsColumns.CONCRETE_ID + " AS " + Contacts.Entity.RAW_CONTACT_ID + ", "
+                + DataColumns.CONCRETE_ID + " AS " + Contacts.Entity.DATA_ID + ","
+                + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
+                + " FROM " + Tables.RAW_CONTACTS
+                + " JOIN " + Tables.CONTACTS + " ON ("
+                +   RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
+                + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
+                +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"
+                + " LEFT OUTER JOIN " + Tables.DATA + " ON ("
+                +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
+                + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
+                +   DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
+                + " LEFT OUTER JOIN " + Tables.MIMETYPES + " ON ("
+                +   DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
+                + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
+                +   MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
+                +   "' AND " + GroupsColumns.CONCRETE_ID + "="
+                + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")";
+
+        db.execSQL("CREATE VIEW " + Views.ENTITIES + " AS "
+                + entitiesSelect);
+        db.execSQL("CREATE VIEW " + Views.ENTITIES_RESTRICTED + " AS "
+                + entitiesSelect + " WHERE " + RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0");
+    }
+
+    private static String buildPhotoUriAlias(String contactIdColumn, String alias) {
+        return "(CASE WHEN " + Contacts.PHOTO_ID + " IS NULL"
+                + " OR " + Contacts.PHOTO_ID + "=0"
+                + " THEN NULL"
+                + " ELSE " + "'" + Contacts.CONTENT_URI + "/'||"
+                        + contactIdColumn + "|| '/" + Photo.CONTENT_DIRECTORY + "'"
+                + " END)"
+                + " AS " + alias;
+    }
+
+    private static void createGroupsView(SQLiteDatabase db) {
+        db.execSQL("DROP VIEW IF EXISTS " + Views.GROUPS_ALL + ";");
+        String groupsColumns =
+                Groups.ACCOUNT_NAME + ","
+                + Groups.ACCOUNT_TYPE + ","
+                + Groups.SOURCE_ID + ","
+                + Groups.VERSION + ","
+                + Groups.DIRTY + ","
+                + Groups.TITLE + ","
+                + Groups.TITLE_RES + ","
+                + Groups.NOTES + ","
+                + Groups.SYSTEM_ID + ","
+                + Groups.DELETED + ","
+                + Groups.GROUP_VISIBLE + ","
+                + Groups.SHOULD_SYNC + ","
+                + Groups.AUTO_ADD + ","
+                + Groups.FAVORITES + ","
+                + Groups.SYNC1 + ","
+                + Groups.SYNC2 + ","
+                + Groups.SYNC3 + ","
+                + Groups.SYNC4 + ","
+                + PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE;
+
+        String groupsSelect = "SELECT "
+                + GroupsColumns.CONCRETE_ID + " AS " + Groups._ID + ","
+                + groupsColumns
+                + " FROM " + Tables.GROUPS_JOIN_PACKAGES;
+
+        db.execSQL("CREATE VIEW " + Views.GROUPS_ALL + " AS " + groupsSelect);
     }
 
     @Override
@@ -1526,10 +1578,92 @@
             oldVersion = 353;
         }
 
+        // Honeycomb upgrades
+        if (oldVersion < 400) {
+            upgradeViewsAndTriggers = true;
+            upgradeToVersion400(db);
+            oldVersion = 400;
+        }
+
+        if (oldVersion == 400) {
+            upgradeViewsAndTriggers = true;
+            upgradeToVersion401(db);
+            oldVersion = 401;
+        }
+
+        if (oldVersion == 401) {
+            upgradeToVersion402(db);
+            oldVersion = 402;
+        }
+
+        if (oldVersion == 402) {
+            upgradeViewsAndTriggers = true;
+            upgradeToVersion403(db);
+            oldVersion = 403;
+        }
+
+        if (oldVersion == 403) {
+            upgradeViewsAndTriggers = true;
+            oldVersion = 404;
+        }
+
+        if (oldVersion == 404) {
+            upgradeViewsAndTriggers = true;
+            upgradeToVersion405(db);
+            oldVersion = 405;
+        }
+
+        if (oldVersion == 405) {
+            upgradeViewsAndTriggers = true;
+            upgradeToVersion406(db);
+            oldVersion = 406;
+        }
+
+        if (oldVersion == 406) {
+            upgradeViewsAndTriggers = true;
+            oldVersion = 407;
+        }
+
+        if (oldVersion == 407) {
+            // Obsolete
+            oldVersion = 408;
+        }
+
+        if (oldVersion == 408) {
+            upgradeViewsAndTriggers = true;
+            upgradeToVersion409(db);
+            oldVersion = 409;
+        }
+
+        if (oldVersion == 409) {
+            upgradeViewsAndTriggers = true;
+            oldVersion = 410;
+        }
+
+        if (oldVersion == 410) {
+            upgradeToVersion411(db);
+            oldVersion = 411;
+        }
+
+        if (oldVersion == 411) {
+            // Same upgrade as 353, only on Honeycomb devices
+            upgradeToVersion353(db);
+            oldVersion = 412;
+        }
+
+        if (oldVersion == 412) {
+            upgradeToVersion413(db);
+            oldVersion = 413;
+        }
+
+        if (oldVersion == 413) {
+            upgradeNameLookup = true;
+            oldVersion = 414;
+        }
+
         if (upgradeViewsAndTriggers) {
             createContactsViews(db);
             createGroupsView(db);
-            createContactEntitiesView(db);
             createContactsTriggers(db);
             createContactsIndexes(db);
             LegacyApiSupport.createViews(db);
@@ -1600,8 +1734,7 @@
                 " ADD " + Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)");
         db.execSQL(
                 "ALTER TABLE " + Tables.RAW_CONTACTS +
-                " ADD " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
-                        + " INTEGER NOT NULL DEFAULT 0");
+                " ADD contact_in_visible_group INTEGER NOT NULL DEFAULT 0");
 
         // For each Contact, find the RawContact that contributed the display name
         db.execSQL(
@@ -1643,7 +1776,7 @@
         // indexing on (display_name, in_visible_group)
         db.execSQL(
                 "UPDATE " + Tables.RAW_CONTACTS +
-                " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" +
+                " SET contact_in_visible_group=(" +
                         "SELECT " + Contacts.IN_VISIBLE_GROUP +
                         " FROM " + Tables.CONTACTS +
                         " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")" +
@@ -1651,7 +1784,7 @@
         );
 
         db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
-                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+                "contact_in_visible_group" + "," +
                 RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" +
         ");");
 
@@ -1695,12 +1828,12 @@
 
         db.execSQL("DROP INDEX raw_contact_sort_key1_index");
         db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
-                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+                "contact_in_visible_group" + "," +
                 RawContacts.SORT_KEY_PRIMARY +
         ");");
 
         db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
-                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+                "contact_in_visible_group" + "," +
                 RawContacts.SORT_KEY_ALTERNATIVE +
         ");");
     }
@@ -2141,13 +2274,20 @@
     }
 
     private void upgradeToVersion308(SQLiteDatabase db) {
-            db.execSQL("CREATE TABLE accounts (" +
-                    "account_name TEXT, " +
-                    "account_type TEXT " +
-            ");");
+        db.execSQL("CREATE TABLE accounts (" +
+                "account_name TEXT, " +
+                "account_type TEXT " +
+        ");");
 
-            db.execSQL("INSERT INTO accounts " +
-                    "SELECT DISTINCT account_name, account_type FROM raw_contacts");
+        db.execSQL("INSERT INTO accounts " +
+                "SELECT DISTINCT account_name, account_type FROM raw_contacts");
+    }
+
+    private void upgradeToVersion400(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE " + Tables.GROUPS
+                + " ADD " + Groups.FAVORITES + " INTEGER NOT NULL DEFAULT 0;");
+        db.execSQL("ALTER TABLE " + Tables.GROUPS
+                + " ADD " + Groups.AUTO_ADD + " INTEGER NOT NULL DEFAULT 0;");
     }
 
     private void upgradeToVersion353(SQLiteDatabase db) {
@@ -2440,6 +2580,169 @@
         stmt.executeInsert();
     }
 
+    /**
+     * Changing the VISIBLE bit from a field on both RawContacts and Contacts to a separate table.
+     */
+    private void upgradeToVersion401(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" +
+                Contacts._ID + " INTEGER PRIMARY KEY" +
+        ");");
+        db.execSQL("INSERT INTO " + Tables.VISIBLE_CONTACTS +
+                " SELECT " + Contacts._ID +
+                " FROM " + Tables.CONTACTS +
+                " WHERE " + Contacts.IN_VISIBLE_GROUP + "!=0");
+        db.execSQL("DROP INDEX contacts_visible_index");
+    }
+
+    /**
+     * Introducing a new table: directories.
+     */
+    private void upgradeToVersion402(SQLiteDatabase db) {
+        createDirectoriesTable(db);
+    }
+
+    private void upgradeToVersion403(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS directories;");
+        createDirectoriesTable(db);
+
+        db.execSQL("ALTER TABLE raw_contacts"
+                + " ADD raw_contact_is_read_only INTEGER NOT NULL DEFAULT 0;");
+
+        db.execSQL("ALTER TABLE data"
+                + " ADD is_read_only INTEGER NOT NULL DEFAULT 0;");
+    }
+
+    private void upgradeToVersion405(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS phone_lookup;");
+        // Private phone numbers table used for lookup
+        db.execSQL("CREATE TABLE " + Tables.PHONE_LOOKUP + " (" +
+                PhoneLookupColumns.DATA_ID
+                + " INTEGER REFERENCES data(_id) NOT NULL," +
+                PhoneLookupColumns.RAW_CONTACT_ID
+                + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
+                PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," +
+                PhoneLookupColumns.MIN_MATCH + " TEXT NOT NULL" +
+        ");");
+
+        db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" +
+                PhoneLookupColumns.NORMALIZED_NUMBER + "," +
+                PhoneLookupColumns.RAW_CONTACT_ID + "," +
+                PhoneLookupColumns.DATA_ID +
+        ");");
+
+        db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
+                PhoneLookupColumns.MIN_MATCH + "," +
+                PhoneLookupColumns.RAW_CONTACT_ID + "," +
+                PhoneLookupColumns.DATA_ID +
+        ");");
+
+        final long mimeTypeId = lookupMimeTypeId(db, Phone.CONTENT_ITEM_TYPE);
+        if (mimeTypeId == -1) {
+            return;
+        }
+
+        String mCountryIso = getCountryIso();
+        Cursor cursor = db.rawQuery(
+                    "SELECT _id, " + Phone.RAW_CONTACT_ID + ", " + Phone.NUMBER +
+                    " FROM " + Tables.DATA +
+                    " WHERE " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId
+                            + " AND " + Phone.NUMBER + " NOT NULL", null);
+
+        ContentValues phoneValues = new ContentValues();
+        try {
+            while (cursor.moveToNext()) {
+                long dataID = cursor.getLong(0);
+                long rawContactID = cursor.getLong(1);
+                String number = cursor.getString(2);
+                String numberE164 = PhoneNumberUtils.formatNumberToE164(number, mCountryIso);
+                String normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
+                if (!TextUtils.isEmpty(normalizedNumber)) {
+                    phoneValues.clear();
+                    phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactID);
+                    phoneValues.put(PhoneLookupColumns.DATA_ID, dataID);
+                    phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
+                    phoneValues.put(PhoneLookupColumns.MIN_MATCH,
+                            PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber));
+                    db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
+
+                    if (numberE164 != null && !numberE164.equals(normalizedNumber)) {
+                        phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, numberE164);
+                        phoneValues.put(PhoneLookupColumns.MIN_MATCH,
+                                PhoneNumberUtils.toCallerIDMinMatch(numberE164));
+                        db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
+                    }
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void upgradeToVersion406(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE calls ADD countryiso TEXT;");
+    }
+
+    private void upgradeToVersion409(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS directories;");
+        createDirectoriesTable(db);
+    }
+
+    /**
+     * Adding DEFAULT_DIRECTORY table.
+     */
+    private void upgradeToVersion411(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS " + Tables.DEFAULT_DIRECTORY);
+        db.execSQL("CREATE TABLE " + Tables.DEFAULT_DIRECTORY + " (" +
+                Contacts._ID + " INTEGER PRIMARY KEY" +
+        ");");
+
+        // Process contacts without an account
+        db.execSQL("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 ");
+
+        // Process accounts that don't have a default group (e.g. Exchange)
+        db.execSQL("INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
+                " SELECT " + RawContacts.CONTACT_ID +
+                " FROM " + Tables.RAW_CONTACTS +
+                " 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 +
+                "    AND " + Groups.AUTO_ADD + " != 0" +
+                ")");
+
+        long mimetype = lookupMimeTypeId(db, GroupMembership.CONTENT_ITEM_TYPE);
+
+        // Process accounts that do have a default group (e.g. Google)
+        db.execSQL("INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
+                " SELECT " + RawContacts.CONTACT_ID +
+                " FROM " + Tables.RAW_CONTACTS +
+                " JOIN " + Tables.DATA +
+                "   ON (" + RawContactsColumns.CONCRETE_ID + "=" + Data.RAW_CONTACT_ID + ")" +
+                " WHERE " + DataColumns.MIMETYPE_ID + "=" + mimetype +
+                " 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 +
+                "    AND " + Groups.AUTO_ADD + " != 0" +
+                ")");
+    }
+
+    private void upgradeToVersion413(SQLiteDatabase db) {
+        db.execSQL(
+                "ALTER TABLE " + Tables.DIRECTORIES +
+                " ADD " + DirectoryColumns.TYPE_RESOURCE_NAME + " TEXT;");
+    }
+
     public String extractHandleFromEmailAddress(String email) {
         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
         if (tokens.length == 0) {
@@ -2495,8 +2798,6 @@
                     "contacts_restricted_index", "10000 9000");
             updateIndexStats(db, Tables.CONTACTS,
                     "contacts_has_phone_index", "10000 500");
-            updateIndexStats(db, Tables.CONTACTS,
-                    "contacts_visible_index", "10000 500 1");
 
             updateIndexStats(db, Tables.RAW_CONTACTS,
                     "raw_contacts_source_id_index", "10000 1 1 1");
@@ -2575,6 +2876,7 @@
         db.execSQL("DELETE FROM " + Tables.SETTINGS + ";");
         db.execSQL("DELETE FROM " + Tables.ACTIVITIES + ";");
         db.execSQL("DELETE FROM " + Tables.CALLS + ";");
+        db.execSQL("DELETE FROM " + Tables.DIRECTORIES + ";");
 
         // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
     }
@@ -2704,67 +3006,90 @@
      * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts.
      */
     public void updateAllVisible() {
-        SQLiteDatabase db = getWritableDatabase();
-        final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
-        String[] selectionArgs = new String[]{String.valueOf(groupMembershipMimetypeId)};
-
-        // There are a couple questions that can be asked regarding the
-        // following two update statements:
-        //
-        // Q: Why do we run these two queries separately? They seem like they could be combined.
-        // A: This is a result of painstaking experimentation.  Turns out that the most
-        // important optimization is to make sure we never update a value to its current value.
-        // Changing 0 to 0 is unexpectedly expensive - SQLite actually writes the unchanged
-        // rows back to disk.  The other consideration is that the CONTACT_IS_VISIBLE condition
-        // is very complex and executing it twice in the same statement ("if contact_visible !=
-        // CONTACT_IS_VISIBLE change it to CONTACT_IS_VISIBLE") is more expensive than running
-        // two update statements.
-        //
-        // Q: How come we are using db.update instead of compiled statements?
-        // A: This is a limitation of the compiled statement API. It does not return the
-        // number of rows changed.  As you will see later in this method we really need
-        // to know how many rows have been changed.
-
-        // First update contacts that are currently marked as invisible, but need to be visible
-        ContentValues values = new ContentValues();
-        values.put(Contacts.IN_VISIBLE_GROUP, 1);
-        int countMadeVisible = db.update(Tables.CONTACTS, values,
-                Contacts.IN_VISIBLE_GROUP + "=0" + " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1",
-                selectionArgs);
-
-        // Next update contacts that are currently marked as visible, but need to be invisible
-        values.put(Contacts.IN_VISIBLE_GROUP, 0);
-        int countMadeInvisible = db.update(Tables.CONTACTS, values,
-                Contacts.IN_VISIBLE_GROUP + "=1" + " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=0",
-                selectionArgs);
-
-        if (countMadeVisible != 0 || countMadeInvisible != 0) {
-            // TODO break out the fields (contact_in_visible_group, sort_key, sort_key_alt) into
-            // a separate table.
-            // Rationale: The following statement will take a very long time on
-            // a large database even though we are only changing one field from 0 to 1 or from
-            // 1 to 0.  The reason for the slowness is that SQLite will need to write the whole
-            // page even when only one bit on it changes. Changing the visibility of a
-            // significant number of contacts will likely read and write almost the entire
-            // raw_contacts table.  So, the solution is to break out into a separate table
-            // the changing field along with the sort keys used for index-based sorting.
-            // That table will occupy a smaller number of pages, so rewriting it would
-            // not be as expensive.
-            mVisibleUpdateRawContacts.execute();
-        }
+        updateCustomContactVisibility(getWritableDatabase(), "");
     }
 
     /**
-     * Update {@link Contacts#IN_VISIBLE_GROUP} for a specific contact.
+     * Update {@link Contacts#IN_VISIBLE_GROUP} and
+     * {@link Tables#DEFAULT_DIRECTORY} for a specific contact.
      */
     public void updateContactVisible(long contactId) {
-        final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
-        mVisibleSpecificUpdate.bindLong(1, groupMembershipMimetypeId);
-        mVisibleSpecificUpdate.bindLong(2, contactId);
-        mVisibleSpecificUpdate.execute();
+        SQLiteDatabase db = getWritableDatabase();
+        updateCustomContactVisibility(getWritableDatabase(),
+                " AND " + Contacts._ID + "=" + contactId);
 
-        mVisibleSpecificUpdateRawContacts.bindLong(1, contactId);
-        mVisibleSpecificUpdateRawContacts.execute();
+        String contactIdAsString = String.valueOf(contactId);
+        long mimetype = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
+
+        // The contact will be included in the default directory if contains
+        // a raw contact that is in any group or in an account that
+        // does not have any AUTO_ADD groups.
+        long visibleRawContact = DatabaseUtils.longForQuery(db,
+                "SELECT EXISTS (" +
+                    "SELECT " + RawContacts.CONTACT_ID +
+                    " FROM " + Tables.RAW_CONTACTS +
+                    " JOIN " + Tables.DATA +
+                    "   ON (" + RawContactsColumns.CONCRETE_ID + "="
+                            + Data.RAW_CONTACT_ID + ")" +
+                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
+                    "   AND " + DataColumns.MIMETYPE_ID + "=?" +
+                ") OR EXISTS (" +
+                    "SELECT " + RawContacts._ID +
+                    " FROM " + Tables.RAW_CONTACTS +
+                    " WHERE " + RawContacts.CONTACT_ID + "=?" +
+                    "   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 " + 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" +
+                ")",
+                new String[] {
+                    contactIdAsString,
+                    String.valueOf(mimetype),
+                    contactIdAsString,
+                    contactIdAsString
+                });
+
+        if (visibleRawContact != 0) {
+            db.execSQL("INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY + " VALUES(?)",
+                    new String[] { contactIdAsString });
+        } else {
+            db.execSQL("DELETE FROM " + Tables.DEFAULT_DIRECTORY + " WHERE " + Contacts._ID + "=?",
+                    new String[] { contactIdAsString });
+        }
+    }
+
+    private void updateCustomContactVisibility(SQLiteDatabase db, String selection) {
+        final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
+        String[] selectionArgs = new String[]{String.valueOf(groupMembershipMimetypeId)};
+
+        // 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,
+                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,
+                selectionArgs);
     }
 
     /**
@@ -2792,30 +3117,15 @@
         }
     }
 
-    public void buildPhoneLookupAndRawContactQuery(SQLiteQueryBuilder qb, String number) {
-        String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
-        qb.setTables(Tables.DATA_JOIN_RAW_CONTACTS +
-                " JOIN " + Tables.PHONE_LOOKUP
-                + " ON(" + DataColumns.CONCRETE_ID + "=" + PhoneLookupColumns.DATA_ID + ")");
-
-        StringBuilder sb = new StringBuilder();
-        sb.append(PhoneLookupColumns.MIN_MATCH + "='");
-        sb.append(minMatch);
-        sb.append("' AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
-        DatabaseUtils.appendEscapedSQLString(sb, number);
-        sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
-
-        qb.appendWhere(sb.toString());
-    }
-
-    public void buildPhoneLookupAndContactQuery(SQLiteQueryBuilder qb, String number) {
-        String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
+    public void buildPhoneLookupAndContactQuery(
+            SQLiteQueryBuilder qb, String normalizedNumber, String numberE164) {
+        String minMatch = PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber);
         StringBuilder sb = new StringBuilder();
         appendPhoneLookupTables(sb, minMatch, true);
         qb.setTables(sb.toString());
 
         sb = new StringBuilder();
-        appendPhoneLookupSelection(sb, number);
+        appendPhoneLookupSelection(sb, normalizedNumber, numberE164);
         qb.appendWhere(sb.toString());
     }
 
@@ -2825,7 +3135,7 @@
         sb.append("(SELECT DISTINCT raw_contact_id" + " FROM ");
         appendPhoneLookupTables(sb, minMatch, false);
         sb.append(" WHERE ");
-        appendPhoneLookupSelection(sb, number);
+        appendPhoneLookupSelection(sb, number, null);
         sb.append(")");
         return sb.toString();
     }
@@ -2837,17 +3147,38 @@
             sb.append(" JOIN " + getContactView() + " contacts_view"
                     + " ON (contacts_view._id = raw_contacts.contact_id)");
         }
-        sb.append(", (SELECT data_id FROM phone_lookup "
-                + "WHERE (" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.MIN_MATCH + " = '");
+        sb.append(", (SELECT data_id, normalized_number, length(normalized_number) as len "
+                + " FROM phone_lookup " + " WHERE (" + Tables.PHONE_LOOKUP + "."
+                + PhoneLookupColumns.MIN_MATCH + " = '");
         sb.append(minMatch);
         sb.append("')) AS lookup, " + Tables.DATA);
     }
 
-    private void appendPhoneLookupSelection(StringBuilder sb, String number) {
-        sb.append("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id"
-                + " AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
-        DatabaseUtils.appendEscapedSQLString(sb, number);
-        sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
+    private void appendPhoneLookupSelection(StringBuilder sb, String number, String numberE164) {
+        sb.append("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id");
+        boolean hasNumberE164 = !TextUtils.isEmpty(numberE164);
+        boolean hasNumber = !TextUtils.isEmpty(number);
+        if (hasNumberE164 || hasNumber) {
+            sb.append(" AND ( ");
+            if (hasNumberE164) {
+                sb.append(" lookup.normalized_number = ");
+                DatabaseUtils.appendEscapedSQLString(sb, numberE164);
+            }
+            if (hasNumberE164 && hasNumber) {
+                sb.append(" OR ");
+            }
+            if (hasNumber) {
+                int numberLen = number.length();
+                sb.append(" lookup.len <= ");
+                sb.append(numberLen);
+                sb.append(" AND substr(");
+                DatabaseUtils.appendEscapedSQLString(sb, number);
+                sb.append(',');
+                sb.append(numberLen);
+                sb.append(" - lookup.len + 1) = lookup.normalized_number");
+            }
+            sb.append(')');
+        }
     }
 
     public String getUseStrictPhoneNumberComparisonParameter() {
@@ -2970,10 +3301,14 @@
      * Stores a key-value pair in the {@link Tables#PROPERTIES} table.
      */
     public void setProperty(String key, String value) {
+        setProperty(getWritableDatabase(), key, value);
+    }
+
+    private void setProperty(SQLiteDatabase db, String key, String value) {
         ContentValues values = new ContentValues();
         values.put(PropertiesColumns.PROPERTY_KEY, key);
         values.put(PropertiesColumns.PROPERTY_VALUE, value);
-        getWritableDatabase().replace(Tables.PROPERTIES, null, values);
+        db.replace(Tables.PROPERTIES, null, values);
     }
 
     /**
@@ -2982,7 +3317,9 @@
      */
     boolean hasAccessToRestrictedData() {
         final PackageManager pm = mContext.getPackageManager();
-        final String[] callerPackages = pm.getPackagesForUid(Binder.getCallingUid());
+        int caller = Binder.getCallingUid();
+        if (caller == 0) return true; // root can do anything
+        final String[] callerPackages = pm.getPackagesForUid(caller);
 
         // Has restricted access if caller matches any packages
         for (String callerPackage : callerPackages) {
@@ -3039,13 +3376,22 @@
         return Views.GROUPS_ALL;
     }
 
-    public String getContactEntitiesView() {
-        return getContactEntitiesView(false);
+    public String getRawEntitiesView() {
+        return getRawEntitiesView(false);
     }
 
-    public String getContactEntitiesView(boolean requireRestrictedView) {
+    public String getRawEntitiesView(boolean requireRestrictedView) {
         return (hasAccessToRestrictedData() && !requireRestrictedView) ?
-                Tables.CONTACT_ENTITIES : Tables.CONTACT_ENTITIES_RESTRICTED;
+                Views.RAW_ENTITIES : Views.RAW_ENTITIES_RESTRICTED;
+    }
+
+    public String getEntitiesView() {
+        return getEntitiesView(false);
+    }
+
+    public String getEntitiesView(boolean requireRestrictedView) {
+        return (hasAccessToRestrictedData() && !requireRestrictedView) ?
+                Views.ENTITIES : Views.ENTITIES_RESTRICTED;
     }
 
     /**
@@ -3123,4 +3469,10 @@
 
         return sb.toString();
     }
+
+    protected String getCountryIso() {
+        CountryDetector detector =
+            (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR);
+        return detector.detectCountry().getCountryIso();
+    }
 }
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 61a67d4..2120676 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -16,7 +16,8 @@
 
 package com.android.providers.contacts;
 
-import com.android.internal.content.SyncStateContentProviderHelper;
+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.AggregatedPresenceColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
@@ -35,6 +36,8 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.vcard.VCardComposer;
+import com.android.vcard.VCardConfig;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 import com.google.android.collect.Sets;
@@ -58,8 +61,6 @@
 import android.content.SharedPreferences;
 import android.content.SyncAdapterType;
 import android.content.UriMatcher;
-import android.content.res.AssetFileDescriptor;
-import android.content.res.Configuration;
 import android.database.CharArrayBuffer;
 import android.database.Cursor;
 import android.database.CursorWrapper;
@@ -67,29 +68,36 @@
 import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
 import android.database.sqlite.SQLiteConstraintException;
-import android.database.sqlite.SQLiteContentHelper;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.database.sqlite.SQLiteStatement;
 import android.net.Uri;
+import android.net.Uri.Builder;
 import android.os.AsyncTask;
 import android.os.Bundle;
-import android.os.MemoryFile;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
-import android.pim.vcard.VCardComposer;
-import android.pim.vcard.VCardConfig;
 import android.preference.PreferenceManager;
 import android.provider.BaseColumns;
 import android.provider.ContactsContract;
-import android.provider.LiveFolders;
-import android.provider.OpenableColumns;
-import android.provider.SyncStateContract;
 import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.ContactCounts;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
 import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
 import android.provider.ContactsContract.DisplayNameSources;
 import android.provider.ContactsContract.FullNameStyle;
 import android.provider.ContactsContract.Groups;
@@ -101,16 +109,9 @@
 import android.provider.ContactsContract.SearchSnippetColumns;
 import android.provider.ContactsContract.Settings;
 import android.provider.ContactsContract.StatusUpdates;
-import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.LiveFolders;
+import android.provider.OpenableColumns;
+import android.provider.SyncStateContract;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 import android.util.Log;
@@ -147,7 +148,6 @@
     /** Default for the maximum number of returned aggregation suggestions. */
     private static final int DEFAULT_MAX_SUGGESTIONS = 5;
 
-    private static final String GOOGLE_MY_CONTACTS_GROUP_TITLE = "System Group: My Contacts";
     /**
      * 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,
@@ -164,10 +164,10 @@
 
     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
 
-    private static final String TIMES_CONTACED_SORT_COLUMN = "times_contacted_sort";
+    private static final String TIMES_CONTACTED_SORT_COLUMN = "times_contacted_sort";
 
     private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, "
-            + TIMES_CONTACED_SORT_COLUMN + " DESC, "
+            + TIMES_CONTACTED_SORT_COLUMN + " DESC, "
             + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
     private static final String STREQUENT_LIMIT =
             "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
@@ -189,14 +189,19 @@
     private static final int CONTACTS_ID = 1001;
     private static final int CONTACTS_LOOKUP = 1002;
     private static final int CONTACTS_LOOKUP_ID = 1003;
-    private static final int CONTACTS_DATA = 1004;
+    private static final int CONTACTS_ID_DATA = 1004;
     private static final int CONTACTS_FILTER = 1005;
     private static final int CONTACTS_STREQUENT = 1006;
     private static final int CONTACTS_STREQUENT_FILTER = 1007;
     private static final int CONTACTS_GROUP = 1008;
-    private static final int CONTACTS_PHOTO = 1009;
+    private static final int CONTACTS_ID_PHOTO = 1009;
     private static final int CONTACTS_AS_VCARD = 1010;
     private static final int CONTACTS_AS_MULTI_VCARD = 1011;
+    private static final int CONTACTS_LOOKUP_DATA = 1012;
+    private static final int CONTACTS_LOOKUP_ID_DATA = 1013;
+    private static final int CONTACTS_ID_ENTITIES = 1014;
+    private static final int CONTACTS_LOOKUP_ENTITIES = 1015;
+    private static final int CONTACTS_LOOKUP_ID_ENTITIES = 1016;
 
     private static final int RAW_CONTACTS = 2002;
     private static final int RAW_CONTACTS_ID = 2003;
@@ -246,6 +251,38 @@
 
     private static final int PROVIDER_STATUS = 16001;
 
+    private static final int DIRECTORIES = 17001;
+    private static final int DIRECTORIES_ID = 17002;
+
+    private static final int COMPLETE_NAME = 18000;
+
+    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 " + 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 "
+                    + Groups.AUTO_ADD + " != 0";
+
+    private static final String[] PROJECTION_GROUP_ID
+            = new String[]{Tables.GROUPS + "." + Groups._ID};
+
+    private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? "
+            + "AND " + GroupMembership.GROUP_ROW_ID + "=? "
+            + "AND " + GroupMembership.RAW_CONTACT_ID + "=?";
+
+    private static final String SELECTION_STARRED_FROM_RAW_CONTACTS =
+            "SELECT " + RawContacts.STARRED
+                    + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?";
+
     private interface DataContactsQuery {
         public static final String TABLE = "data "
                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
@@ -340,6 +377,32 @@
             " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
             " WHERE " + RawContacts._ID + " IN (";
 
+    // Current contacts - those contacted within the last 3 days (in seconds)
+    private static final long EMAIL_FILTER_CURRENT = 3 * 24 * 60 * 60;
+
+    // Recent contacts - those contacted within the last 30 days (in seconds)
+    private static final long EMAIL_FILTER_RECENT = 30 * 24 * 60 * 60;
+
+    private static final String TIME_SINCE_LAST_CONTACTED =
+            "(strftime('%s', 'now') - " + Contacts.LAST_TIME_CONTACTED + "/1000)";
+
+    /*
+     * Sorting order for email address suggestions: first starred, then the rest.
+     * Within the starred/unstarred groups - three buckets: very recently contacted, then fairly
+     * recently contacted, then the rest.  Within each of the bucket - descending count
+     * of times contacted. If all else fails, alphabetical.  (Super)primary email
+     * address is returned before other addresses for the same contact.
+     */
+    private static final String EMAIL_FILTER_SORT_ORDER =
+            "(CASE WHEN " + Contacts.STARRED + "=1 THEN 0 ELSE 1 END), "
+            + "(CASE WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + EMAIL_FILTER_CURRENT + " THEN 0 "
+            + " WHEN " + TIME_SINCE_LAST_CONTACTED + " < " + EMAIL_FILTER_RECENT + " THEN 1 "
+            + " ELSE 2 END),"
+            + Contacts.TIMES_CONTACTED + " DESC, "
+            + Contacts.DISPLAY_NAME + ", "
+            + Data.CONTACT_ID + ", "
+            + Data.IS_SUPER_PRIMARY + " DESC";
+
     /** Name lookup types used for contact filtering */
     private static final String CONTACT_LOOKUP_NAME_TYPES =
             NameLookupType.NAME_COLLATION_KEY + "," +
@@ -349,41 +412,405 @@
             NameLookupType.ORGANIZATION + "," +
             NameLookupType.NAME_CONSONANTS;
 
+    /**
+     * If any of these columns are used in a Data projection, there is no point in
+     * using the DISTINCT keyword, which can negatively affect performance.
+     */
+    private static final String[] DISTINCT_DATA_PROHIBITING_COLUMNS = {
+            Data._ID,
+            Data.RAW_CONTACT_ID,
+            Data.NAME_RAW_CONTACT_ID,
+            RawContacts.ACCOUNT_NAME,
+            RawContacts.ACCOUNT_TYPE,
+            RawContacts.DIRTY,
+            RawContacts.NAME_VERIFIED,
+            RawContacts.SOURCE_ID,
+            RawContacts.VERSION,
+    };
+
+    private static final ProjectionMap sContactsColumns = ProjectionMap.builder()
+            .add(Contacts.CUSTOM_RINGTONE)
+            .add(Contacts.DISPLAY_NAME)
+            .add(Contacts.DISPLAY_NAME_ALTERNATIVE)
+            .add(Contacts.DISPLAY_NAME_SOURCE)
+            .add(Contacts.IN_VISIBLE_GROUP)
+            .add(Contacts.LAST_TIME_CONTACTED)
+            .add(Contacts.LOOKUP_KEY)
+            .add(Contacts.PHONETIC_NAME)
+            .add(Contacts.PHONETIC_NAME_STYLE)
+            .add(Contacts.PHOTO_ID)
+            .add(Contacts.PHOTO_URI)
+            .add(Contacts.PHOTO_THUMBNAIL_URI)
+            .add(Contacts.SEND_TO_VOICEMAIL)
+            .add(Contacts.SORT_KEY_ALTERNATIVE)
+            .add(Contacts.SORT_KEY_PRIMARY)
+            .add(Contacts.STARRED)
+            .add(Contacts.TIMES_CONTACTED)
+            .add(Contacts.HAS_PHONE_NUMBER)
+            .build();
+
+    private static final ProjectionMap sContactsPresenceColumns = ProjectionMap.builder()
+            .add(Contacts.CONTACT_PRESENCE,
+                    Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)
+            .add(Contacts.CONTACT_CHAT_CAPABILITY,
+                    Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)
+            .add(Contacts.CONTACT_STATUS,
+                    ContactsStatusUpdatesColumns.CONCRETE_STATUS)
+            .add(Contacts.CONTACT_STATUS_TIMESTAMP,
+                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
+            .add(Contacts.CONTACT_STATUS_RES_PACKAGE,
+                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
+            .add(Contacts.CONTACT_STATUS_LABEL,
+                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)
+            .add(Contacts.CONTACT_STATUS_ICON,
+                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)
+            .build();
+
+    private static final ProjectionMap sSnippetColumns = ProjectionMap.builder()
+            .add(SearchSnippetColumns.SNIPPET_MIMETYPE)
+            .add(SearchSnippetColumns.SNIPPET_DATA_ID)
+            .add(SearchSnippetColumns.SNIPPET_DATA1)
+            .add(SearchSnippetColumns.SNIPPET_DATA2)
+            .add(SearchSnippetColumns.SNIPPET_DATA3)
+            .add(SearchSnippetColumns.SNIPPET_DATA4)
+            .build();
+
+
+    private static final ProjectionMap sRawContactColumns = ProjectionMap.builder()
+            .add(RawContacts.ACCOUNT_NAME)
+            .add(RawContacts.ACCOUNT_TYPE)
+            .add(RawContacts.DIRTY)
+            .add(RawContacts.NAME_VERIFIED)
+            .add(RawContacts.SOURCE_ID)
+            .add(RawContacts.VERSION)
+            .build();
+
+    private static final ProjectionMap sRawContactSyncColumns = ProjectionMap.builder()
+            .add(RawContacts.SYNC1)
+            .add(RawContacts.SYNC2)
+            .add(RawContacts.SYNC3)
+            .add(RawContacts.SYNC4)
+            .build();
+
+    private static final ProjectionMap sDataColumns = ProjectionMap.builder()
+            .add(Data.DATA1)
+            .add(Data.DATA2)
+            .add(Data.DATA3)
+            .add(Data.DATA4)
+            .add(Data.DATA5)
+            .add(Data.DATA6)
+            .add(Data.DATA7)
+            .add(Data.DATA8)
+            .add(Data.DATA9)
+            .add(Data.DATA10)
+            .add(Data.DATA11)
+            .add(Data.DATA12)
+            .add(Data.DATA13)
+            .add(Data.DATA14)
+            .add(Data.DATA15)
+            .add(Data.DATA_VERSION)
+            .add(Data.IS_PRIMARY)
+            .add(Data.IS_SUPER_PRIMARY)
+            .add(Data.MIMETYPE)
+            .add(Data.RES_PACKAGE)
+            .add(Data.SYNC1)
+            .add(Data.SYNC2)
+            .add(Data.SYNC3)
+            .add(Data.SYNC4)
+            .add(GroupMembership.GROUP_SOURCE_ID)
+            .build();
+
+    private static final ProjectionMap sContactPresenceColumns = ProjectionMap.builder()
+            .add(Contacts.CONTACT_PRESENCE,
+                    Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.PRESENCE)
+            .add(Contacts.CONTACT_CHAT_CAPABILITY,
+                    Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.CHAT_CAPABILITY)
+            .add(Contacts.CONTACT_STATUS,
+                    ContactsStatusUpdatesColumns.CONCRETE_STATUS)
+            .add(Contacts.CONTACT_STATUS_TIMESTAMP,
+                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
+            .add(Contacts.CONTACT_STATUS_RES_PACKAGE,
+                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
+            .add(Contacts.CONTACT_STATUS_LABEL,
+                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)
+            .add(Contacts.CONTACT_STATUS_ICON,
+                    ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)
+            .build();
+
+    private static final ProjectionMap sDataPresenceColumns = ProjectionMap.builder()
+            .add(Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE)
+            .add(Data.CHAT_CAPABILITY, Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)
+            .add(Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS)
+            .add(Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
+            .add(Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
+            .add(Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL)
+            .add(Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON)
+            .build();
 
     /** Contains just BaseColumns._COUNT */
-    private static final HashMap<String, String> sCountProjectionMap;
+    private static final ProjectionMap sCountProjectionMap = ProjectionMap.builder()
+            .add(BaseColumns._COUNT, "COUNT(*)")
+            .build();
+
     /** Contains just the contacts columns */
-    private static final HashMap<String, String> sContactsProjectionMap;
+    private static final ProjectionMap sContactsProjectionMap = ProjectionMap.builder()
+            .add(Contacts._ID)
+            .add(Contacts.HAS_PHONE_NUMBER)
+            .add(Contacts.NAME_RAW_CONTACT_ID)
+            .addAll(sContactsColumns)
+            .addAll(sContactsPresenceColumns)
+            .build();
+
     /** Contains just the contacts columns */
-    private static final HashMap<String, String> sContactsProjectionWithSnippetMap;
+    private static final ProjectionMap sContactsProjectionWithSnippetMap = ProjectionMap.builder()
+            .addAll(sContactsProjectionMap)
+            .addAll(sSnippetColumns)
+            .build();
 
     /** Used for pushing starred contacts to the top of a times contacted list **/
-    private static final HashMap<String, String> sStrequentStarredProjectionMap;
-    private static final HashMap<String, String> sStrequentFrequentProjectionMap;
+    private static final ProjectionMap sStrequentStarredProjectionMap = ProjectionMap.builder()
+            .addAll(sContactsProjectionMap)
+            .add(TIMES_CONTACTED_SORT_COLUMN, String.valueOf(Long.MAX_VALUE))
+            .build();
+
+    private static final ProjectionMap sStrequentFrequentProjectionMap = ProjectionMap.builder()
+            .addAll(sContactsProjectionMap)
+            .add(TIMES_CONTACTED_SORT_COLUMN, Contacts.TIMES_CONTACTED)
+            .build();
+
     /** Contains just the contacts vCard columns */
-    private static final HashMap<String, String> sContactsVCardProjectionMap;
+    private static final ProjectionMap sContactsVCardProjectionMap = ProjectionMap.builder()
+            .add(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME + " || '.vcf'")
+            .add(OpenableColumns.SIZE, "NULL")
+            .build();
+
     /** Contains just the raw contacts columns */
-    private static final HashMap<String, String> sRawContactsProjectionMap;
-    /** Contains the columns from the raw contacts entity view*/
-    private static final HashMap<String, String> sRawContactsEntityProjectionMap;
+    private static final ProjectionMap sRawContactsProjectionMap = ProjectionMap.builder()
+            .add(RawContacts._ID)
+            .add(RawContacts.CONTACT_ID)
+            .add(RawContacts.DELETED)
+            .add(RawContacts.DISPLAY_NAME_PRIMARY)
+            .add(RawContacts.DISPLAY_NAME_ALTERNATIVE)
+            .add(RawContacts.DISPLAY_NAME_SOURCE)
+            .add(RawContacts.PHONETIC_NAME)
+            .add(RawContacts.PHONETIC_NAME_STYLE)
+            .add(RawContacts.SORT_KEY_PRIMARY)
+            .add(RawContacts.SORT_KEY_ALTERNATIVE)
+            .add(RawContacts.TIMES_CONTACTED)
+            .add(RawContacts.LAST_TIME_CONTACTED)
+            .add(RawContacts.CUSTOM_RINGTONE)
+            .add(RawContacts.SEND_TO_VOICEMAIL)
+            .add(RawContacts.STARRED)
+            .add(RawContacts.AGGREGATION_MODE)
+            .addAll(sRawContactColumns)
+            .addAll(sRawContactSyncColumns)
+            .build();
+
+    /** Contains the columns from the raw entity view*/
+    private static final ProjectionMap sRawEntityProjectionMap = ProjectionMap.builder()
+            .add(RawContacts._ID)
+            .add(RawContacts.CONTACT_ID)
+            .add(RawContacts.Entity.DATA_ID)
+            .add(RawContacts.IS_RESTRICTED)
+            .add(RawContacts.DELETED)
+            .add(RawContacts.STARRED)
+            .addAll(sRawContactColumns)
+            .addAll(sRawContactSyncColumns)
+            .addAll(sDataColumns)
+            .build();
+
+    /** Contains the columns from the contact entity view*/
+    private static final ProjectionMap sEntityProjectionMap = ProjectionMap.builder()
+            .add(Contacts.Entity._ID)
+            .add(Contacts.Entity.CONTACT_ID)
+            .add(Contacts.Entity.RAW_CONTACT_ID)
+            .add(Contacts.Entity.DATA_ID)
+            .add(Contacts.Entity.NAME_RAW_CONTACT_ID)
+            .add(Contacts.Entity.DELETED)
+            .add(Contacts.Entity.IS_RESTRICTED)
+            .addAll(sContactsColumns)
+            .addAll(sContactPresenceColumns)
+            .addAll(sRawContactColumns)
+            .addAll(sRawContactSyncColumns)
+            .addAll(sDataColumns)
+            .addAll(sDataPresenceColumns)
+            .build();
+
     /** Contains columns from the data view */
-    private static final HashMap<String, String> sDataProjectionMap;
+    private static final ProjectionMap sDataProjectionMap = ProjectionMap.builder()
+            .add(Data._ID)
+            .add(Data.RAW_CONTACT_ID)
+            .add(Data.CONTACT_ID)
+            .add(Data.NAME_RAW_CONTACT_ID)
+            .addAll(sDataColumns)
+            .addAll(sDataPresenceColumns)
+            .addAll(sRawContactColumns)
+            .addAll(sContactsColumns)
+            .addAll(sContactPresenceColumns)
+            .build();
+
     /** Contains columns from the data view */
-    private static final HashMap<String, String> sDistinctDataProjectionMap;
+    private static final ProjectionMap sDistinctDataProjectionMap = ProjectionMap.builder()
+            .add(Data._ID, "MIN(" + Data._ID + ")")
+            .add(RawContacts.CONTACT_ID)
+            .addAll(sDataColumns)
+            .addAll(sDataPresenceColumns)
+            .addAll(sContactsColumns)
+            .addAll(sContactPresenceColumns)
+            .build();
+
     /** Contains the data and contacts columns, for joined tables */
-    private static final HashMap<String, String> sPhoneLookupProjectionMap;
+    private static final ProjectionMap sPhoneLookupProjectionMap = ProjectionMap.builder()
+            .add(PhoneLookup._ID, "contacts_view." + Contacts._ID)
+            .add(PhoneLookup.LOOKUP_KEY, "contacts_view." + Contacts.LOOKUP_KEY)
+            .add(PhoneLookup.DISPLAY_NAME, "contacts_view." + Contacts.DISPLAY_NAME)
+            .add(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED)
+            .add(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED)
+            .add(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED)
+            .add(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP)
+            .add(PhoneLookup.PHOTO_ID, "contacts_view." + Contacts.PHOTO_ID)
+            .add(PhoneLookup.PHOTO_URI, "contacts_view." + Contacts.PHOTO_URI)
+            .add(PhoneLookup.PHOTO_THUMBNAIL_URI, "contacts_view." + Contacts.PHOTO_THUMBNAIL_URI)
+            .add(PhoneLookup.CUSTOM_RINGTONE, "contacts_view." + Contacts.CUSTOM_RINGTONE)
+            .add(PhoneLookup.HAS_PHONE_NUMBER, "contacts_view." + Contacts.HAS_PHONE_NUMBER)
+            .add(PhoneLookup.SEND_TO_VOICEMAIL, "contacts_view." + Contacts.SEND_TO_VOICEMAIL)
+            .add(PhoneLookup.NUMBER, Phone.NUMBER)
+            .add(PhoneLookup.TYPE, Phone.TYPE)
+            .add(PhoneLookup.LABEL, Phone.LABEL)
+            .add(PhoneLookup.NORMALIZED_NUMBER, Phone.NORMALIZED_NUMBER)
+            .build();
+
     /** Contains the just the {@link Groups} columns */
-    private static final HashMap<String, String> sGroupsProjectionMap;
+    private static final ProjectionMap sGroupsProjectionMap = ProjectionMap.builder()
+            .add(Groups._ID)
+            .add(Groups.ACCOUNT_NAME)
+            .add(Groups.ACCOUNT_TYPE)
+            .add(Groups.SOURCE_ID)
+            .add(Groups.DIRTY)
+            .add(Groups.VERSION)
+            .add(Groups.RES_PACKAGE)
+            .add(Groups.TITLE)
+            .add(Groups.TITLE_RES)
+            .add(Groups.GROUP_VISIBLE)
+            .add(Groups.SYSTEM_ID)
+            .add(Groups.DELETED)
+            .add(Groups.NOTES)
+            .add(Groups.SHOULD_SYNC)
+            .add(Groups.FAVORITES)
+            .add(Groups.AUTO_ADD)
+            .add(Groups.SYNC1)
+            .add(Groups.SYNC2)
+            .add(Groups.SYNC3)
+            .add(Groups.SYNC4)
+            .build();
+
     /** Contains {@link Groups} columns along with summary details */
-    private static final HashMap<String, String> sGroupsSummaryProjectionMap;
+    private static final ProjectionMap sGroupsSummaryProjectionMap = ProjectionMap.builder()
+            .addAll(sGroupsProjectionMap)
+            .add(Groups.SUMMARY_COUNT,
+                    "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
+                    + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS
+                    + " WHERE " + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP
+                    + " AND " + Clauses.BELONGS_TO_GROUP
+                    + ")")
+            .add(Groups.SUMMARY_WITH_PHONES,
+                    "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
+                    + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS
+                    + " WHERE " + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP
+                    + " AND " + Clauses.BELONGS_TO_GROUP
+                    + " AND " + Contacts.HAS_PHONE_NUMBER + ")")
+            .build();
+
     /** Contains the agg_exceptions columns */
-    private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
+    private static final ProjectionMap sAggregationExceptionsProjectionMap = ProjectionMap.builder()
+            .add(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id")
+            .add(AggregationExceptions.TYPE)
+            .add(AggregationExceptions.RAW_CONTACT_ID1)
+            .add(AggregationExceptions.RAW_CONTACT_ID2)
+            .build();
+
     /** Contains the agg_exceptions columns */
-    private static final HashMap<String, String> sSettingsProjectionMap;
+    private static final ProjectionMap sSettingsProjectionMap = ProjectionMap.builder()
+            .add(Settings.ACCOUNT_NAME)
+            .add(Settings.ACCOUNT_TYPE)
+            .add(Settings.UNGROUPED_VISIBLE)
+            .add(Settings.SHOULD_SYNC)
+            .add(Settings.ANY_UNSYNCED,
+                    "(CASE WHEN MIN(" + Settings.SHOULD_SYNC
+                        + ",(SELECT "
+                                + "(CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL"
+                                + " THEN 1"
+                                + " ELSE MIN(" + Groups.SHOULD_SYNC + ")"
+                                + " END)"
+                            + " FROM " + Tables.GROUPS
+                            + " WHERE " + GroupsColumns.CONCRETE_ACCOUNT_NAME + "="
+                                    + SettingsColumns.CONCRETE_ACCOUNT_NAME
+                                + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
+                                    + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0"
+                    + " THEN 1"
+                    + " ELSE 0"
+                    + " END)")
+            .add(Settings.UNGROUPED_COUNT,
+                    "(SELECT COUNT(*)"
+                    + " FROM (SELECT 1"
+                            + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS
+                            + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
+                            + " HAVING " + Clauses.HAVING_NO_GROUPS
+                    + "))")
+            .add(Settings.UNGROUPED_WITH_PHONES,
+                    "(SELECT COUNT(*)"
+                    + " FROM (SELECT 1"
+                            + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS
+                            + " WHERE " + Contacts.HAS_PHONE_NUMBER
+                            + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
+                            + " HAVING " + Clauses.HAVING_NO_GROUPS
+                    + "))")
+            .build();
+
     /** Contains StatusUpdates columns */
-    private static final HashMap<String, String> sStatusUpdatesProjectionMap;
+    private static final ProjectionMap sStatusUpdatesProjectionMap = ProjectionMap.builder()
+            .add(PresenceColumns.RAW_CONTACT_ID)
+            .add(StatusUpdates.DATA_ID, DataColumns.CONCRETE_ID)
+            .add(StatusUpdates.IM_ACCOUNT)
+            .add(StatusUpdates.IM_HANDLE)
+            .add(StatusUpdates.PROTOCOL)
+            // We cannot allow a null in the custom protocol field, because SQLite3 does not
+            // properly enforce uniqueness of null values
+            .add(StatusUpdates.CUSTOM_PROTOCOL,
+                    "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL + "=''"
+                    + " THEN NULL"
+                    + " ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END)")
+            .add(StatusUpdates.PRESENCE)
+            .add(StatusUpdates.CHAT_CAPABILITY)
+            .add(StatusUpdates.STATUS)
+            .add(StatusUpdates.STATUS_TIMESTAMP)
+            .add(StatusUpdates.STATUS_RES_PACKAGE)
+            .add(StatusUpdates.STATUS_ICON)
+            .add(StatusUpdates.STATUS_LABEL)
+            .build();
+
     /** Contains Live Folders columns */
-    private static final HashMap<String, String> sLiveFoldersProjectionMap;
+    private static final ProjectionMap sLiveFoldersProjectionMap = ProjectionMap.builder()
+            .add(LiveFolders._ID, Contacts._ID)
+            .add(LiveFolders.NAME, Contacts.DISPLAY_NAME)
+            // TODO: Put contact photo back when we have a way to display a default icon
+            // for contacts without a photo
+            // .add(LiveFolders.ICON_BITMAP, Photos.DATA)
+            .build();
+
+    /** Contains {@link Directory} columns */
+    private static final ProjectionMap sDirectoryProjectionMap = ProjectionMap.builder()
+            .add(Directory._ID)
+            .add(Directory.PACKAGE_NAME)
+            .add(Directory.TYPE_RESOURCE_ID)
+            .add(Directory.DISPLAY_NAME)
+            .add(Directory.DIRECTORY_AUTHORITY)
+            .add(Directory.ACCOUNT_TYPE)
+            .add(Directory.ACCOUNT_NAME)
+            .add(Directory.EXPORT_SUPPORT)
+            .add(Directory.SHORTCUT_SUPPORT)
+            .add(Directory.PHOTO_SUPPORT)
+            .build();
 
     // where clause to update the status_updates table
     private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
@@ -433,15 +860,24 @@
         final UriMatcher matcher = sUriMatcher;
         matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
-        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_ID_DATA);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/entities", CONTACTS_ID_ENTITIES);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
                 AGGREGATION_SUGGESTIONS);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
                 AGGREGATION_SUGGESTIONS);
-        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_ID_PHOTO);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/data", CONTACTS_LOOKUP_DATA);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/data",
+                CONTACTS_LOOKUP_ID_DATA);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/entities",
+                CONTACTS_LOOKUP_ENTITIES);
+        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/entities",
+                CONTACTS_LOOKUP_ID_ENTITIES);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
                 CONTACTS_AS_MULTI_VCARD);
@@ -465,6 +901,7 @@
         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
         matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup", EMAILS_LOOKUP);
         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
@@ -507,469 +944,24 @@
                 LIVE_FOLDERS_CONTACTS_FAVORITES);
 
         matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS);
+
+        matcher.addURI(ContactsContract.AUTHORITY, "directories", DIRECTORIES);
+        matcher.addURI(ContactsContract.AUTHORITY, "directories/#", DIRECTORIES_ID);
+
+        matcher.addURI(ContactsContract.AUTHORITY, "complete_name", COMPLETE_NAME);
     }
 
-    static {
-        sCountProjectionMap = new HashMap<String, String>();
-        sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)");
-
-        sContactsProjectionMap = new HashMap<String, String>();
-        sContactsProjectionMap.put(Contacts._ID, Contacts._ID);
-        sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_PRIMARY);
-        sContactsProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
-                Contacts.DISPLAY_NAME_ALTERNATIVE);
-        sContactsProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
-        sContactsProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
-        sContactsProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
-        sContactsProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
-        sContactsProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
-        sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
-        sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
-        sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
-        sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
-        sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
-        sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
-        sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER);
-        sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
-        sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
-
-        // Handle projections for Contacts-level statuses
-        addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE,
-                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
-        addProjection(sContactsProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY,
-                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
-        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS);
-        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
-        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
-        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
-        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
-
-        sContactsProjectionWithSnippetMap = new HashMap<String, String>();
-        sContactsProjectionWithSnippetMap.putAll(sContactsProjectionMap);
-        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_MIMETYPE,
-                SearchSnippetColumns.SNIPPET_MIMETYPE);
-        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA_ID,
-                SearchSnippetColumns.SNIPPET_DATA_ID);
-        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA1,
-                SearchSnippetColumns.SNIPPET_DATA1);
-        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA2,
-                SearchSnippetColumns.SNIPPET_DATA2);
-        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA3,
-                SearchSnippetColumns.SNIPPET_DATA3);
-        sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA4,
-                SearchSnippetColumns.SNIPPET_DATA4);
-
-        sStrequentStarredProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
-        sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
-                  Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN);
-
-        sStrequentFrequentProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
-        sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
-                  Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN);
-
-        sContactsVCardProjectionMap = Maps.newHashMap();
-        sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME
-                + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME);
-        sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "NULL AS " + OpenableColumns.SIZE);
-
-        sRawContactsProjectionMap = new HashMap<String, String>();
-        sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID);
-        sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
-        sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
-        sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
-        sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
-        sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
-        sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
-        sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED);
-        sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_PRIMARY,
-                RawContacts.DISPLAY_NAME_PRIMARY);
-        sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_ALTERNATIVE,
-                RawContacts.DISPLAY_NAME_ALTERNATIVE);
-        sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_SOURCE,
-                RawContacts.DISPLAY_NAME_SOURCE);
-        sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME,
-                RawContacts.PHONETIC_NAME);
-        sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME_STYLE,
-                RawContacts.PHONETIC_NAME_STYLE);
-        sRawContactsProjectionMap.put(RawContacts.NAME_VERIFIED,
-                RawContacts.NAME_VERIFIED);
-        sRawContactsProjectionMap.put(RawContacts.SORT_KEY_PRIMARY,
-                RawContacts.SORT_KEY_PRIMARY);
-        sRawContactsProjectionMap.put(RawContacts.SORT_KEY_ALTERNATIVE,
-                RawContacts.SORT_KEY_ALTERNATIVE);
-        sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED);
-        sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED,
-                RawContacts.LAST_TIME_CONTACTED);
-        sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE);
-        sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL);
-        sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED);
-        sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE);
-        sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1);
-        sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2);
-        sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3);
-        sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4);
-
-        sDataProjectionMap = new HashMap<String, String>();
-        sDataProjectionMap.put(Data._ID, Data._ID);
-        sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID);
-        sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
-        sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
-        sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
-        sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
-        sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
-        sDataProjectionMap.put(Data.DATA1, Data.DATA1);
-        sDataProjectionMap.put(Data.DATA2, Data.DATA2);
-        sDataProjectionMap.put(Data.DATA3, Data.DATA3);
-        sDataProjectionMap.put(Data.DATA4, Data.DATA4);
-        sDataProjectionMap.put(Data.DATA5, Data.DATA5);
-        sDataProjectionMap.put(Data.DATA6, Data.DATA6);
-        sDataProjectionMap.put(Data.DATA7, Data.DATA7);
-        sDataProjectionMap.put(Data.DATA8, Data.DATA8);
-        sDataProjectionMap.put(Data.DATA9, Data.DATA9);
-        sDataProjectionMap.put(Data.DATA10, Data.DATA10);
-        sDataProjectionMap.put(Data.DATA11, Data.DATA11);
-        sDataProjectionMap.put(Data.DATA12, Data.DATA12);
-        sDataProjectionMap.put(Data.DATA13, Data.DATA13);
-        sDataProjectionMap.put(Data.DATA14, Data.DATA14);
-        sDataProjectionMap.put(Data.DATA15, Data.DATA15);
-        sDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
-        sDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
-        sDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
-        sDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
-        sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID);
-        sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
-        sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
-        sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
-        sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
-        sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
-        sDataProjectionMap.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
-        sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
-        sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
-        sDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
-                Contacts.DISPLAY_NAME_ALTERNATIVE);
-        sDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
-        sDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
-        sDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
-        sDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
-        sDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
-        sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
-        sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
-        sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
-        sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
-        sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
-        sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
-        sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
-        sDataProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID);
-        sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
-
-        HashMap<String, String> columns;
-        columns = new HashMap<String, String>();
-        columns.put(RawContacts._ID, RawContacts._ID);
-        columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
-        columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
-        columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
-        columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
-        columns.put(RawContacts.VERSION, RawContacts.VERSION);
-        columns.put(RawContacts.DIRTY, RawContacts.DIRTY);
-        columns.put(RawContacts.DELETED, RawContacts.DELETED);
-        columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED);
-        columns.put(RawContacts.SYNC1, RawContacts.SYNC1);
-        columns.put(RawContacts.SYNC2, RawContacts.SYNC2);
-        columns.put(RawContacts.SYNC3, RawContacts.SYNC3);
-        columns.put(RawContacts.SYNC4, RawContacts.SYNC4);
-        columns.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
-        columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
-        columns.put(Data.MIMETYPE, Data.MIMETYPE);
-        columns.put(Data.DATA1, Data.DATA1);
-        columns.put(Data.DATA2, Data.DATA2);
-        columns.put(Data.DATA3, Data.DATA3);
-        columns.put(Data.DATA4, Data.DATA4);
-        columns.put(Data.DATA5, Data.DATA5);
-        columns.put(Data.DATA6, Data.DATA6);
-        columns.put(Data.DATA7, Data.DATA7);
-        columns.put(Data.DATA8, Data.DATA8);
-        columns.put(Data.DATA9, Data.DATA9);
-        columns.put(Data.DATA10, Data.DATA10);
-        columns.put(Data.DATA11, Data.DATA11);
-        columns.put(Data.DATA12, Data.DATA12);
-        columns.put(Data.DATA13, Data.DATA13);
-        columns.put(Data.DATA14, Data.DATA14);
-        columns.put(Data.DATA15, Data.DATA15);
-        columns.put(Data.SYNC1, Data.SYNC1);
-        columns.put(Data.SYNC2, Data.SYNC2);
-        columns.put(Data.SYNC3, Data.SYNC3);
-        columns.put(Data.SYNC4, Data.SYNC4);
-        columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID);
-        columns.put(Data.STARRED, Data.STARRED);
-        columns.put(Data.DATA_VERSION, Data.DATA_VERSION);
-        columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
-        columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
-        columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
-        sRawContactsEntityProjectionMap = columns;
-
-        // Handle projections for Contacts-level statuses
-        addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE,
-                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
-        addProjection(sContactsProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY,
-                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
-        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS);
-        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
-        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
-        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
-        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
-
-        // Handle projections for Data-level statuses
-        addProjection(sDataProjectionMap, Data.PRESENCE,
-                Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
-        addProjection(sDataProjectionMap, Data.CONTACT_CHAT_CAPABILITY,
-                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
-        addProjection(sDataProjectionMap, Data.STATUS,
-                StatusUpdatesColumns.CONCRETE_STATUS);
-        addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP,
-                StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
-        addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE,
-                StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
-        addProjection(sDataProjectionMap, Data.STATUS_LABEL,
-                StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
-        addProjection(sDataProjectionMap, Data.STATUS_ICON,
-                StatusUpdatesColumns.CONCRETE_STATUS_ICON);
-
-        // Projection map for data grouped by contact (not raw contact) and some data field(s)
-        sDistinctDataProjectionMap = new HashMap<String, String>();
-        sDistinctDataProjectionMap.put(Data._ID,
-                "MIN(" + Data._ID + ") AS " + Data._ID);
-        sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
-        sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
-        sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
-        sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
-        sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
-        sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1);
-        sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2);
-        sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3);
-        sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4);
-        sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5);
-        sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6);
-        sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7);
-        sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8);
-        sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9);
-        sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10);
-        sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11);
-        sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12);
-        sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13);
-        sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14);
-        sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15);
-        sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
-        sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
-        sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
-        sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
-        sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
-        sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
-        sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
-        sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
-                Contacts.DISPLAY_NAME_ALTERNATIVE);
-        sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
-        sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
-        sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
-        sDistinctDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
-        sDistinctDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE,
-                Contacts.SORT_KEY_ALTERNATIVE);
-        sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
-        sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
-        sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
-        sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
-        sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
-        sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
-        sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
-        sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID,
-                GroupMembership.GROUP_SOURCE_ID);
-
-        // Handle projections for Contacts-level statuses
-        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE,
-                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
-        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY,
-                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
-        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS);
-        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
-        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
-        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
-        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
-                ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
-
-        // Handle projections for Data-level statuses
-        addProjection(sDistinctDataProjectionMap, Data.PRESENCE,
-                Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
-        addProjection(sDistinctDataProjectionMap, Data.CHAT_CAPABILITY,
-                Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
-        addProjection(sDistinctDataProjectionMap, Data.STATUS,
-                StatusUpdatesColumns.CONCRETE_STATUS);
-        addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP,
-                StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
-        addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE,
-                StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
-        addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL,
-                StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
-        addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON,
-                StatusUpdatesColumns.CONCRETE_STATUS_ICON);
-
-        sPhoneLookupProjectionMap = new HashMap<String, String>();
-        sPhoneLookupProjectionMap.put(PhoneLookup._ID,
-                "contacts_view." + Contacts._ID
-                        + " AS " + PhoneLookup._ID);
-        sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY,
-                "contacts_view." + Contacts.LOOKUP_KEY
-                        + " AS " + PhoneLookup.LOOKUP_KEY);
-        sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME,
-                "contacts_view." + Contacts.DISPLAY_NAME
-                        + " AS " + PhoneLookup.DISPLAY_NAME);
-        sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED,
-                "contacts_view." + Contacts.LAST_TIME_CONTACTED
-                        + " AS " + PhoneLookup.LAST_TIME_CONTACTED);
-        sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED,
-                "contacts_view." + Contacts.TIMES_CONTACTED
-                        + " AS " + PhoneLookup.TIMES_CONTACTED);
-        sPhoneLookupProjectionMap.put(PhoneLookup.STARRED,
-                "contacts_view." + Contacts.STARRED
-                        + " AS " + PhoneLookup.STARRED);
-        sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP,
-                "contacts_view." + Contacts.IN_VISIBLE_GROUP
-                        + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
-        sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID,
-                "contacts_view." + Contacts.PHOTO_ID
-                        + " AS " + PhoneLookup.PHOTO_ID);
-        sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE,
-                "contacts_view." + Contacts.CUSTOM_RINGTONE
-                        + " AS " + PhoneLookup.CUSTOM_RINGTONE);
-        sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER,
-                "contacts_view." + Contacts.HAS_PHONE_NUMBER
-                        + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
-        sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL,
-                "contacts_view." + Contacts.SEND_TO_VOICEMAIL
-                        + " AS " + PhoneLookup.SEND_TO_VOICEMAIL);
-        sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER,
-                Phone.NUMBER + " AS " + PhoneLookup.NUMBER);
-        sPhoneLookupProjectionMap.put(PhoneLookup.TYPE,
-                Phone.TYPE + " AS " + PhoneLookup.TYPE);
-        sPhoneLookupProjectionMap.put(PhoneLookup.LABEL,
-                Phone.LABEL + " AS " + PhoneLookup.LABEL);
-
-        // Groups projection map
-        columns = new HashMap<String, String>();
-        columns.put(Groups._ID, Groups._ID);
-        columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
-        columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
-        columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID);
-        columns.put(Groups.DIRTY, Groups.DIRTY);
-        columns.put(Groups.VERSION, Groups.VERSION);
-        columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE);
-        columns.put(Groups.TITLE, Groups.TITLE);
-        columns.put(Groups.TITLE_RES, Groups.TITLE_RES);
-        columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
-        columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID);
-        columns.put(Groups.DELETED, Groups.DELETED);
-        columns.put(Groups.NOTES, Groups.NOTES);
-        columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC);
-        columns.put(Groups.SYNC1, Groups.SYNC1);
-        columns.put(Groups.SYNC2, Groups.SYNC2);
-        columns.put(Groups.SYNC3, Groups.SYNC3);
-        columns.put(Groups.SYNC4, Groups.SYNC4);
-        sGroupsProjectionMap = columns;
-
-        // RawContacts and groups projection map
-        columns = new HashMap<String, String>();
-        columns.putAll(sGroupsProjectionMap);
-        columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
-                + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
-                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
-                + ") AS " + Groups.SUMMARY_COUNT);
-        columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
-                + ContactsColumns.CONCRETE_ID + ") FROM "
-                + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
-                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
-                + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES);
-        sGroupsSummaryProjectionMap = columns;
-
-        // Aggregate exception projection map
-        columns = new HashMap<String, String>();
-        columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
-        columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
-        columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1);
-        columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2);
-        sAggregationExceptionsProjectionMap = columns;
-
-        // Settings projection map
-        columns = new HashMap<String, String>();
-        columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME);
-        columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE);
-        columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE);
-        columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC);
-        columns.put(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC
-                + ",(SELECT (CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL THEN 1 ELSE MIN("
-                + Groups.SHOULD_SYNC + ") END) FROM " + Tables.GROUPS + " WHERE "
-                + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME
-                + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
-                + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0 THEN 1 ELSE 0 END) AS "
-                + Settings.ANY_UNSYNCED);
-        columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
-                + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY "
-                + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS
-                + ")) AS " + Settings.UNGROUPED_COUNT);
-        columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
-                + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE "
-                + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
-                + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS "
-                + Settings.UNGROUPED_WITH_PHONES);
-        sSettingsProjectionMap = columns;
-
-        columns = new HashMap<String, String>();
-        columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID);
-        columns.put(StatusUpdates.DATA_ID,
-                DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID);
-        columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT);
-        columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE);
-        columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL);
-        // We cannot allow a null in the custom protocol field, because SQLite3 does not
-        // properly enforce uniqueness of null values
-        columns.put(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL
-                + "='' THEN NULL ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END) AS "
-                + StatusUpdates.CUSTOM_PROTOCOL);
-        columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE);
-        columns.put(StatusUpdates.CHAT_CAPABILITY, StatusUpdates.CHAT_CAPABILITY);
-        columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS);
-        columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP);
-        columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE);
-        columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON);
-        columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL);
-        sStatusUpdatesProjectionMap = columns;
-
-        // Live folder projection
-        sLiveFoldersProjectionMap = new HashMap<String, String>();
-        sLiveFoldersProjectionMap.put(LiveFolders._ID,
-                Contacts._ID + " AS " + LiveFolders._ID);
-        sLiveFoldersProjectionMap.put(LiveFolders.NAME,
-                Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME);
-        // TODO: Put contact photo back when we have a way to display a default icon
-        // for contacts without a photo
-        // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP,
-        //      Photos.DATA + " AS " + LiveFolders.ICON_BITMAP);
+    private static class DirectoryInfo {
+        String authority;
+        String accountName;
+        String accountType;
     }
 
-    private static void addProjection(HashMap<String, String> map, String toField, String fromField) {
-        map.put(toField, fromField + " AS " + toField);
-    }
+    /**
+     * Cached information about contact directories.
+     */
+    private HashMap<String, DirectoryInfo> mDirectoryCache = new HashMap<String, DirectoryInfo>();
+    private boolean mDirectoryCacheValid = false;
 
     /**
      * Handles inserts and update for a specific Data type.
@@ -1237,7 +1229,7 @@
          * is not provided, generate one by concatenating first name and last
          * name.
          */
-        private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) {
+        public void fixStructuredNameComponents(ContentValues augmented, ContentValues update) {
             final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME);
 
             final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
@@ -1604,14 +1596,18 @@
             long dataId;
             if (values.containsKey(Phone.NUMBER)) {
                 String number = values.getAsString(Phone.NUMBER);
-                String normalizedNumber = computeNormalizedNumber(number);
-                values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
+
+                String numberE164 =
+                        PhoneNumberUtils.formatNumberToE164(number, getCurrentCountryIso());
+                if (numberE164 != null) {
+                    values.put(PhoneColumns.NORMALIZED_NUMBER, numberE164);
+                }
                 dataId = super.insert(db, rawContactId, values);
 
-                updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
+                updatePhoneLookup(db, rawContactId, dataId, number, numberE164);
                 mContactAggregator.updateHasPhoneNumber(db, rawContactId);
                 fixRawContactDisplayName(db, rawContactId);
-                if (normalizedNumber != null) {
+                if (numberE164 != null) {
                     triggerAggregation(rawContactId);
                 }
             } else {
@@ -1625,10 +1621,16 @@
                 boolean callerIsSyncAdapter) {
             String number = null;
             String normalizedNumber = null;
+            String numberE164 = null;
             if (values.containsKey(Phone.NUMBER)) {
                 number = values.getAsString(Phone.NUMBER);
-                normalizedNumber = computeNormalizedNumber(number);
-                values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
+                if (number != null) {
+                    numberE164 =
+                        PhoneNumberUtils.formatNumberToE164(number, getCurrentCountryIso());
+                }
+                if (numberE164 != null) {
+                    values.put(PhoneColumns.NORMALIZED_NUMBER, numberE164);
+                }
             }
 
             if (!super.update(db, values, c, callerIsSyncAdapter)) {
@@ -1638,7 +1640,7 @@
             if (values.containsKey(Phone.NUMBER)) {
                 long dataId = c.getLong(DataUpdateQuery._ID);
                 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
-                updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
+                updatePhoneLookup(db, rawContactId, dataId, number, numberE164);
                 mContactAggregator.updateHasPhoneNumber(db, rawContactId);
                 fixRawContactDisplayName(db, rawContactId);
                 triggerAggregation(rawContactId);
@@ -1660,28 +1662,28 @@
             return count;
         }
 
-        private String computeNormalizedNumber(String number) {
-            String normalizedNumber = null;
-            if (number != null) {
-                normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
-            }
-            return normalizedNumber;
-        }
-
         private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId,
-                String number, String normalizedNumber) {
+                String number, String numberE164) {
+            mSelectionArgs1[0] = String.valueOf(dataId);
+            db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1);
             if (number != null) {
-                ContentValues phoneValues = new ContentValues();
-                phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
-                phoneValues.put(PhoneLookupColumns.DATA_ID, dataId);
-                phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
-                phoneValues.put(PhoneLookupColumns.MIN_MATCH,
-                        PhoneNumberUtils.toCallerIDMinMatch(number));
+                String normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
+                if (!TextUtils.isEmpty(normalizedNumber)) {
+                    ContentValues phoneValues = new ContentValues();
+                    phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
+                    phoneValues.put(PhoneLookupColumns.DATA_ID, dataId);
+                    phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
+                    phoneValues.put(PhoneLookupColumns.MIN_MATCH,
+                            PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber));
+                    db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
 
-                db.replace(Tables.PHONE_LOOKUP, null, phoneValues);
-            } else {
-                mSelectionArgs1[0] = String.valueOf(dataId);
-                db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1);
+                    if (numberE164 != null && !numberE164.equals(normalizedNumber)) {
+                        phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, numberE164);
+                        phoneValues.put(PhoneLookupColumns.MIN_MATCH,
+                                PhoneNumberUtils.toCallerIDMinMatch(numberE164));
+                        db.insert(Tables.PHONE_LOOKUP, null, phoneValues);
+                    }
+                }
             }
         }
 
@@ -1703,6 +1705,16 @@
 
     public class GroupMembershipRowHandler extends DataRowHandler {
 
+        private static final String SELECTION_RAW_CONTACT_ID = RawContacts._ID + "=?";
+
+        private static final String QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID =
+                "SELECT COUNT(*) FROM " + Tables.DATA + " LEFT OUTER JOIN " + Tables .GROUPS
+                        + " ON " + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID
+                        + "=" + GroupsColumns.CONCRETE_ID
+                        + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
+                        + " AND " + Tables.DATA + "." + GroupMembership.RAW_CONTACT_ID + "=?"
+                        + " AND " + Groups.FAVORITES + "!=0";
+
         public GroupMembershipRowHandler() {
             super(GroupMembership.CONTENT_ITEM_TYPE);
         }
@@ -1711,6 +1723,9 @@
         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
             resolveGroupSourceIdInValues(rawContactId, db, values, true);
             long dataId = super.insert(db, rawContactId, values);
+            if (hasFavoritesGroupMembership(db, rawContactId)) {
+                updateRawContactsStar(db, rawContactId, true /* starred */);
+            }
             updateVisibility(rawContactId);
             return dataId;
         }
@@ -1719,18 +1734,46 @@
         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
                 boolean callerIsSyncAdapter) {
             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
+            boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId);
             resolveGroupSourceIdInValues(rawContactId, db, values, false);
             if (!super.update(db, values, c, callerIsSyncAdapter)) {
                 return false;
             }
+            boolean isStarred = hasFavoritesGroupMembership(db, rawContactId);
+            if (wasStarred != isStarred) {
+                updateRawContactsStar(db, rawContactId, isStarred);
+            }
             updateVisibility(rawContactId);
             return true;
         }
 
+        private void updateRawContactsStar(SQLiteDatabase db, long rawContactId, boolean starred) {
+            ContentValues rawContactValues = new ContentValues();
+            rawContactValues.put(RawContacts.STARRED, starred ? 1 : 0);
+            if (db.update(Tables.RAW_CONTACTS, rawContactValues, SELECTION_RAW_CONTACT_ID,
+                    new String[]{Long.toString(rawContactId)}) > 0) {
+                mContactAggregator.updateStarred(rawContactId);
+            }
+        }
+
+        private boolean hasFavoritesGroupMembership(SQLiteDatabase db, long rawContactId) {
+            final long groupMembershipMimetypeId = mDbHelper
+                    .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
+            boolean isStarred = 0 < DatabaseUtils
+                    .longForQuery(db, QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID,
+                    new String[]{Long.toString(groupMembershipMimetypeId), Long.toString(rawContactId)});
+            return isStarred;
+        }
+
         @Override
         public int delete(SQLiteDatabase db, Cursor c) {
             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
+            boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId);
             int count = super.delete(db, c);
+            boolean isStarred = hasFavoritesGroupMembership(db, rawContactId);
+            if (wasStarred && !isStarred) {
+                updateRawContactsStar(db, rawContactId, false /* starred */);
+            }
             updateVisibility(rawContactId);
             return count;
         }
@@ -1832,6 +1875,7 @@
     // is a list of groups with this group id.
     private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
 
+    private ContactDirectoryManager mContactDirectoryManager;
     private ContactAggregator mContactAggregator;
     private LegacyApiSupport mLegacyApiSupport;
     private GlobalSearchSupport mGlobalSearchSupport;
@@ -1857,6 +1901,8 @@
 
     private Locale mCurrentLocale;
 
+    private CountryMonitor mCountryMonitor;
+
 
     @Override
     public boolean onCreate() {
@@ -1871,13 +1917,11 @@
 
     private boolean initialize() {
         final Context context = getContext();
+        mCountryMonitor = CountryMonitor.getInstance(context);
         mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
+        mContactDirectoryManager = new ContactDirectoryManager(this);
         mGlobalSearchSupport = new GlobalSearchSupport(this);
         mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
-        mContactAggregator = new ContactAggregator(this, mDbHelper,
-                createPhotoPriorityResolver(context));
-        mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
-
         mDb = mDbHelper.getWritableDatabase();
 
         initForDefaultLocale();
@@ -1999,6 +2043,8 @@
             verifyLocale();
         }
 
+        startContactDirectoryManager();
+
         if (isAggregationUpgradeNeeded()) {
             upgradeAggregationAlgorithm();
         }
@@ -2006,6 +2052,10 @@
         return (mDb != null);
     }
 
+    protected String getCurrentCountryIso() {
+        return mCountryMonitor.getCountryIso();
+    }
+
     private void initDataRowHandlers() {
       mDataRowHandlers = new HashMap<String, DataRowHandler>();
 
@@ -2024,6 +2074,7 @@
       mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler());
       mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler());
     }
+
     /**
      * Visible for testing.
      */
@@ -2041,11 +2092,14 @@
         mPostalSplitter = new PostalSplitter(mCurrentLocale);
         mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase());
         ContactLocaleUtils.getIntance().setLocale(mCurrentLocale);
+        mContactAggregator = new ContactAggregator(this, mDbHelper,
+                createPhotoPriorityResolver(getContext()), mNameSplitter, mCommonNicknameCache);
+        mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
+
         initDataRowHandlers();
     }
 
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onLocaleChanged() {
         if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
             return;
         }
@@ -2056,7 +2110,7 @@
 
     protected void verifyAccounts() {
         AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
-        onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
+        updateAccounts(AccountManager.get(getContext()).getAccounts());
     }
 
     /**
@@ -2119,10 +2173,20 @@
     }
 
     /* Visible for testing */
+    public ContactDirectoryManager getContactDirectoryManager() {
+        return mContactDirectoryManager;
+    }
+
+    /* Visible for testing */
     protected Locale getLocale() {
         return Locale.getDefault();
     }
 
+    /* Visible for testing */
+    protected void startContactDirectoryManager() {
+        getContactDirectoryManager().start();
+    }
+
     protected boolean isLegacyContactImportNeeded() {
         int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0"));
         return version < PROPERTY_CONTACTS_IMPORT_VERSION;
@@ -2425,7 +2489,7 @@
             }
 
             case RAW_CONTACTS: {
-                id = insertRawContact(uri, values);
+                id = insertRawContact(uri, values, callerIsSyncAdapter);
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 break;
             }
@@ -2552,9 +2616,10 @@
      *
      * @param uri the values for the new row
      * @param values the account this contact should be associated with. may be null.
+     * @param callerIsSyncAdapter
      * @return the row ID of the newly created row
      */
-    private long insertRawContact(Uri uri, ContentValues values) {
+    private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
         mValues.clear();
         mValues.putAll(values);
         mValues.putNull(RawContacts.CONTACT_ID);
@@ -2576,9 +2641,69 @@
         // Trigger creation of a Contact based on this RawContact at the end of transaction
         mInsertedRawContacts.put(rawContactId, account);
 
+        if (!callerIsSyncAdapter) {
+            addAutoAddMembership(rawContactId);
+            final Long starred = values.getAsLong(RawContacts.STARRED);
+            if (starred != null && starred != 0) {
+                updateFavoritesMembership(rawContactId, starred != 0);
+            }
+        }
+
         return rawContactId;
     }
 
+    private void addAutoAddMembership(long rawContactId) {
+        final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID,
+                rawContactId);
+        if (groupId != null) {
+            insertDataGroupMembership(rawContactId, groupId);
+        }
+    }
+
+    private Long findGroupByRawContactId(String selection, long rawContactId) {
+        Cursor c = mDb.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, PROJECTION_GROUP_ID,
+                selection,
+                new String[]{Long.toString(rawContactId)},
+                null /* groupBy */, null /* having */, null /* orderBy */);
+        try {
+            while (c.moveToNext()) {
+                return c.getLong(0);
+            }
+            return null;
+        } finally {
+            c.close();
+        }
+    }
+
+    private void updateFavoritesMembership(long rawContactId, boolean isStarred) {
+        final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID,
+                rawContactId);
+        if (groupId != null) {
+            if (isStarred) {
+                insertDataGroupMembership(rawContactId, groupId);
+            } else {
+                deleteDataGroupMembership(rawContactId, groupId);
+            }
+        }
+    }
+
+    private void insertDataGroupMembership(long rawContactId, long groupId) {
+        ContentValues groupMembershipValues = new ContentValues();
+        groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId);
+        groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId);
+        groupMembershipValues.put(DataColumns.MIMETYPE_ID,
+                mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
+        mDb.insert(Tables.DATA, null, groupMembershipValues);
+    }
+
+    private void deleteDataGroupMembership(long rawContactId, long groupId) {
+        final String[] selectionArgs = {
+                Long.toString(mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)),
+                Long.toString(groupId),
+                Long.toString(rawContactId)};
+        mDb.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs);
+    }
+
     /**
      * Inserts an item in the data table
      *
@@ -3014,12 +3139,45 @@
         }
         mValues.remove(Groups.RES_PACKAGE);
 
+        final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null
+                ? mValues.getAsLong(Groups.FAVORITES) != 0
+                : false;
+
         if (!callerIsSyncAdapter) {
             mValues.put(Groups.DIRTY, 1);
         }
 
         long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues);
 
+        if (!callerIsSyncAdapter && isFavoritesGroup) {
+            // add all starred raw contacts to this group
+            String selection;
+            String[] selectionArgs;
+            if (account == null) {
+                selection = RawContacts.ACCOUNT_NAME + " IS NULL AND "
+                        + RawContacts.ACCOUNT_TYPE + " IS NULL";
+                selectionArgs = null;
+            } else {
+                selection = RawContacts.ACCOUNT_NAME + "=? AND "
+                        + RawContacts.ACCOUNT_TYPE + "=?";
+                selectionArgs = new String[]{account.name, account.type};
+            }
+            Cursor c = mDb.query(Tables.RAW_CONTACTS,
+                    new String[]{RawContacts._ID, RawContacts.STARRED},
+                    selection, selectionArgs, null, null, null);
+            try {
+                while (c.moveToNext()) {
+                    if (c.getLong(1) != 0) {
+                        final long rawContactId = c.getLong(0);
+                        insertDataGroupMembership(rawContactId, result);
+                        setRawContactDirty(rawContactId);
+                    }
+                }
+            } finally {
+                c.close();
+            }
+        }
+
         if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
             mVisibleTouched = true;
         }
@@ -3121,7 +3279,7 @@
         try {
             cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
                     mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null,
-                    Contacts.IN_VISIBLE_GROUP + " DESC, " + Data.RAW_CONTACT_ID);
+                    Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID);
             if (cursor.moveToFirst()) {
                 dataId = cursor.getLong(DataContactsQuery.DATA_ID);
                 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
@@ -3251,7 +3409,7 @@
 
             case CONTACTS_ID: {
                 long contactId = ContentUris.parseId(uri);
-                return deleteContact(contactId);
+                return deleteContact(contactId, callerIsSyncAdapter);
             }
 
             case CONTACTS_LOOKUP: {
@@ -3263,16 +3421,16 @@
                 }
                 final String lookupKey = pathSegments.get(2);
                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
-                return deleteContact(contactId);
+                return deleteContact(contactId, callerIsSyncAdapter);
             }
 
             case CONTACTS_LOOKUP_ID: {
                 // lookup contact by id and lookup key to see if they still match the actual record
-                long contactId = ContentUris.parseId(uri);
                 final List<String> pathSegments = uri.getPathSegments();
                 final String lookupKey = pathSegments.get(2);
                 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
                 setTablesAndProjectionMapForContacts(lookupQb, uri, null);
+                long contactId = ContentUris.parseId(uri);
                 String[] args;
                 if (selectionArgs == null) {
                     args = new String[2];
@@ -3288,7 +3446,7 @@
                 try {
                     if (c.getCount() == 1) {
                         // contact was unmodified so go ahead and delete it
-                        return deleteContact(contactId);
+                        return deleteContact(contactId, callerIsSyncAdapter);
                     } else {
                         // row was changed (e.g. the merging might have changed), we got multiple
                         // rows or the supplied selection filtered the record out
@@ -3405,7 +3563,7 @@
         return count;
     }
 
-    private int deleteContact(long contactId) {
+    private int deleteContact(long contactId, boolean callerIsSyncAdapter) {
         mSelectionArgs1[0] = Long.toString(contactId);
         Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1,
@@ -3413,7 +3571,7 @@
         try {
             while (c.moveToNext()) {
                 long rawContactId = c.getLong(0);
-                markRawContactAsDeleted(rawContactId);
+                markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
             }
         } finally {
             c.close();
@@ -3431,7 +3589,7 @@
             return count;
         } else {
             mDbHelper.removeContactIfSingleton(rawContactId);
-            return markRawContactAsDeleted(rawContactId);
+            return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
         }
     }
 
@@ -3446,7 +3604,7 @@
       return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
     }
 
-    private int markRawContactAsDeleted(long rawContactId) {
+    private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) {
         mSyncToNetwork = true;
 
         mValues.clear();
@@ -3455,7 +3613,7 @@
         mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
         mValues.putNull(RawContacts.CONTACT_ID);
         mValues.put(RawContacts.DIRTY, 1);
-        return updateRawContact(rawContactId, mValues);
+        return updateRawContact(rawContactId, mValues, callerIsSyncAdapter);
     }
 
     @Override
@@ -3492,12 +3650,12 @@
             }
 
             case CONTACTS: {
-                count = updateContactOptions(values, selection, selectionArgs);
+                count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter);
                 break;
             }
 
             case CONTACTS_ID: {
-                count = updateContactOptions(ContentUris.parseId(uri), values);
+                count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter);
                 break;
             }
 
@@ -3511,7 +3669,7 @@
                 }
                 final String lookupKey = pathSegments.get(2);
                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
-                count = updateContactOptions(contactId, values);
+                count = updateContactOptions(contactId, values, callerIsSyncAdapter);
                 break;
             }
 
@@ -3547,7 +3705,7 @@
 
             case RAW_CONTACTS: {
                 selection = appendAccountToSelection(uri, selection);
-                count = updateRawContacts(values, selection, selectionArgs);
+                count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
                 break;
             }
 
@@ -3556,10 +3714,12 @@
                 if (selection != null) {
                     selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
                     count = updateRawContacts(values, RawContacts._ID + "=?"
-                                    + " AND(" + selection + ")", selectionArgs);
+                                    + " AND(" + selection + ")", selectionArgs,
+                            callerIsSyncAdapter);
                 } else {
                     mSelectionArgs1[0] = String.valueOf(rawContactId);
-                    count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1);
+                    count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1,
+                            callerIsSyncAdapter);
                 }
                 break;
             }
@@ -3603,6 +3763,12 @@
                 break;
             }
 
+            case DIRECTORIES: {
+                mContactDirectoryManager.scheduleDirectoryUpdateForCaller();
+                count = 1;
+                break;
+            }
+
             default: {
                 mSyncToNetwork = true;
                 return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
@@ -3724,12 +3890,18 @@
         return count;
     }
 
-    private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) {
+    private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs,
+            boolean callerIsSyncAdapter) {
         if (values.containsKey(RawContacts.CONTACT_ID)) {
             throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
                     "in content values. Contact IDs are assigned automatically");
         }
 
+        if (!callerIsSyncAdapter) {
+            selection = DatabaseUtils.concatenateWhere(selection,
+                    RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0");
+        }
+
         int count = 0;
         Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
                 new String[] { RawContacts._ID }, selection,
@@ -3737,7 +3909,7 @@
         try {
             while (cursor.moveToNext()) {
                 long rawContactId = cursor.getLong(0);
-                updateRawContact(rawContactId, values);
+                updateRawContact(rawContactId, values, callerIsSyncAdapter);
                 count++;
             }
         } finally {
@@ -3747,7 +3919,8 @@
         return count;
     }
 
-    private int updateRawContact(long rawContactId, ContentValues values) {
+    private int updateRawContact(long rawContactId, ContentValues values,
+            boolean callerIsSyncAdapter) {
         final String selection = RawContacts._ID + " = ?";
         mSelectionArgs1[0] = Long.toString(rawContactId);
         final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
@@ -3783,8 +3956,30 @@
                 }
             }
             if (values.containsKey(RawContacts.STARRED)) {
+                if (!callerIsSyncAdapter) {
+                    updateFavoritesMembership(rawContactId,
+                            values.getAsLong(RawContacts.STARRED) != 0);
+                }
                 mContactAggregator.updateStarred(rawContactId);
+            } else {
+                // if this raw contact is being associated with an account, then update the
+                // 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)) {
+                    boolean starred = 0 != DatabaseUtils.longForQuery(mDb,
+                            SELECTION_STARRED_FROM_RAW_CONTACTS,
+                            new String[]{Long.toString(rawContactId)});
+                    updateFavoritesMembership(rawContactId, starred);
+                }
             }
+
+            // 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)) {
+                addAutoAddMembership(rawContactId);
+            }
+
             if (values.containsKey(RawContacts.SOURCE_ID)) {
                 mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId);
             }
@@ -3835,6 +4030,11 @@
             mValues.remove(Data.IS_PRIMARY);
         }
 
+        if (!callerIsSyncAdapter) {
+            selection = DatabaseUtils.concatenateWhere(selection,
+                    Data.IS_READ_ONLY + "=0");
+        }
+
         int count = 0;
 
         // Note that the query will return data according to the access restrictions,
@@ -3866,7 +4066,7 @@
     }
 
     private int updateContactOptions(ContentValues values, String selection,
-            String[] selectionArgs) {
+            String[] selectionArgs, boolean callerIsSyncAdapter) {
         int count = 0;
         Cursor cursor = mDb.query(mDbHelper.getContactView(),
                 new String[] { Contacts._ID }, selection,
@@ -3874,7 +4074,7 @@
         try {
             while (cursor.moveToNext()) {
                 long contactId = cursor.getLong(0);
-                updateContactOptions(contactId, values);
+                updateContactOptions(contactId, values, callerIsSyncAdapter);
                 count++;
             }
         } finally {
@@ -3884,7 +4084,8 @@
         return count;
     }
 
-    private int updateContactOptions(long contactId, ContentValues values) {
+    private int updateContactOptions(long contactId, ContentValues values,
+            boolean callerIsSyncAdapter) {
 
         mValues.clear();
         ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
@@ -3909,7 +4110,23 @@
         }
 
         mSelectionArgs1[0] = String.valueOf(contactId);
-        mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
+        mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?"
+                + " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1);
+
+        if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) {
+            Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
+                    new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?",
+                    mSelectionArgs1, null, null, null);
+            try {
+                while (cursor.moveToNext()) {
+                    long rawContactId = cursor.getLong(0);
+                    updateFavoritesMembership(rawContactId,
+                            mValues.getAsLong(RawContacts.STARRED) != 0);
+                }
+            } finally {
+                cursor.close();
+            }
+        }
 
         // Copy changeable values to prevent automatically managed fields from
         // being explicitly updated by clients.
@@ -3978,44 +4195,25 @@
         return 1;
     }
 
-    /**
-     * Check whether GOOGLE_MY_CONTACTS_GROUP exists, otherwise create it.
-     *
-     * @return the group id
-     */
-    private long getOrCreateMyContactsGroupInTransaction(String accountName, String accountType) {
-        Cursor cursor = mDb.query(Tables.GROUPS, new String[] {"_id"},
-                Groups.ACCOUNT_NAME + " =? AND " + Groups.ACCOUNT_TYPE + " =? AND "
-                    + Groups.TITLE + " =?",
-                new String[] {accountName, accountType, GOOGLE_MY_CONTACTS_GROUP_TITLE},
-                null, null, null);
-        try {
-            if(cursor.moveToNext()) {
-                return cursor.getLong(0);
-            }
-        } finally {
-            cursor.close();
+    public void onAccountsUpdated(Account[] accounts) {
+        boolean accountsChanged = updateAccounts(accounts);
+        if (accountsChanged) {
+            mContactDirectoryManager.scheduleScanAllPackages(true);
         }
-
-        ContentValues values = new ContentValues();
-        values.put(Groups.TITLE, GOOGLE_MY_CONTACTS_GROUP_TITLE);
-        values.put(Groups.ACCOUNT_NAME, accountName);
-        values.put(Groups.ACCOUNT_TYPE, accountType);
-        values.put(Groups.GROUP_VISIBLE, "1");
-        return mDb.insert(Tables.GROUPS, null, values);
     }
 
-    public void onAccountsUpdated(Account[] accounts) {
+    private boolean updateAccounts(Account[] accounts) {
         // TODO : Check the unit test.
+        boolean accountsChanged = false;
         HashSet<Account> existingAccounts = new HashSet<Account>();
-        boolean hasUnassignedContacts[] = new boolean[]{false};
         mDb.beginTransaction();
         try {
-            findValidAccounts(existingAccounts, hasUnassignedContacts);
+            findValidAccounts(existingAccounts);
 
             // Add a row to the ACCOUNTS table for each new account
             for (Account account : accounts) {
                 if (!existingAccounts.contains(account)) {
+                    accountsChanged = true;
                     mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
                             + ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)",
                             new String[] {account.name, account.type});
@@ -4029,38 +4227,44 @@
                 accountsToDelete.remove(account);
             }
 
-            for (Account account : accountsToDelete) {
-                Log.d(TAG, "removing data for removed account " + account);
-                String[] params = new String[] {account.name, account.type};
-                mDb.execSQL(
-                        "DELETE FROM " + Tables.GROUPS +
-                        " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
-                                " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
-                mDb.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 + " = ?)", params);
-                mDb.execSQL(
-                        "DELETE FROM " + Tables.RAW_CONTACTS +
-                        " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
-                        " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
-                mDb.execSQL(
-                        "DELETE FROM " + Tables.SETTINGS +
-                        " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
-                        " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
-                mDb.execSQL(
-                        "DELETE FROM " + Tables.ACCOUNTS +
-                        " WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
-                        " AND " + RawContacts.ACCOUNT_TYPE + "=?", params);
-            }
-
             if (!accountsToDelete.isEmpty()) {
+                accountsChanged = true;
+                for (Account account : accountsToDelete) {
+                    Log.d(TAG, "removing data for removed account " + account);
+                    String[] params = new String[] {account.name, account.type};
+                    mDb.execSQL(
+                            "DELETE FROM " + Tables.GROUPS +
+                            " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
+                                    " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
+                    mDb.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 + " = ?)", params);
+                    mDb.execSQL(
+                            "DELETE FROM " + Tables.RAW_CONTACTS +
+                            " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
+                            " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
+                    mDb.execSQL(
+                            "DELETE FROM " + Tables.SETTINGS +
+                            " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
+                            " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
+                    mDb.execSQL(
+                            "DELETE FROM " + Tables.ACCOUNTS +
+                            " WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
+                            " AND " + RawContacts.ACCOUNT_TYPE + "=?", params);
+                    mDb.execSQL(
+                            "DELETE FROM " + Tables.DIRECTORIES +
+                            " WHERE " + Directory.ACCOUNT_NAME + "=?" +
+                            " AND " + Directory.ACCOUNT_TYPE + "=?", params);
+                    resetDirectoryCache();
+                }
+
                 // Find all aggregated contacts that used to contain the raw contacts
                 // we have just deleted and see if they are still referencing the deleted
-                // names of photos.  If so, fix up those contacts.
+                // names or photos.  If so, fix up those contacts.
                 HashSet<Long> orphanContactIds = Sets.newHashSet();
                 Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID +
                         " FROM " + Tables.CONTACTS +
@@ -4083,83 +4287,34 @@
                 for (Long contactId : orphanContactIds) {
                     mContactAggregator.updateAggregateData(contactId);
                 }
+                mDbHelper.updateAllVisible();
             }
 
-            if (hasUnassignedContacts[0]) {
-
-                Account primaryAccount = null;
-                for (Account account : accounts) {
-                    if (isWritableAccount(account.type)) {
-                        primaryAccount = account;
-                        break;
-                    }
-                }
-
-                if (primaryAccount != null) {
-                    String[] params = new String[] {primaryAccount.name, primaryAccount.type};
-                    if (primaryAccount.type.equals(DEFAULT_ACCOUNT_TYPE)) {
-                        long groupId = getOrCreateMyContactsGroupInTransaction(
-                                primaryAccount.name, primaryAccount.type);
-                        if (groupId != -1) {
-                            long mimeTypeId = mDbHelper.getMimeTypeId(
-                                    GroupMembership.CONTENT_ITEM_TYPE);
-                            mDb.execSQL(
-                                    "INSERT INTO " + Tables.DATA + "(" + DataColumns.MIMETYPE_ID +
-                                        ", " + Data.RAW_CONTACT_ID + ", "
-                                        + GroupMembership.GROUP_ROW_ID + ") " +
-                                    "SELECT " + mimeTypeId + ", "
-                                            + RawContacts._ID + ", " + groupId +
-                                    " FROM " + Tables.RAW_CONTACTS +
-                                    " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
-                                    " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL"
-                            );
-                        }
-                    }
-                    mDb.execSQL(
-                            "UPDATE " + Tables.RAW_CONTACTS +
-                            " SET " + RawContacts.ACCOUNT_NAME + "=?,"
-                                    + RawContacts.ACCOUNT_TYPE + "=?" +
-                            " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
-                            " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL", params);
-
-                    // We don't currently support groups for unsynced accounts, so this is for
-                    // the future
-                    mDb.execSQL(
-                            "UPDATE " + Tables.GROUPS +
-                            " SET " + Groups.ACCOUNT_NAME + "=?,"
-                                    + Groups.ACCOUNT_TYPE + "=?" +
-                            " WHERE " + Groups.ACCOUNT_NAME + " IS NULL" +
-                            " AND " + Groups.ACCOUNT_TYPE + " IS NULL", params);
-
-                    mDb.execSQL(
-                            "DELETE FROM " + Tables.ACCOUNTS +
-                            " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
-                            " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL");
-                }
+            if (accountsChanged) {
+                mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
             }
-
-            mDbHelper.updateAllVisible();
-
-            mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
             mDb.setTransactionSuccessful();
         } finally {
             mDb.endTransaction();
         }
         mAccountWritability.clear();
+        return accountsChanged;
+    }
+
+    public void onPackageChanged(String packageName) {
+        mContactDirectoryManager.onPackageChanged(packageName);
     }
 
     /**
      * Finds all distinct accounts present in the specified table.
      */
-    private void findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts) {
+    private void findValidAccounts(Set<Account> validAccounts) {
         Cursor c = mDb.rawQuery(
                 "SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE +
                 " FROM " + Tables.ACCOUNTS, null);
         try {
             while (c.moveToNext()) {
-                if (c.isNull(0) && c.isNull(1)) {
-                    hasUnassignedContacts[0] = true;
-                } else {
+                if (!c.isNull(0) || !c.isNull(1)) {
                     validAccounts.add(new Account(c.getString(0), c.getString(1)));
                 }
             }
@@ -4195,6 +4350,104 @@
     @Override
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
+        String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY);
+        if (directory == null) {
+            return queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1);
+        } else if (directory.equals("0")) {
+            return queryLocal(uri, projection, selection, selectionArgs, sortOrder,
+                    Directory.DEFAULT);
+        } else if (directory.equals("1")) {
+            return queryLocal(uri, projection, selection, selectionArgs, sortOrder,
+                    Directory.LOCAL_INVISIBLE);
+        }
+
+        DirectoryInfo directoryInfo = getDirectoryAuthority(directory);
+        if (directoryInfo == null) {
+            throw new IllegalArgumentException(
+                    mDbHelper.exceptionMessage("Invalid directory ID", uri));
+        }
+
+        Builder builder = new Uri.Builder();
+        builder.scheme(ContentResolver.SCHEME_CONTENT);
+        builder.authority(directoryInfo.authority);
+        builder.encodedPath(uri.getEncodedPath());
+        if (directoryInfo.accountName != null) {
+            builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName);
+        }
+        if (directoryInfo.accountType != null) {
+            builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType);
+        }
+
+        String limit = getLimit(uri);
+        if (limit != null) {
+            builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, limit);
+        }
+
+        Uri directoryUri = builder.build();
+
+        if (projection == null) {
+            projection = getDefaultProjection(uri);
+        }
+
+        Cursor cursor = getContext().getContentResolver().query(directoryUri, projection, selection,
+                selectionArgs, sortOrder);
+        while (cursor instanceof CursorWrapper) {
+            cursor = ((CursorWrapper)cursor).getWrappedCursor();
+        }
+        return cursor;
+    }
+
+    private static final class DirectoryQuery {
+        public static final String[] COLUMNS = new String[] {
+                Directory._ID,
+                Directory.DIRECTORY_AUTHORITY,
+                Directory.ACCOUNT_NAME,
+                Directory.ACCOUNT_TYPE
+        };
+
+        public static final int DIRECTORY_ID = 0;
+        public static final int AUTHORITY = 1;
+        public static final int ACCOUNT_NAME = 2;
+        public static final int ACCOUNT_TYPE = 3;
+    }
+
+    /**
+     * Reads and caches directory information for the database.
+     */
+    private DirectoryInfo getDirectoryAuthority(String directoryId) {
+        synchronized (mDirectoryCache) {
+            if (!mDirectoryCacheValid) {
+                mDirectoryCache.clear();
+                Cursor cursor = mDb.query(Tables.DIRECTORIES,
+                        DirectoryQuery.COLUMNS,
+                        null, null, null, null, null);
+                try {
+                    while (cursor.moveToNext()) {
+                        DirectoryInfo info = new DirectoryInfo();
+                        String id = cursor.getString(DirectoryQuery.DIRECTORY_ID);
+                        info.authority = cursor.getString(DirectoryQuery.AUTHORITY);
+                        info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
+                        info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
+                        mDirectoryCache.put(id, info);
+                    }
+                } finally {
+                    cursor.close();
+                }
+                mDirectoryCacheValid = true;
+            }
+
+            return mDirectoryCache.get(directoryId);
+        }
+    }
+
+    public void resetDirectoryCache() {
+        synchronized(mDirectoryCache) {
+            mDirectoryCacheValid = false;
+        }
+    }
+
+    public Cursor queryLocal(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder, long directoryId) {
         if (VERBOSE_LOGGING) {
             Log.v(TAG, "query: " + uri);
         }
@@ -4215,6 +4468,7 @@
 
             case CONTACTS: {
                 setTablesAndProjectionMapForContacts(qb, uri, projection);
+                appendLocalDirectorySelectionIfNeeded(qb, directoryId);
                 break;
             }
 
@@ -4234,29 +4488,19 @@
                     throw new IllegalArgumentException(mDbHelper.exceptionMessage(
                             "Missing a lookup key", uri));
                 }
+
                 String lookupKey = pathSegments.get(2);
                 if (segmentCount == 4) {
-                    // TODO: pull this out into a method and generalize to not require contactId
                     long contactId = Long.parseLong(pathSegments.get(3));
                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
                     setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
-                    String[] args;
-                    if (selectionArgs == null) {
-                        args = new String[2];
-                    } else {
-                        args = new String[selectionArgs.length + 2];
-                        System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
-                    }
-                    args[0] = String.valueOf(contactId);
-                    args[1] = Uri.encode(lookupKey);
-                    lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
-                    Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
-                            groupBy, limit);
-                    if (c.getCount() != 0) {
+
+                    Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
+                            projection, selection, selectionArgs, sortOrder, groupBy, limit,
+                            Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey);
+                    if (c != null) {
                         return c;
                     }
-
-                    c.close();
                 }
 
                 setTablesAndProjectionMapForContacts(qb, uri, projection);
@@ -4266,6 +4510,37 @@
                 break;
             }
 
+            case CONTACTS_LOOKUP_DATA:
+            case CONTACTS_LOOKUP_ID_DATA: {
+                List<String> pathSegments = uri.getPathSegments();
+                int segmentCount = pathSegments.size();
+                if (segmentCount < 4) {
+                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+                            "Missing a lookup key", uri));
+                }
+                String lookupKey = pathSegments.get(2);
+                if (segmentCount == 5) {
+                    long contactId = Long.parseLong(pathSegments.get(3));
+                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
+                    setTablesAndProjectionMapForData(lookupQb, uri, projection, false);
+                    lookupQb.appendWhere(" AND ");
+                    Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
+                            projection, selection, selectionArgs, sortOrder, groupBy, limit,
+                            Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey);
+                    if (c != null) {
+                        return c;
+                    }
+
+                    // TODO see if the contact exists but has no data rows (rare)
+                }
+
+                setTablesAndProjectionMapForData(qb, uri, projection, false);
+                selectionArgs = insertSelectionArg(selectionArgs,
+                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
+                qb.appendWhere(" AND " + Data.CONTACT_ID + "=?");
+                break;
+            }
+
             case CONTACTS_AS_VCARD: {
                 // When reading as vCard always use restricted view
                 final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
@@ -4293,6 +4568,7 @@
                     filterParam = uri.getLastPathSegment();
                 }
                 setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam);
+                appendLocalDirectorySelectionIfNeeded(qb, directoryId);
                 break;
             }
 
@@ -4313,8 +4589,10 @@
                 String[] starredProjection = null;
                 String[] frequentProjection = null;
                 if (projection != null) {
-                    starredProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
-                    frequentProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
+                    starredProjection =
+                            appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN);
+                    frequentProjection =
+                            appendProjectionArg(projection, TIMES_CONTACTED_SORT_COLUMN);
                 }
 
                 // Build the first query for starred
@@ -4357,7 +4635,7 @@
                 break;
             }
 
-            case CONTACTS_DATA: {
+            case CONTACTS_ID_DATA: {
                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
@@ -4365,7 +4643,7 @@
                 break;
             }
 
-            case CONTACTS_PHOTO: {
+            case CONTACTS_ID_PHOTO: {
                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
@@ -4374,6 +4652,45 @@
                 break;
             }
 
+            case CONTACTS_ID_ENTITIES: {
+                long contactId = Long.parseLong(uri.getPathSegments().get(1));
+                setTablesAndProjectionMapForEntities(qb, uri, projection);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
+                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
+                break;
+            }
+
+            case CONTACTS_LOOKUP_ENTITIES:
+            case CONTACTS_LOOKUP_ID_ENTITIES: {
+                List<String> pathSegments = uri.getPathSegments();
+                int segmentCount = pathSegments.size();
+                if (segmentCount < 4) {
+                    throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+                            "Missing a lookup key", uri));
+                }
+                String lookupKey = pathSegments.get(2);
+                if (segmentCount == 5) {
+                    long contactId = Long.parseLong(pathSegments.get(3));
+                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
+                    setTablesAndProjectionMapForEntities(lookupQb, uri, projection);
+                    lookupQb.appendWhere(" AND ");
+
+                    Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
+                            projection, selection, selectionArgs, sortOrder, groupBy, limit,
+                            Contacts.Entity.CONTACT_ID, contactId,
+                            Contacts.Entity.LOOKUP_KEY, lookupKey);
+                    if (c != null) {
+                        return c;
+                    }
+                }
+
+                setTablesAndProjectionMapForEntities(qb, uri, projection);
+                selectionArgs = insertSelectionArg(selectionArgs,
+                        String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
+                qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?");
+                break;
+            }
+
             case PHONES: {
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
@@ -4406,18 +4723,17 @@
                         hasCondition = true;
                     }
 
-                    if (isPhoneNumber(filterParam)) {
+                    String number = PhoneNumberUtils.normalizeNumber(filterParam);
+                    if (!TextUtils.isEmpty(number)) {
                         if (orNeeded) {
                             sb.append(" OR ");
                         }
-                        String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam);
-                        String reversed = PhoneNumberUtils.getStrippedReversed(number);
                         sb.append(Data._ID +
-                                " IN (SELECT " + PhoneLookupColumns.DATA_ID
-                                  + " FROM " + Tables.PHONE_LOOKUP
-                                  + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%");
-                        sb.append(reversed);
-                        sb.append("')");
+                                " IN (SELECT DISTINCT " + PhoneLookupColumns.DATA_ID
+                                + " FROM " + Tables.PHONE_LOOKUP
+                                + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
+                        sb.append(number);
+                        sb.append("%')");
                         hasCondition = true;
                     }
 
@@ -4509,7 +4825,7 @@
                 }
                 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
                 if (sortOrder == null) {
-                    sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
+                    sortOrder = EMAIL_FILTER_SORT_ORDER;
                 }
                 break;
             }
@@ -4568,13 +4884,16 @@
                 if (TextUtils.isEmpty(sortOrder)) {
                     // Default the sort order to something reasonable so we get consistent
                     // results when callers don't request an ordering
-                    sortOrder = RawContactsColumns.CONCRETE_ID;
+                    sortOrder = " length(lookup.normalized_number) DESC";
                 }
 
                 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
-                mDbHelper.buildPhoneLookupAndContactQuery(qb, number);
+                String numberE164 =
+                        PhoneNumberUtils.formatNumberToE164(number, getCurrentCountryIso());
+                String normalizedNumber =
+                        PhoneNumberUtils.normalizeNumber(number);
+                mDbHelper.buildPhoneLookupAndContactQuery(qb, normalizedNumber, numberE164);
                 qb.setProjectionMap(sPhoneLookupProjectionMap);
-
                 // Phone lookup cannot be combined with a selection
                 selection = null;
                 selectionArgs = null;
@@ -4623,10 +4942,26 @@
                     maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
                 }
 
+                ArrayList<AggregationSuggestionParameter> parameters = null;
+                List<String> query = uri.getQueryParameters("query");
+                if (query != null && !query.isEmpty()) {
+                    parameters = new ArrayList<AggregationSuggestionParameter>(query.size());
+                    for (String parameter : query) {
+                        int offset = parameter.indexOf(':');
+                        parameters.add(offset == -1
+                                ? new AggregationSuggestionParameter(
+                                        AggregationSuggestions.PARAMETER_MATCH_NAME,
+                                        parameter)
+                                : new AggregationSuggestionParameter(
+                                        parameter.substring(0, offset),
+                                        parameter.substring(offset + 1)));
+                    }
+                }
+
                 setTablesAndProjectionMapForContacts(qb, uri, projection);
 
                 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId,
-                        maxSuggestions, filter);
+                        maxSuggestions, filter, parameters);
             }
 
             case SETTINGS: {
@@ -4696,13 +5031,13 @@
                 break;
 
             case RAW_CONTACT_ENTITIES: {
-                setTablesAndProjectionMapForRawContactsEntities(qb, uri);
+                setTablesAndProjectionMapForRawEntities(qb, uri);
                 break;
             }
 
             case RAW_CONTACT_ENTITY_ID: {
                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
-                setTablesAndProjectionMapForRawContactsEntities(qb, uri);
+                setTablesAndProjectionMapForRawEntities(qb, uri);
                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
                 qb.appendWhere(" AND " + RawContacts._ID + "=?");
                 break;
@@ -4712,6 +5047,25 @@
                 return queryProviderStatus(uri, projection);
             }
 
+            case DIRECTORIES : {
+                qb.setTables(Tables.DIRECTORIES);
+                qb.setProjectionMap(sDirectoryProjectionMap);
+                break;
+            }
+
+            case DIRECTORIES_ID : {
+                long id = ContentUris.parseId(uri);
+                qb.setTables(Tables.DIRECTORIES);
+                qb.setProjectionMap(sDirectoryProjectionMap);
+                selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(id));
+                qb.appendWhere(Directory._ID + "=?");
+                break;
+            }
+
+            case COMPLETE_NAME: {
+                return completeName(uri, projection);
+            }
+
             default:
                 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
                         sortOrder, limit);
@@ -4758,6 +5112,35 @@
         return cursor;
     }
 
+    /**
+     * 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.
+     */
+    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[] args;
+        if (selectionArgs == null) {
+            args = new String[2];
+        } else {
+            args = new String[selectionArgs.length + 2];
+            System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
+        }
+        args[0] = String.valueOf(contactId);
+        args[1] = Uri.encode(lookupKey);
+        lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?");
+        Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
+                groupBy, limit);
+        if (c.getCount() != 0) {
+            return c;
+        }
+
+        c.close();
+        return null;
+    }
 
     private static final class AddressBookIndexQuery {
         public static final String LETTER = "letter";
@@ -5176,25 +5559,30 @@
                     ") AS " + SearchSnippetColumns.SNIPPET_MIMETYPE);
         }
 
-        sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS +
-                " WHERE " + DataColumns.CONCRETE_ID +
-                " IN (");
+        sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS + " WHERE ");
 
-        // Construct a query that gives us exactly one data _id per matching contact.
-        // MIN stands in for ANY in this context.
-        sb.append(
-                "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" +
-                " FROM " + Tables.NAME_LOOKUP +
-                " JOIN " + Tables.RAW_CONTACTS +
-                " ON (" + RawContactsColumns.CONCRETE_ID
-                        + "=" + Tables.NAME_LOOKUP + "." + NameLookupColumns.RAW_CONTACT_ID + ")" +
-                " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '");
-        sb.append(NameNormalizer.normalize(filter));
-        sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
-                    " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" +
-                " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID);
+        if (!TextUtils.isEmpty(filter)) {
+            sb.append(DataColumns.CONCRETE_ID + " IN (");
 
-        sb.append(")) ON (" + Contacts._ID + "=snippet_contact_id)");
+            // Construct a query that gives us exactly one data _id per matching contact.
+            // MIN stands in for ANY in this context.
+            sb.append(
+                    "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" +
+                    " FROM " + Tables.NAME_LOOKUP +
+                    " JOIN " + Tables.RAW_CONTACTS +
+                    " ON (" + RawContactsColumns.CONCRETE_ID
+                            + "=" + Tables.NAME_LOOKUP + "." + NameLookupColumns.RAW_CONTACT_ID + ")" +
+                    " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '");
+            sb.append(NameNormalizer.normalize(filter));
+            sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
+                        " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" +
+                    " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID +
+                    ")");
+        } else {
+            sb.append("0");     // Empty filter - return an empty set
+        }
+
+        sb.append(") ON (" + Contacts._ID + "=snippet_contact_id)");
 
         qb.setTables(sb.toString());
         qb.setProjectionMap(sContactsProjectionWithSnippetMap);
@@ -5208,22 +5596,8 @@
             excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
         }
         sb.append(mDbHelper.getContactView(excludeRestrictedData));
-        if (mDbHelper.isInProjection(projection,
-                Contacts.CONTACT_PRESENCE)) {
-            sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
-                    " ON (" + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ")");
-        }
-        if (mDbHelper.isInProjection(projection,
-                Contacts.CONTACT_STATUS,
-                Contacts.CONTACT_STATUS_RES_PACKAGE,
-                Contacts.CONTACT_STATUS_ICON,
-                Contacts.CONTACT_STATUS_LABEL,
-                Contacts.CONTACT_STATUS_TIMESTAMP)) {
-            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
-                    + ContactsStatusUpdatesColumns.ALIAS +
-                    " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
-                            + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
-        }
+        appendContactPresenceJoin(sb, projection, Contacts._ID);
+        appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
     }
 
     private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
@@ -5240,80 +5614,29 @@
         appendAccountFromParameter(qb, uri);
     }
 
-    private void setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri) {
-        // Note: currently, "export only" equals to "restricted", but may not in the future.
-        boolean excludeRestrictedData = readBooleanQueryParameter(uri,
-                Data.FOR_EXPORT_ONLY, false);
-
-        String requestingPackage = getQueryParameter(uri,
-                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
-        if (requestingPackage != null) {
-            excludeRestrictedData = excludeRestrictedData
-                    || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
-        }
-        qb.setTables(mDbHelper.getContactEntitiesView(excludeRestrictedData));
-        qb.setProjectionMap(sRawContactsEntityProjectionMap);
+    private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) {
+        qb.setTables(mDbHelper.getRawEntitiesView(shouldExcludeRestrictedData(uri)));
+        qb.setProjectionMap(sRawEntityProjectionMap);
         appendAccountFromParameter(qb, uri);
     }
 
     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
             String[] projection, boolean distinct) {
         StringBuilder sb = new StringBuilder();
-        // Note: currently, "export only" equals to "restricted", but may not in the future.
-        boolean excludeRestrictedData = readBooleanQueryParameter(uri,
-                Data.FOR_EXPORT_ONLY, false);
-
-        String requestingPackage = getQueryParameter(uri,
-                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
-        if (requestingPackage != null) {
-            excludeRestrictedData = excludeRestrictedData
-                    || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
-        }
-
-        sb.append(mDbHelper.getDataView(excludeRestrictedData));
+        sb.append(mDbHelper.getDataView(shouldExcludeRestrictedData(uri)));
         sb.append(" data");
 
-        // Include aggregated presence when requested
-        if (mDbHelper.isInProjection(projection, Data.CONTACT_PRESENCE)) {
-            sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
-                    " ON (" + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + "="
-                    + RawContacts.CONTACT_ID + ")");
-        }
-
-        // Include aggregated status updates when requested
-        if (mDbHelper.isInProjection(projection,
-                Data.CONTACT_STATUS,
-                Data.CONTACT_STATUS_RES_PACKAGE,
-                Data.CONTACT_STATUS_ICON,
-                Data.CONTACT_STATUS_LABEL,
-                Data.CONTACT_STATUS_TIMESTAMP)) {
-            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
-                    + ContactsStatusUpdatesColumns.ALIAS +
-                    " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
-                            + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
-        }
-
-        // Include individual presence when requested
-        if (mDbHelper.isInProjection(projection, Data.PRESENCE)) {
-            sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
-                    " ON (" + StatusUpdates.DATA_ID + "="
-                    + DataColumns.CONCRETE_ID + ")");
-        }
-
-        // Include individual status updates when requested
-        if (mDbHelper.isInProjection(projection,
-                Data.STATUS,
-                Data.STATUS_RES_PACKAGE,
-                Data.STATUS_ICON,
-                Data.STATUS_LABEL,
-                Data.STATUS_TIMESTAMP)) {
-            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
-                    " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
-                            + DataColumns.CONCRETE_ID + ")");
-        }
+        appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID);
+        appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
+        appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
+        appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
 
         qb.setTables(sb.toString());
-        qb.setProjectionMap(distinct ? sDistinctDataProjectionMap : sDataProjectionMap);
+
+        boolean useDistinct = distinct
+                || !mDbHelper.isInProjection(projection, DISTINCT_DATA_PROHIBITING_COLUMNS);
+        qb.setDistinct(useDistinct);
+        qb.setProjectionMap(useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap);
         appendAccountFromParameter(qb, uri);
     }
 
@@ -5322,13 +5645,46 @@
         StringBuilder sb = new StringBuilder();
         sb.append(mDbHelper.getDataView());
         sb.append(" data");
+        appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
+        appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
 
-        if (mDbHelper.isInProjection(projection, StatusUpdates.PRESENCE)) {
-            sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
-                    " ON(" + Tables.PRESENCE + "." + StatusUpdates.DATA_ID
-                    + "=" + DataColumns.CONCRETE_ID + ")");
+        qb.setTables(sb.toString());
+        qb.setProjectionMap(sStatusUpdatesProjectionMap);
+    }
+
+    private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri,
+            String[] projection) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(mDbHelper.getEntitiesView(shouldExcludeRestrictedData(uri)));
+        sb.append(" data");
+
+        appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID);
+        appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
+        appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID);
+        appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID);
+
+        qb.setTables(sb.toString());
+        qb.setProjectionMap(sEntityProjectionMap);
+        appendAccountFromParameter(qb, uri);
+    }
+
+    private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection,
+            String lastStatusUpdateIdColumn) {
+        if (mDbHelper.isInProjection(projection,
+                Contacts.CONTACT_STATUS,
+                Contacts.CONTACT_STATUS_RES_PACKAGE,
+                Contacts.CONTACT_STATUS_ICON,
+                Contacts.CONTACT_STATUS_LABEL,
+                Contacts.CONTACT_STATUS_TIMESTAMP)) {
+            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
+                    + ContactsStatusUpdatesColumns.ALIAS +
+                    " ON (" + lastStatusUpdateIdColumn + "="
+                            + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
         }
+    }
 
+    private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection,
+            String dataIdColumn) {
         if (mDbHelper.isInProjection(projection,
                 StatusUpdates.STATUS,
                 StatusUpdates.STATUS_RES_PACKAGE,
@@ -5336,11 +5692,52 @@
                 StatusUpdates.STATUS_LABEL,
                 StatusUpdates.STATUS_TIMESTAMP)) {
             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
-                    " ON(" + Tables.STATUS_UPDATES + "." + StatusUpdatesColumns.DATA_ID
-                    + "=" + DataColumns.CONCRETE_ID + ")");
+                    " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
+                            + dataIdColumn + ")");
         }
-        qb.setTables(sb.toString());
-        qb.setProjectionMap(sStatusUpdatesProjectionMap);
+    }
+
+    private void appendContactPresenceJoin(StringBuilder sb, String[] projection,
+            String contactIdColumn) {
+        if (mDbHelper.isInProjection(projection,
+                Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) {
+            sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
+                    " ON (" + contactIdColumn + " = "
+                            + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")");
+        }
+    }
+
+    private void appendDataPresenceJoin(StringBuilder sb, String[] projection,
+            String dataIdColumn) {
+        if (mDbHelper.isInProjection(projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) {
+            sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
+                    " ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")");
+        }
+    }
+
+    private void appendLocalDirectorySelectionIfNeeded(SQLiteQueryBuilder qb, long directoryId) {
+        if (directoryId == Directory.DEFAULT) {
+            qb.appendWhere(Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY);
+        } else if (directoryId == Directory.LOCAL_INVISIBLE){
+            qb.appendWhere(Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY);
+        }
+    }
+
+    private boolean shouldExcludeRestrictedData(Uri uri) {
+        // Note: currently, "export only" equals to "restricted", but may not in the future.
+        boolean excludeRestrictedData = readBooleanQueryParameter(uri,
+                Data.FOR_EXPORT_ONLY, false);
+        if (excludeRestrictedData) {
+            return true;
+        }
+
+        String requestingPackage = getQueryParameter(uri,
+                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
+        if (requestingPackage != null) {
+            return !mDbHelper.hasAccessToRestrictedData(requestingPackage);
+        }
+
+        return false;
     }
 
     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
@@ -5404,7 +5801,7 @@
      *         the parameter is not set, or is set to an invalid value.
      */
     private String getLimit(Uri uri) {
-        String limitParam = getQueryParameter(uri, "limit");
+        String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY);
         if (limitParam == null) {
             return null;
         }
@@ -5422,36 +5819,6 @@
         }
     }
 
-    /**
-     * Returns true if all the characters are meaningful as digits
-     * in a phone number -- letters, digits, and a few punctuation marks.
-     */
-    private boolean isPhoneNumber(CharSequence cons) {
-        int len = cons.length();
-
-        for (int i = 0; i < len; i++) {
-            char c = cons.charAt(i);
-
-            if ((c >= '0') && (c <= '9')) {
-                continue;
-            }
-            if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+')
-                    || (c == '#') || (c == '*')) {
-                continue;
-            }
-            if ((c >= 'A') && (c <= 'Z')) {
-                continue;
-            }
-            if ((c >= 'a') && (c <= 'z')) {
-                continue;
-            }
-
-            return false;
-        }
-
-        return true;
-    }
-
     String getContactsRestrictions() {
         if (mDbHelper.hasAccessToRestrictedData()) {
             return "1";
@@ -5470,17 +5837,17 @@
     }
 
     @Override
-    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
         int match = sUriMatcher.match(uri);
         switch (match) {
-            case CONTACTS_PHOTO: {
-                return openPhotoAssetFile(uri, mode,
+            case CONTACTS_ID_PHOTO: {
+                return openPhotoFile(uri, mode,
                         Data._ID + "=" + Contacts.PHOTO_ID + " AND " + RawContacts.CONTACT_ID + "=?",
                         new String[]{uri.getPathSegments().get(1)});
             }
 
             case DATA_ID: {
-                return openPhotoAssetFile(uri, mode,
+                return openPhotoFile(uri, mode,
                         Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'",
                         new String[]{uri.getPathSegments().get(1)});
             }
@@ -5495,7 +5862,7 @@
                 // then pipe into MemoryFile once the exact size is known.
                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
                 outputRawContactsAsVCard(localStream, selection, mSelectionArgs1);
-                return buildAssetFileDescriptor(localStream);
+                return buildFileDescriptor(localStream);
             }
 
             case CONTACTS_AS_MULTI_VCARD: {
@@ -5522,7 +5889,7 @@
                 // then pipe into MemoryFile once the exact size is known.
                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
                 outputRawContactsAsVCard(localStream, selection, null);
-                return buildAssetFileDescriptor(localStream);
+                return buildFileDescriptor(localStream);
             }
 
             default:
@@ -5531,7 +5898,7 @@
         }
     }
 
-    private AssetFileDescriptor openPhotoAssetFile(Uri uri, String mode, String selection,
+    private ParcelFileDescriptor openPhotoFile(Uri uri, String mode, String selection,
             String[] selectionArgs)
             throws FileNotFoundException {
         if (!"r".equals(mode)) {
@@ -5543,33 +5910,26 @@
                 "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
                 " WHERE " + selection;
         SQLiteDatabase db = mDbHelper.getReadableDatabase();
-        return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql,
-                selectionArgs);
+        return DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs);
     }
 
     private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
 
     /**
-     * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the
+     * Returns a {@link ParcelFileDescriptor} backed by the
      * contents of the given {@link ByteArrayOutputStream}.
      */
-    private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
-        AssetFileDescriptor fd = null;
+    private ParcelFileDescriptor buildFileDescriptor(ByteArrayOutputStream stream) {
         try {
             stream.flush();
 
             final byte[] byteData = stream.toByteArray();
-            final int size = byteData.length;
 
-            final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size);
-            memoryFile.writeBytes(byteData, 0, 0, size);
-            memoryFile.deactivate();
-
-            fd = AssetFileDescriptor.fromMemoryFile(memoryFile);
+            return ParcelFileDescriptor.fromData(byteData, CONTACT_MEMORY_FILE_NAME);
         } catch (IOException e) {
-            Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString());
+            Log.w(TAG, "Problem writing stream into an ParcelFileDescriptor: " + e.toString());
+            return null;
         }
-        return fd;
     }
 
     /**
@@ -5611,6 +5971,8 @@
             case CONTACTS_AS_VCARD:
             case CONTACTS_AS_MULTI_VCARD:
                 return Contacts.CONTENT_VCARD_TYPE;
+            case CONTACTS_ID_PHOTO:
+                return "image/png";
             case RAW_CONTACTS:
                 return RawContacts.CONTENT_TYPE;
             case RAW_CONTACTS_ID:
@@ -5645,12 +6007,64 @@
                 return SearchManager.SUGGEST_MIME_TYPE;
             case SEARCH_SHORTCUT:
                 return SearchManager.SHORTCUT_MIME_TYPE;
-
+            case DIRECTORIES:
+                return Directory.CONTENT_TYPE;
+            case DIRECTORIES_ID:
+                return Directory.CONTENT_ITEM_TYPE;
             default:
                 return mLegacyApiSupport.getType(uri);
         }
     }
 
+    public String[] getDefaultProjection(Uri uri) {
+        final int match = sUriMatcher.match(uri);
+        switch (match) {
+            case CONTACTS:
+            case CONTACTS_LOOKUP:
+            case CONTACTS_ID:
+            case CONTACTS_LOOKUP_ID:
+            case AGGREGATION_SUGGESTIONS:
+                return sContactsProjectionMap.getColumnNames();
+
+            case CONTACTS_ID_ENTITIES:
+                return sEntityProjectionMap.getColumnNames();
+
+            case CONTACTS_AS_VCARD:
+            case CONTACTS_AS_MULTI_VCARD:
+                return sContactsVCardProjectionMap.getColumnNames();
+
+            case RAW_CONTACTS:
+            case RAW_CONTACTS_ID:
+                return sRawContactsProjectionMap.getColumnNames();
+
+            case DATA_ID:
+            case PHONES:
+            case PHONES_ID:
+            case EMAILS:
+            case EMAILS_ID:
+            case POSTALS:
+            case POSTALS_ID:
+                return sDataProjectionMap.getColumnNames();
+
+            case PHONE_LOOKUP:
+                return sPhoneLookupProjectionMap.getColumnNames();
+
+            case AGGREGATION_EXCEPTIONS:
+            case AGGREGATION_EXCEPTION_ID:
+                return sAggregationExceptionsProjectionMap.getColumnNames();
+
+            case SETTINGS:
+                return sSettingsProjectionMap.getColumnNames();
+
+            case DIRECTORIES:
+            case DIRECTORIES_ID:
+                return sDirectoryProjectionMap.getColumnNames();
+
+            default:
+                return null;
+        }
+    }
+
     private void setDisplayName(long rawContactId, int displayNameSource,
             String displayNamePrimary, String displayNameAlternative, String phoneticName,
             int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) {
@@ -5861,6 +6275,54 @@
     }
 
     /**
+     * Takes components of a name from the query parameters and returns a cursor with those
+     * components as well as all missing components.  There is no database activity involved
+     * in this so the call can be made on the UI thread.
+     */
+    private Cursor completeName(Uri uri, String[] projection) {
+        if (projection == null) {
+            projection = sDataProjectionMap.getColumnNames();
+        }
+
+        ContentValues values = new ContentValues();
+        StructuredNameRowHandler handler =
+                (StructuredNameRowHandler) getDataRowHandler(StructuredName.CONTENT_ITEM_TYPE);
+
+        copyQueryParamsToContentValues(values, uri,
+                StructuredName.DISPLAY_NAME,
+                StructuredName.PREFIX,
+                StructuredName.GIVEN_NAME,
+                StructuredName.MIDDLE_NAME,
+                StructuredName.FAMILY_NAME,
+                StructuredName.SUFFIX,
+                StructuredName.PHONETIC_NAME,
+                StructuredName.PHONETIC_FAMILY_NAME,
+                StructuredName.PHONETIC_MIDDLE_NAME,
+                StructuredName.PHONETIC_GIVEN_NAME
+        );
+
+        handler.fixStructuredNameComponents(values, values);
+
+        MatrixCursor cursor = new MatrixCursor(projection);
+        Object[] row = new Object[projection.length];
+        for (int i = 0; i < projection.length; i++) {
+            row[i] = values.get(projection[i]);
+        }
+        cursor.addRow(row);
+        return cursor;
+    }
+
+    private void copyQueryParamsToContentValues(ContentValues values, Uri uri, String... columns) {
+        for (String column : columns) {
+            String param = uri.getQueryParameter(column);
+            if (param != null) {
+                values.put(column, param);
+            }
+        }
+    }
+
+
+    /**
      * Inserts an argument at the beginning of the selection arg list.
      */
     private String[] insertSelectionArg(String[] selectionArgs, String arg) {
@@ -5934,6 +6396,7 @@
         return writable;
     }
 
+
     /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
             boolean defaultValue) {
 
diff --git a/src/com/android/providers/contacts/CountryMonitor.java b/src/com/android/providers/contacts/CountryMonitor.java
new file mode 100644
index 0000000..c370b38
--- /dev/null
+++ b/src/com/android/providers/contacts/CountryMonitor.java
@@ -0,0 +1,67 @@
+/*
+ * 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 android.content.Context;
+import android.location.Country;
+import android.location.CountryDetector;
+import android.location.CountryListener;
+import android.os.Looper;
+
+/**
+ * This class monitors the change of country.
+ * <p>
+ * {@link #getCountryIso()} is used to get the ISO 3166-1 two letters country
+ * code of current country.
+ */
+public class CountryMonitor {
+    private static CountryMonitor sSingleton;
+    private String mCurrentCountryIso;
+    private Context mContext;
+
+    public synchronized static CountryMonitor getInstance(Context context) {
+        if (sSingleton == null) {
+            sSingleton = new CountryMonitor(context);
+        }
+        return sSingleton;
+    }
+
+    private CountryMonitor(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Get the current country code
+     *
+     * @return the ISO 3166-1 two letters country code of current country.
+     */
+    public synchronized String getCountryIso() {
+        if (mCurrentCountryIso == null) {
+            final CountryDetector countryDetector =
+                    (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR);
+            mCurrentCountryIso = countryDetector.detectCountry().getCountryIso();
+            countryDetector.addCountryListener(new CountryListener() {
+                public void onCountryDetected(Country country) {
+                    synchronized (sSingleton) {
+                        mCurrentCountryIso = country.getCountryIso();
+                    }
+                }
+            }, Looper.getMainLooper());
+        }
+        return mCurrentCountryIso;
+    }
+}
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index d890310..96bd39c 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -27,6 +27,7 @@
 import android.content.ContentUris;
 import android.content.res.Resources;
 import android.database.Cursor;
+import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
 import android.provider.Contacts.Intents;
@@ -74,7 +75,8 @@
     private interface SearchSuggestionQuery {
         public static final String TABLE = "data "
                 + " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
-                + " JOIN contacts ON (raw_contacts.contact_id = contacts._id)"
+                + " JOIN visible_contacts on (raw_contacts.contact_id = visible_contacts._id) "
+                + " JOIN contacts ON (visible_contacts._id = contacts._id) "
                 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON ("
                 +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
 
@@ -342,18 +344,6 @@
         appendMimeTypeFilter(sb);
         sb.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN ");
         mContactsProvider.appendRawContactsByFilterAsNestedQuery(sb, searchClause);
-
-        /*
-         *  Prepending "+" to the IN_VISIBLE_GROUP column disables the index on the
-         *  that column.  The logic is this:  let's say we have 10,000 contacts
-         *  of which 500 are visible.  The first letter we type narrows this down
-         *  to 10,000/26 = 384, which is already less than 500 that we would get
-         *  from the IN_VISIBLE_GROUP index.  Typing the second letter will narrow
-         *  the search down to 10,000/26/26 = 14 contacts. And a lot of people
-         *  will have more that 5% of their contacts visible, while the alphabet
-         *  will always have 26 letters.
-         */
-        sb.append(" AND " + "+" + Contacts.IN_VISIBLE_GROUP + "=1");
         String selection = sb.toString();
 
         return buildCursorForSearchSuggestions(db, selection, null, limit);
diff --git a/src/com/android/providers/contacts/LegacyContactImporter.java b/src/com/android/providers/contacts/LegacyContactImporter.java
index 2634d44..ae89cc8 100644
--- a/src/com/android/providers/contacts/LegacyContactImporter.java
+++ b/src/com/android/providers/contacts/LegacyContactImporter.java
@@ -399,9 +399,8 @@
                 RawContacts.ACCOUNT_NAME + "," +
                 RawContacts.ACCOUNT_TYPE + "," +
                 RawContacts.SOURCE_ID + "," +
-                RawContactsColumns.DISPLAY_NAME + "," +
-                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP +
-         ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
+                RawContactsColumns.DISPLAY_NAME +
+         ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
 
         int ID = 1;
         int CONTACT_ID = 2;
@@ -417,7 +416,6 @@
         int ACCOUNT_TYPE = 12;
         int SOURCE_ID = 13;
         int DISPLAY_NAME = 14;
-        int CONTACT_IN_VISIBLE_GROUP = 15;
     }
 
     private interface ContactsInsert {
@@ -428,9 +426,8 @@
                 Contacts.SEND_TO_VOICEMAIL + "," +
                 Contacts.STARRED + "," +
                 Contacts.TIMES_CONTACTED + "," +
-                Contacts.NAME_RAW_CONTACT_ID + "," +
-                Contacts.IN_VISIBLE_GROUP +
-         ") VALUES (?,?,?,?,?,?,?,?)";
+                Contacts.NAME_RAW_CONTACT_ID +
+         ") VALUES (?,?,?,?,?,?,?)";
 
         int ID = 1;
         int CUSTOM_RINGTONE = 2;
@@ -439,7 +436,6 @@
         int STARRED = 5;
         int TIMES_CONTACTED = 6;
         int NAME_RAW_CONTACT_ID = 7;
-        int IN_VISIBLE_GROUP = 8;
     }
 
     private interface StructuredNameInsert {
@@ -555,7 +551,6 @@
                 c.getString(PeopleQuery._SYNC_LOCAL_ID));
         bindString(insert, RawContactsInsert.DISPLAY_NAME,
                 c.getString(PeopleQuery.NAME));
-        insert.bindLong(RawContactsInsert.CONTACT_IN_VISIBLE_GROUP, 1);
 
         String account = c.getString(PeopleQuery._SYNC_ACCOUNT);
         if (!TextUtils.isEmpty(account)) {
@@ -585,7 +580,6 @@
         insert.bindLong(ContactsInsert.TIMES_CONTACTED,
                 c.getLong(PeopleQuery.TIMES_CONTACTED));
         insert.bindLong(ContactsInsert.NAME_RAW_CONTACT_ID, id);
-        insert.bindLong(ContactsInsert.IN_VISIBLE_GROUP, 1);
 
         insert(insert);
     }
diff --git a/src/com/android/providers/contacts/LocaleChangeReceiver.java b/src/com/android/providers/contacts/LocaleChangeReceiver.java
new file mode 100644
index 0000000..290b373
--- /dev/null
+++ b/src/com/android/providers/contacts/LocaleChangeReceiver.java
@@ -0,0 +1,41 @@
+/*
+ * 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 android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.provider.ContactsContract;
+
+/**
+ * Locale change intent receiver that invokes {@link ContactsProvider2#onLocaleChanged} to update
+ * the database for the new locale.
+ */
+public class LocaleChangeReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        IContentProvider iprovider =
+            context.getContentResolver().acquireProvider(ContactsContract.AUTHORITY);
+        ContentProvider provider = ContentProvider.coerceToLocalContentProvider(iprovider);
+        if (provider instanceof ContactsProvider2) {
+            ((ContactsProvider2)provider).onLocaleChanged();
+        }
+    }
+}
diff --git a/src/com/android/providers/contacts/NameLookupBuilder.java b/src/com/android/providers/contacts/NameLookupBuilder.java
index 00416c9..46ce481 100644
--- a/src/com/android/providers/contacts/NameLookupBuilder.java
+++ b/src/com/android/providers/contacts/NameLookupBuilder.java
@@ -36,7 +36,7 @@
     private StringBuilder mStringBuilder = new StringBuilder();
     private String[] mNames = new String[NameSplitter.MAX_TOKENS];
 
-    private static int[] KOREAN_JAUM_CONVERT_MAP = {
+    private static final int[] KOREAN_JAUM_CONVERT_MAP = {
         // JAUM in Hangul Compatibility Jamo area 0x3131 ~ 0x314E to
         // in Hangul Jamo area 0x1100 ~ 0x1112
         0x1100, // 0x3131 HANGUL LETTER KIYEOK
@@ -70,8 +70,6 @@
         0x1111, // 0x314D HANGUL LETTER PHIEUPH
         0x1112  // 0x314E HANGUL LETTER HIEUH
     };
-    private static int KOREAN_JAUM_CONVERT_MAP_COUNT = 30;
-
 
     public NameLookupBuilder(NameSplitter splitter) {
         mSplitter = splitter;
@@ -139,13 +137,23 @@
         insertNameVariants(rawContactId, dataId, 0, tokenCount, !tooManyTokens, true);
         insertNicknamePermutations(rawContactId, dataId, 0, tokenCount);
         insertNameShorthandLookup(rawContactId, dataId, name, fullNameStyle);
-        insertLocaleBasedSpecificLookup(rawContactId, dataId, name, fullNameStyle);
+        insertNameLookupForLocaleBasedName(rawContactId, dataId, name, fullNameStyle);
     }
 
-    private void insertLocaleBasedSpecificLookup(long rawContactId, long dataId, String name,
-            int fullNameStyle) {
+    /**
+     * Insert more name indexes according to locale specifies.
+     */
+    private void insertNameLookupForLocaleBasedName(long rawContactId, long dataId,
+            String fullName, int fullNameStyle) {
         if (fullNameStyle == FullNameStyle.KOREAN) {
-            insertKoreanNameConsonantsLookup(rawContactId, dataId, name);
+            NameSplitter.Name name = new NameSplitter.Name();
+            mSplitter.split(name, fullName, fullNameStyle);
+            if (name.givenNames != null) {
+                insertNameLookup(rawContactId, dataId, NameLookupType.NAME_SHORTHAND,
+                        normalizeName(name.givenNames));
+                insertKoreanNameConsonantsLookup(rawContactId, dataId, name.givenNames);
+            }
+            insertKoreanNameConsonantsLookup(rawContactId, dataId, fullName);
         }
     }
 
@@ -161,8 +169,8 @@
         mStringBuilder.setLength(0);
         do {
             character = name.codePointAt(position++);
-            if (character == 0x20) {
-                // Skip spaces.
+            if ((character == 0x20) || (character == 0x2c) || (character == 0x2E)) {
+                // Skip spaces, commas and periods.
                 continue;
             }
             // Exclude characters that are not in Korean leading consonants area
@@ -181,7 +189,7 @@
             } else if (character >= 0x3131) {
                 // Hangul Compatibility Jamo area 0x3131 ~ 0x314E :
                 // Convert to Hangul Jamo area 0x1100 ~ 0x1112
-                if (character - 0x3131 >= KOREAN_JAUM_CONVERT_MAP_COUNT) {
+                if (character - 0x3131 >= KOREAN_JAUM_CONVERT_MAP.length) {
                     // This is not lead-consonant
                     break;
                 }
diff --git a/src/com/android/providers/contacts/NameNormalizer.java b/src/com/android/providers/contacts/NameNormalizer.java
index 6dfe8bd..2ac3865 100644
--- a/src/com/android/providers/contacts/NameNormalizer.java
+++ b/src/com/android/providers/contacts/NameNormalizer.java
@@ -15,11 +15,10 @@
  */
 package com.android.providers.contacts;
 
-import com.ibm.icu4jni.text.CollationAttribute;
-import com.ibm.icu4jni.text.CollationKey; // TODO: java.text.CollationKey post-froyo
-import com.ibm.icu4jni.text.Collator;
-import com.ibm.icu4jni.text.RuleBasedCollator;
 import java.util.Locale;
+import java.text.Collator;
+import java.text.CollationKey;
+import java.text.RuleBasedCollator;
 
 /**
  * Converts a name to a normalized form by removing all non-letter characters and normalizing
@@ -37,14 +36,12 @@
     private static final RuleBasedCollator sComplexityCollator;
     static {
         sComplexityCollator = (RuleBasedCollator)Collator.getInstance(Locale.getDefault());
-        sComplexityCollator.setStrength(Collator.TERTIARY);
-        sComplexityCollator.setAttribute(CollationAttribute.CASE_FIRST,
-                CollationAttribute.VALUE_LOWER_FIRST);
+        sComplexityCollator.setStrength(Collator.SECONDARY);
     }
 
     /**
      * Converts the supplied name to a string that can be used to perform approximate matching
-     * of names.  It ignores non-letter characters and removes accents.
+     * of names.  It ignores non-letter, non-digit characters, and removes accents.
      */
     public static String normalize(String name) {
         CollationKey key = sCompressingCollator.getCollationKey(lettersAndDigitsOnly(name));
@@ -56,17 +53,24 @@
      * of mixed case characters, accents and, if all else is equal, length.
      */
     public static int compareComplexity(String name1, String name2) {
-        int diff = sComplexityCollator.compare(lettersAndDigitsOnly(name1),
-                lettersAndDigitsOnly(name2));
+        String clean1 = lettersAndDigitsOnly(name1);
+        String clean2 = lettersAndDigitsOnly(name2);
+        int diff = sComplexityCollator.compare(clean1, clean2);
         if (diff != 0) {
             return diff;
         }
-
+        // compareTo sorts uppercase first. We know that there are no non-case
+        // differences from the above test, so we can negate here to get the
+        // lowercase-first comparison we really want...
+        diff = -clean1.compareTo(clean2);
+        if (diff != 0) {
+            return diff;
+        }
         return name1.length() - name2.length();
     }
 
     /**
-     * Returns a string containing just the letters from the original string.
+     * Returns a string containing just the letters and digits from the original string.
      */
     private static String lettersAndDigitsOnly(String name) {
         char[] letters = name.toCharArray();
diff --git a/src/com/android/providers/contacts/NameSplitter.java b/src/com/android/providers/contacts/NameSplitter.java
index 3f9bc86..642bb8f 100644
--- a/src/com/android/providers/contacts/NameSplitter.java
+++ b/src/com/android/providers/contacts/NameSplitter.java
@@ -15,9 +15,6 @@
  */
 package com.android.providers.contacts;
 
-import com.android.internal.util.HanziToPinyin;
-import com.android.internal.util.HanziToPinyin.Token;
-
 import android.content.ContentValues;
 import android.provider.ContactsContract.FullNameStyle;
 import android.provider.ContactsContract.PhoneticNameStyle;
@@ -25,7 +22,6 @@
 import android.text.TextUtils;
 
 import java.lang.Character.UnicodeBlock;
-import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.StringTokenizer;
@@ -63,6 +59,26 @@
     private final Locale mLocale;
     private final String mLanguage;
 
+    /**
+     * Two-Chracter long Korean family names.
+     * http://ko.wikipedia.org/wiki/%ED%95%9C%EA%B5%AD%EC%9D%98_%EB%B3%B5%EC%84%B1
+     */
+    private static final String[] KOREAN_TWO_CHARCTER_FAMILY_NAMES = {
+        "\uAC15\uC804", // Gang Jeon
+        "\uB0A8\uAD81", // Nam Goong
+        "\uB3C5\uACE0", // Dok Go
+        "\uB3D9\uBC29", // Dong Bang
+        "\uB9DD\uC808", // Mang Jeol
+        "\uC0AC\uACF5", // Sa Gong
+        "\uC11C\uBB38", // Seo Moon
+        "\uC120\uC6B0", // Seon Woo
+        "\uC18C\uBD09", // So Bong
+        "\uC5B4\uAE08", // Uh Geum
+        "\uC7A5\uACE1", // Jang Gok
+        "\uC81C\uAC08", // Je Gal
+        "\uD669\uBCF4"  // Hwang Bo
+    };
+
     public static class Name {
         public String prefix;
         public String givenNames;
@@ -335,6 +351,18 @@
             fullNameStyle = getAdjustedFullNameStyle(fullNameStyle);
         }
 
+        split(name, fullName, fullNameStyle);
+    }
+
+    /**
+     * Parses a full name and returns parsed components in the Name object
+     * with a given fullNameStyle.
+     */
+    public void split(Name name, String fullName, int fullNameStyle) {
+        if (fullName == null) {
+            return;
+        }
+
         name.fullNameStyle = fullNameStyle;
 
         switch (fullNameStyle) {
@@ -343,8 +371,11 @@
                 break;
 
             case FullNameStyle.JAPANESE:
+                splitJapaneseName(name, fullName);
+                break;
+
             case FullNameStyle.KOREAN:
-                splitJapaneseOrKoreanName(name, fullName);
+                splitKoreanName(name, fullName);
                 break;
 
             default:
@@ -427,7 +458,7 @@
      *   [family name] given name(s)
      * </pre>
      */
-    private void splitJapaneseOrKoreanName(Name name, String fullName) {
+    private void splitJapaneseName(Name name, String fullName) {
         StringTokenizer tokenizer = new StringTokenizer(fullName);
         while (tokenizer.hasMoreTokens()) {
             String token = tokenizer.nextToken();
@@ -443,6 +474,47 @@
     }
 
     /**
+     * Splits a full name composed according to the Korean tradition:
+     * <pre>
+     *   [family name] given name(s)
+     * </pre>
+     */
+    private void splitKoreanName(Name name, String fullName) {
+        StringTokenizer tokenizer = new StringTokenizer(fullName);
+        if (tokenizer.countTokens() > 1) {
+            // Each name can be identified by separators.
+            while (tokenizer.hasMoreTokens()) {
+                String token = tokenizer.nextToken();
+                if (name.givenNames == null) {
+                    name.givenNames = token;
+                } else if (name.familyName == null) {
+                    name.familyName = name.givenNames;
+                    name.givenNames = token;
+                } else {
+                    name.givenNames += " " + token;
+                }
+            }
+        } else {
+            // There is no separator. Try to guess family name.
+            // The length of most family names is 1.
+            int familyNameLength = 1;
+
+            // Compare with 2-length family names.
+            for (String twoLengthFamilyName : KOREAN_TWO_CHARCTER_FAMILY_NAMES) {
+                if (fullName.startsWith(twoLengthFamilyName)) {
+                    familyNameLength = 2;
+                    break;
+                }
+            }
+
+            name.familyName = fullName.substring(0, familyNameLength);
+            if (fullName.length() > familyNameLength) {
+                name.givenNames = fullName.substring(familyNameLength);
+            }
+        }
+    }
+
+    /**
      * Concatenates components of a name according to the rules dictated by the name style.
      *
      * @param givenNameFirst is ignored for CJK display name styles
diff --git a/src/com/android/providers/contacts/PackageIntentReceiver.java b/src/com/android/providers/contacts/PackageIntentReceiver.java
new file mode 100644
index 0000000..b65f469
--- /dev/null
+++ b/src/com/android/providers/contacts/PackageIntentReceiver.java
@@ -0,0 +1,44 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+/**
+ * Package intent receiver that invokes {@link ContactsProvider2#onPackageChanged} to update
+ * the contact directory list.
+ */
+public class PackageIntentReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Uri packageUri = intent.getData();
+        String packageName = packageUri.getSchemeSpecificPart();
+        IContentProvider iprovider =
+            context.getContentResolver().acquireProvider(ContactsContract.AUTHORITY);
+        ContentProvider provider = ContentProvider.coerceToLocalContentProvider(iprovider);
+        if (provider instanceof ContactsProvider2) {
+            ((ContactsProvider2)provider).onPackageChanged(packageName);
+        }
+    }
+}
diff --git a/src/com/android/providers/contacts/ProjectionMap.java b/src/com/android/providers/contacts/ProjectionMap.java
new file mode 100644
index 0000000..56198b8
--- /dev/null
+++ b/src/com/android/providers/contacts/ProjectionMap.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+        }
+
+    }
+
+    public 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/ReorderingCursorWrapper.java b/src/com/android/providers/contacts/ReorderingCursorWrapper.java
index d332fa3..e52b095 100644
--- a/src/com/android/providers/contacts/ReorderingCursorWrapper.java
+++ b/src/com/android/providers/contacts/ReorderingCursorWrapper.java
@@ -94,6 +94,11 @@
     }
 
     @Override
+    public int getType(int column) {
+        return mCursor.getType(column);
+    }
+
+    @Override
     public boolean isNull(int column) {
         return mCursor.isNull(column);
     }
diff --git a/tests/assets/test1/expected_contacts.txt b/tests/assets/test1/expected_contacts.txt
index ebe4dad..a73f0fe 100644
--- a/tests/assets/test1/expected_contacts.txt
+++ b/tests/assets/test1/expected_contacts.txt
@@ -10,7 +10,7 @@
 10    starred=0
 11    in_visible_group=0
 12    has_phone_number=0
-13    lookup=2273r1-523450522C46324E483C32
+13    lookup=2273r1-533551532D47334F493D33
 14 }
 15 1 {
 16    _id=2
@@ -52,5 +52,5 @@
 52    starred=0
 53    in_visible_group=1
 54    has_phone_number=1
-55    lookup=2187r11-2C3232343248462C46324E483C32
+55    lookup=2187r11-2D3333353349472D47334F493D33
 56 }
diff --git a/tests/assets/testSynced/expected_contacts.txt b/tests/assets/testSynced/expected_contacts.txt
index 8ce6f03..bcb5cf8 100644
--- a/tests/assets/testSynced/expected_contacts.txt
+++ b/tests/assets/testSynced/expected_contacts.txt
@@ -10,7 +10,7 @@
 10    starred=0
 11    in_visible_group=0
 12    has_phone_number=0
-13    lookup=2273r1-423444445C52345052
+13    lookup=2273r1-433545455D53355153
 14 }
 15 1 {
 16    _id=2
@@ -94,7 +94,7 @@
 94    starred=0
 95    in_visible_group=1
 96    has_phone_number=1
-97    lookup=389r7-464852505C463034324654442E344E
+97    lookup=389r7-474953515D473135334755452F354F
 98 }
 99 7 {
 100    _id=8
@@ -108,7 +108,7 @@
 108    starred=0
 109    in_visible_group=1
 110    has_phone_number=1
-111    lookup=389r8-464852505C463034324654442E344E442C3C42
+111    lookup=389r8-474953515D473135334755452F354F452D3D43
 112 }
 113 8 {
 114    _id=9
@@ -122,7 +122,7 @@
 122    starred=0
 123    in_visible_group=1
 124    has_phone_number=0
-125    lookup=389r9-464852505C46303432442C3C42
+125    lookup=389r9-474953515D47313533452D3D43
 126 }
 127 9 {
 128    _id=10
@@ -136,7 +136,7 @@
 136    starred=0
 137    in_visible_group=1
 138    has_phone_number=0
-139    lookup=389r10-464852505C46303432
+139    lookup=389r10-474953515D47313533
 140 }
 141 10 {
 142    _id=11
@@ -150,7 +150,7 @@
 150    starred=0
 151    in_visible_group=1
 152    has_phone_number=1
-153    lookup=389r11-297E297E297E297E297E297E297E297E297E297E
+153    lookup=389r11-2A902A902A902A902A902A902A902A902A902A90
 154 }
 155 11 {
 156    _id=12
@@ -164,5 +164,5 @@
 164    starred=0
 165    in_visible_group=1
 166    has_phone_number=0
-167    lookup=389r12-464852505C4630343238442C3C42304844
+167    lookup=389r12-474953515D4731353339452D3D43314945
 168 }
\ No newline at end of file
diff --git a/tests/assets/testUnsynced/expected_contacts.txt b/tests/assets/testUnsynced/expected_contacts.txt
index 32dfdfb..e6f95ba 100644
--- a/tests/assets/testUnsynced/expected_contacts.txt
+++ b/tests/assets/testUnsynced/expected_contacts.txt
@@ -10,7 +10,7 @@
 10    starred=0
 11    in_visible_group=1
 12    has_phone_number=1
-13    lookup=0r1-4654442E344E2C4632442C3C42
+13    lookup=0r1-4755452F354F2D4733452D3D43
 14 }
 15 1 {
 16    _id=2
@@ -24,7 +24,7 @@
 24    starred=0
 25    in_visible_group=1
 26    has_phone_number=1
-27    lookup=0r2-4654442E344E
+27    lookup=0r2-4755452F354F
 28 }
 29 2 {
 30    _id=3
@@ -38,7 +38,7 @@
 38    starred=0
 39    in_visible_group=1
 40    has_phone_number=0
-41    lookup=0r3-442C3C42
+41    lookup=0r3-452D3D43
 42 }
 43 3 {
 44    _id=4
@@ -52,7 +52,7 @@
 52    starred=0
 53    in_visible_group=1
 54    has_phone_number=1
-55    lookup=0r4-2988298E298C297E2984298A297C2980
+55    lookup=0r4-2A9A2AA02A9E2A902A962A9C2A8E2A92
 56 }
 57 4 {
 58    _id=5
@@ -66,5 +66,5 @@
 66    starred=0
 67    in_visible_group=1
 68    has_phone_number=0
-69    lookup=0r5-442C3C42324844304844
+69    lookup=0r5-452D3D43334945314945
 70 }
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index ebca24d..d96c24e 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -18,6 +18,8 @@
 
 import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
 
+import com.google.android.collect.Sets;
+
 import android.accounts.Account;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
@@ -30,12 +32,6 @@
 import android.net.Uri;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.AggregationExceptions;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Groups;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.Settings;
-import android.provider.ContactsContract.StatusUpdates;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Im;
@@ -45,7 +41,14 @@
 import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.Settings;
+import android.provider.ContactsContract.StatusUpdates;
 import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
 import android.test.mock.MockContentResolver;
 import android.util.Log;
 
@@ -57,8 +60,8 @@
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.Map;
-import java.util.Set;
 import java.util.Map.Entry;
+import java.util.Set;
 
 /**
  * A common superclass for {@link ContactsProvider2}-related tests.
@@ -171,24 +174,46 @@
 
     protected long createRawContact(Account account, String... extras) {
         ContentValues values = new ContentValues();
-        for (int i = 0; i < extras.length; ) {
-            values.put(extras[i], extras[i + 1]);
-            i += 2;
-        }
+        extrasVarArgsToValues(values, extras);
         final Uri uri = maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, account);
         Uri contactUri = mResolver.insert(uri, values);
         return ContentUris.parseId(contactUri);
     }
 
+    protected int updateItem(Uri uri, long id, String... extras) {
+        Uri itemUri = ContentUris.withAppendedId(uri, id);
+        return updateItem(itemUri, extras);
+    }
+
+    protected int updateItem(Uri uri, String... extras) {
+        ContentValues values = new ContentValues();
+        extrasVarArgsToValues(values, extras);
+        return mResolver.update(uri, values, null, null);
+    }
+
+    private static void extrasVarArgsToValues(ContentValues values, String... extras) {
+        for (int i = 0; i < extras.length; ) {
+            values.put(extras[i], extras[i + 1]);
+            i += 2;
+        }
+    }
+
     protected long createGroup(Account account, String sourceId, String title) {
-        return createGroup(account, sourceId, title, 1);
+        return createGroup(account, sourceId, title, 1, false, false);
     }
 
     protected long createGroup(Account account, String sourceId, String title, int visible) {
+        return createGroup(account, sourceId, title, visible, false, false);
+    }
+
+    protected long createGroup(Account account, String sourceId, String title,
+            int visible, boolean autoAdd, boolean favorite) {
         ContentValues values = new ContentValues();
         values.put(Groups.SOURCE_ID, sourceId);
         values.put(Groups.TITLE, title);
         values.put(Groups.GROUP_VISIBLE, visible);
+        values.put(Groups.AUTO_ADD, autoAdd ? 1 : 0);
+        values.put(Groups.FAVORITES, favorite ? 1 : 0);
         final Uri uri = maybeAddAccountQueryParameters(Groups.CONTENT_URI, account);
         return ContentUris.parseId(mResolver.insert(uri, values));
     }
@@ -420,6 +445,16 @@
         return photoId;
     }
 
+    protected boolean queryRawContactIsStarred(long rawContactId) {
+        Cursor c = queryRawContact(rawContactId);
+        try {
+            assertTrue(c.moveToFirst());
+            return c.getLong(c.getColumnIndex(RawContacts.STARRED)) != 0;
+        } finally {
+            c.close();
+        }
+    }
+
     protected String queryDisplayName(long contactId) {
         Cursor c = queryContact(contactId);
         assertTrue(c.moveToFirst());
@@ -428,7 +463,7 @@
         return displayName;
     }
 
-    private String queryLookupKey(long contactId) {
+    protected String queryLookupKey(long contactId) {
         Cursor c = queryContact(contactId);
         assertTrue(c.moveToFirst());
         String lookupKey = c.getString(c.getColumnIndex(Contacts.LOOKUP_KEY));
@@ -578,6 +613,14 @@
         }
     }
 
+    protected void assertNoRowsAndClose(Cursor c) {
+        try {
+            assertFalse(c.moveToNext());
+        } finally {
+            c.close();
+        }
+    }
+
     protected static class IdComparator implements Comparator<ContentValues> {
         public int compare(ContentValues o1, ContentValues o2) {
             long id1 = o1.getAsLong(ContactsContract.Data._ID);
@@ -689,6 +732,10 @@
         assertStoredValues(rowUri, null, null, expectedValues);
     }
 
+    protected void assertStoredValues(Uri rowUri, ContentValues[] expectedValues) {
+        assertStoredValues(rowUri, null, null, expectedValues);
+    }
+
     protected void assertStoredValues(Uri rowUri, String selection, String[] selectionArgs,
             ContentValues expectedValues) {
         Cursor c = mResolver.query(rowUri, null, selection, selectionArgs, null);
@@ -712,6 +759,17 @@
         }
     }
 
+    protected void assertStoredValues(
+            Uri rowUri, String selection, String[] selectionArgs, ContentValues[] expectedValues) {
+        Cursor c = mResolver.query(rowUri, null, selection, selectionArgs, null);
+        try {
+            assertEquals("Record count", expectedValues.length, c.getCount());
+            assertCursorValues(c, expectedValues);
+        } finally {
+            c.close();
+        }
+    }
+
     /**
      * Constructs a selection (where clause) out of all supplied values, uses it
      * to query the provider and verifies that a single row is returned and it
@@ -760,12 +818,43 @@
         }
     }
 
+    protected void assertCursorValue(Cursor cursor, String column, Object expectedValue) {
+        String actualValue = cursor.getString(cursor.getColumnIndex(column));
+        assertEquals("Column " + column, String.valueOf(expectedValue),
+                String.valueOf(actualValue));
+    }
+
     protected void assertCursorValues(Cursor cursor, ContentValues expectedValues) {
+        StringBuilder message = new StringBuilder();
+        boolean result = equalsWithExpectedValues(cursor, expectedValues, message);
+        assertTrue(message.toString(), result);
+    }
+
+    protected void assertCursorValues(Cursor cursor, ContentValues[] expectedValues) {
+        StringBuilder message = new StringBuilder();
+        for (ContentValues v : expectedValues) {
+            boolean found = false;
+            cursor.moveToPosition(-1);
+            while (cursor.moveToNext()) {
+                found = equalsWithExpectedValues(cursor, v, message);
+                if (found) {
+                    break;
+                }
+            }
+            assertTrue("Expected values can not be found " + v + message.toString(), found);
+        }
+    }
+
+    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();
             int index = cursor.getColumnIndex(column);
-            assertTrue("No such column: " + column, index != -1);
+            if (index == -1) {
+                msgBuffer.append("No such column: ").append(column);
+                return false;
+            }
             Object expectedValue = expectedValues.get(column);
             String value;
             if (expectedValue instanceof byte[]) {
@@ -775,8 +864,20 @@
                 expectedValue = expectedValues.getAsString(column);
                 value = cursor.getString(index);
             }
-            assertEquals("Column value " + column, expectedValue, value);
+            if (expectedValue != null && !expectedValue.equals(value) || value != null
+                    && !value.equals(expectedValue)) {
+                msgBuffer
+                        .append("Column value ")
+                        .append(column)
+                        .append(" expected <")
+                        .append(expectedValue)
+                        .append(">, but was <")
+                        .append(value)
+                        .append('>');
+                return false;
+            }
         }
+        return true;
     }
 
     private String[] buildProjection(ContentValues values) {
@@ -880,6 +981,14 @@
         assertEquals(expected, ((SynchronousContactsProvider2)mActor.provider).isNetworkNotified());
     }
 
+    protected void assertProjection(Uri uri, String[] expectedProjection) {
+        Cursor cursor = mResolver.query(uri, null, "0", null, null);
+        String[] actualProjection = cursor.getColumnNames();
+        MoreAsserts.assertEquals("Incorrect projection for URI: " + uri,
+                Sets.newHashSet(expectedProjection), Sets.newHashSet(actualProjection));
+        cursor.close();
+    }
+
     /**
      * A contact in the database, and the attributes used to create it.  Construct using
      * {@link GoldenContactBuilder#build()}.
diff --git a/tests/src/com/android/providers/contacts/CallLogProviderTest.java b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
index af5a1fe..392fa65 100644
--- a/tests/src/com/android/providers/contacts/CallLogProviderTest.java
+++ b/tests/src/com/android/providers/contacts/CallLogProviderTest.java
@@ -63,6 +63,7 @@
         ContentValues values = new ContentValues();
         putCallValues(values);
         Uri uri = mResolver.insert(Calls.CONTENT_URI, values);
+        values.put(Calls.COUNTRY_ISO, "us");
         assertStoredValues(uri, values);
         assertSelection(uri, values, Calls._ID, ContentUris.parseId(uri));
     }
@@ -137,6 +138,7 @@
         values.put(Calls.CACHED_NAME, "1-800-GOOG-411");
         values.put(Calls.CACHED_NUMBER_TYPE, Phone.TYPE_CUSTOM);
         values.put(Calls.CACHED_NUMBER_LABEL, "Directory");
+        values.put(Calls.COUNTRY_ISO, "us");
         assertStoredValues(uri, values);
     }
 
@@ -158,6 +160,11 @@
             }
             return mDbHelper;
         }
+
+        @Override
+        protected String getCurrentCountryIso() {
+            return "us";
+        }
     }
 }
 
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
index 4b8429e..013a706 100644
--- a/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorPerformanceTest.java
@@ -17,6 +17,7 @@
 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;
@@ -71,6 +72,13 @@
             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.");
diff --git a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
index 328b03c..551550a 100644
--- a/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/ContactAggregatorTest.java
@@ -28,6 +28,7 @@
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.AggregationSuggestions;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
@@ -967,6 +968,54 @@
         assertAggregated(rawContactId1, rawContactId2);
     }
 
+    public void testAggregationSuggestionsQueryBuilderWithContactId() throws Exception {
+        Uri uri = AggregationSuggestions.builder().setContactId(12).setLimit(7).build();
+        assertEquals("content://com.android.contacts/contacts/12/suggestions?limit=7",
+                uri.toString());
+    }
+
+    public void testAggregationSuggestionsQueryBuilderWithValues() throws Exception {
+        Uri uri = AggregationSuggestions.builder()
+                .addParameter(AggregationSuggestions.PARAMETER_MATCH_NAME, "name1")
+                .addParameter(AggregationSuggestions.PARAMETER_MATCH_NAME, "name2")
+                .addParameter(AggregationSuggestions.PARAMETER_MATCH_EMAIL, "email1")
+                .addParameter(AggregationSuggestions.PARAMETER_MATCH_EMAIL, "email2")
+                .addParameter(AggregationSuggestions.PARAMETER_MATCH_PHONE, "phone1")
+                .addParameter(AggregationSuggestions.PARAMETER_MATCH_NICKNAME, "nickname1")
+                .setLimit(7)
+                .build();
+        assertEquals("content://com.android.contacts/contacts/0/suggestions?"
+                + "limit=7"
+                + "&query=name%3Aname1"
+                + "&query=name%3Aname2"
+                + "&query=email%3Aemail1"
+                + "&query=email%3Aemail2"
+                + "&query=phone%3Aphone1"
+                + "&query=nickname%3Anickname1", uri.toString());
+    }
+
+    public void testAggregationSuggestionsByName() throws Exception {
+        long rawContactId1 = createRawContactWithName("first1", "last1");
+        long rawContactId2 = createRawContactWithName("first2", "last2");
+
+        Uri uri = AggregationSuggestions.builder()
+                .addParameter(AggregationSuggestions.PARAMETER_MATCH_NAME, "last1 first1")
+                .build();
+
+        Cursor cursor = mResolver.query(
+                uri, new String[] { Contacts._ID, Contacts.DISPLAY_NAME }, null, null, null);
+
+        assertEquals(1, cursor.getCount());
+
+        cursor.moveToFirst();
+
+        ContentValues values = new ContentValues();
+        values.put(Contacts._ID, queryContactId(rawContactId1));
+        values.put(Contacts.DISPLAY_NAME, "first1 last1");
+        assertCursorValues(cursor, values);
+        cursor.close();
+    }
+
     private void assertSuggestions(long contactId, long... suggestions) {
         final Uri aggregateUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
         Uri uri = Uri.withAppendedPath(aggregateUri,
diff --git a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
new file mode 100644
index 0000000..a4757ca
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
@@ -0,0 +1,497 @@
+/*
+ * 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 com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
+import com.google.android.collect.Lists;
+
+import android.accounts.Account;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.RawContacts;
+import android.test.mock.MockContentProvider;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Unit tests for {@link ContactDirectoryManager}. Run the test like this:
+ * <code>
+ *   adb shell am instrument -e class com.android.providers.contacts.ContactDirectoryManagerTest
+ *       -w com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class ContactDirectoryManagerTest extends BaseContactsProvider2Test {
+
+    private ContactsMockPackageManager mPackageManager;
+
+    private ContactsProvider2 mProvider;
+
+    private ContactDirectoryManager mDirectoryManager;
+
+    public static class MockContactDirectoryProvider extends MockContentProvider {
+
+        private String mAuthority;
+
+        private MatrixCursor mResponse;
+
+        @Override
+        public void attachInfo(Context context, ProviderInfo info) {
+            mAuthority = info.authority;
+        }
+
+        public MatrixCursor createResponseCursor() {
+            mResponse = new MatrixCursor(
+                    new String[] { Directory.ACCOUNT_NAME, Directory.ACCOUNT_TYPE,
+                            Directory.DISPLAY_NAME, Directory.TYPE_RESOURCE_ID,
+                            Directory.EXPORT_SUPPORT, Directory.SHORTCUT_SUPPORT,
+                            Directory.PHOTO_SUPPORT });
+
+            return mResponse;
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+
+            if (uri.toString().equals("content://" + mAuthority + "/directories")) {
+                return mResponse;
+            } else if (uri.toString().startsWith("content://" + mAuthority + "/contacts")) {
+                MatrixCursor cursor = new MatrixCursor(
+                        new String[] { "projection", "selection", "selectionArgs", "sortOrder",
+                                "accountName", "accountType"});
+                cursor.addRow(new Object[] {
+                    Lists.newArrayList(projection).toString(),
+                    selection,
+                    Lists.newArrayList(selectionArgs).toString(),
+                    sortOrder,
+                    uri.getQueryParameter(RawContacts.ACCOUNT_NAME),
+                    uri.getQueryParameter(RawContacts.ACCOUNT_TYPE),
+                });
+                return cursor;
+            } else if (uri.toString().startsWith(
+                    "content://" + mAuthority + "/aggregation_exceptions")) {
+                return new MatrixCursor(projection);
+            }
+
+            fail("Unexpected uri: " + uri);
+            return null;
+        }
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mProvider = (ContactsProvider2) getProvider();
+        mDirectoryManager = mProvider.getContactDirectoryManager();
+
+        mPackageManager = (ContactsMockPackageManager) getProvider()
+                .getContext().getPackageManager();
+    }
+
+    public void testScanAllProviders() throws Exception {
+        mPackageManager.setInstalledPackages(
+                Lists.newArrayList(
+                        createProviderPackage("test.package1", "authority1"),
+                        createProviderPackage("test.package2", "authority2")));
+
+        MockContactDirectoryProvider provider1 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority1");
+
+        MatrixCursor response1 = provider1.createResponseCursor();
+        addDirectoryRow(response1, "account-name1", "account-type1", "display-name1", 1,
+                Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_FULL_SIZE_ONLY);
+        addDirectoryRow(response1, "account-name2", "account-type2", "display-name2", 2,
+                Directory.EXPORT_SUPPORT_ANY_ACCOUNT, Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY,
+                Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY);
+
+        MockContactDirectoryProvider provider2 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority2");
+
+        MatrixCursor response2 = provider2.createResponseCursor();
+        addDirectoryRow(response2, "account-name3", "account-type3", "display-name3", 3,
+                Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY, Directory.SHORTCUT_SUPPORT_FULL,
+                Directory.PHOTO_SUPPORT_FULL);
+
+        mDirectoryManager.scanAllPackages();
+
+        Cursor cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
+        assertEquals(5, cursor.getCount());
+
+        cursor.moveToPosition(2);
+        assertDirectoryRow(cursor, "test.package1", "authority1", "account-name1", "account-type1",
+                "display-name1", 1, Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_FULL_SIZE_ONLY);
+
+        cursor.moveToNext();
+        assertDirectoryRow(cursor, "test.package1", "authority1", "account-name2", "account-type2",
+                "display-name2", 2, Directory.EXPORT_SUPPORT_ANY_ACCOUNT,
+                Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY, Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY);
+
+        cursor.moveToNext();
+        assertDirectoryRow(cursor, "test.package2", "authority2", "account-name3", "account-type3",
+                "display-name3", 3, Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY,
+                Directory.SHORTCUT_SUPPORT_FULL, Directory.PHOTO_SUPPORT_FULL);
+
+        cursor.close();
+    }
+
+    public void testPackageInstalled() throws Exception {
+        mPackageManager.setInstalledPackages(
+                Lists.newArrayList(createProviderPackage("test.package1", "authority1")));
+
+        MockContactDirectoryProvider provider1 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority1");
+
+        MatrixCursor response1 = provider1.createResponseCursor();
+        addDirectoryRow(response1, "account-name1", "account-type1", "display-name1", 1,
+                Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_FULL);
+
+        mDirectoryManager.scanAllPackages();
+
+        // At this point the manager has discovered a single directory (plus two
+        // standard ones).
+        Cursor cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
+        assertEquals(3, cursor.getCount());
+        cursor.close();
+
+        // Pretend to install another package
+        MockContactDirectoryProvider provider2 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority2");
+
+        MatrixCursor response2 = provider2.createResponseCursor();
+        addDirectoryRow(response2, "account-name3", "account-type3", "display-name3", 3,
+                Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY, Directory.SHORTCUT_SUPPORT_FULL,
+                Directory.PHOTO_SUPPORT_FULL);
+
+        mPackageManager.getInstalledPackages(0).add(
+                createProviderPackage("test.package2", "authority2"));
+
+        mProvider.onPackageChanged("test.package2");
+
+        cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
+        assertEquals(4, cursor.getCount());
+
+        cursor.moveToPosition(2);
+        assertDirectoryRow(cursor, "test.package1", "authority1", "account-name1", "account-type1",
+                "display-name1", 1, Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_FULL);
+
+        cursor.moveToNext();
+        assertDirectoryRow(cursor, "test.package2", "authority2", "account-name3", "account-type3",
+                "display-name3", 3, Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY,
+                Directory.SHORTCUT_SUPPORT_FULL, Directory.PHOTO_SUPPORT_FULL);
+
+        cursor.close();
+    }
+
+    public void testPackageUninstalled() throws Exception {
+        mPackageManager.setInstalledPackages(
+                Lists.newArrayList(
+                        createProviderPackage("test.package1", "authority1"),
+                        createProviderPackage("test.package2", "authority2")));
+
+        MockContactDirectoryProvider provider1 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority1");
+
+        MatrixCursor response1 = provider1.createResponseCursor();
+        addDirectoryRow(response1, "account-name1", "account-type1", "display-name1", 1,
+                Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+
+        MockContactDirectoryProvider provider2 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority2");
+
+        MatrixCursor response2 = provider2.createResponseCursor();
+        addDirectoryRow(response2, "account-name3", "account-type3", "display-name3", 3,
+                Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY, Directory.SHORTCUT_SUPPORT_FULL,
+                Directory.PHOTO_SUPPORT_FULL);
+
+        mDirectoryManager.scanAllPackages();
+
+        // At this point the manager has discovered two custom directories.
+        Cursor cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
+        assertEquals(4, cursor.getCount());
+        cursor.close();
+
+        // Pretend to uninstall one of the packages
+        mPackageManager.getInstalledPackages(0).remove(1);
+
+        mProvider.onPackageChanged("test.package2");
+
+        cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
+        assertEquals(3, cursor.getCount());
+
+        cursor.moveToPosition(2);
+        assertDirectoryRow(cursor, "test.package1", "authority1", "account-name1", "account-type1",
+                "display-name1", 1, Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+
+        cursor.close();
+    }
+
+    public void testPackageReplaced() throws Exception {
+        mPackageManager.setInstalledPackages(
+                Lists.newArrayList(
+                        createProviderPackage("test.package1", "authority1"),
+                        createProviderPackage("test.package2", "authority2")));
+
+        MockContactDirectoryProvider provider1 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority1");
+
+        MatrixCursor response1 = provider1.createResponseCursor();
+        addDirectoryRow(response1, "account-name1", "account-type1", "display-name1", 1,
+                Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+
+        MockContactDirectoryProvider provider2 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority2");
+
+        MatrixCursor response2 = provider2.createResponseCursor();
+        addDirectoryRow(response2, "account-name3", "account-type3", "display-name3", 3,
+                Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY, Directory.SHORTCUT_SUPPORT_FULL,
+                Directory.PHOTO_SUPPORT_FULL);
+
+        mDirectoryManager.scanAllPackages();
+
+        // At this point the manager has discovered two custom directories.
+        Cursor cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
+        assertEquals(4, cursor.getCount());
+        cursor.close();
+
+        // Pretend to replace the package with a different provider inside
+        MatrixCursor response3 = provider2.createResponseCursor();
+        addDirectoryRow(response3, "account-name4", "account-type4", "display-name4", 4,
+                Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+
+        mProvider.onPackageChanged("test.package2");
+
+        cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
+        assertEquals(4, cursor.getCount());
+
+        cursor.moveToPosition(2);
+        assertDirectoryRow(cursor, "test.package1", "authority1", "account-name1", "account-type1",
+                "display-name1", 1, Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+
+        cursor.moveToNext();
+        assertDirectoryRow(cursor, "test.package2", "authority2", "account-name4", "account-type4",
+                "display-name4", 4, Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+
+        cursor.close();
+    }
+
+    public void testAccountRemoval() throws Exception {
+        mPackageManager.setInstalledPackages(
+                Lists.newArrayList(
+                        createProviderPackage("test.package1", "authority1"),
+                        createProviderPackage("test.package2", "authority2")));
+
+        MockContactDirectoryProvider provider1 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority1");
+
+        ((ContactsProvider2)getProvider()).onAccountsUpdated(
+                new Account[]{
+                        new Account("account-name1", "account-type1"),
+                        new Account("account-name2", "account-type2")});
+
+        MatrixCursor response1 = provider1.createResponseCursor();
+        addDirectoryRow(response1, "account-name1", "account-type1", "display-name1", 1,
+                Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+        addDirectoryRow(response1, "account-name2", "account-type2", "display-name2", 2,
+                Directory.EXPORT_SUPPORT_ANY_ACCOUNT, Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY,
+                Directory.PHOTO_SUPPORT_FULL_SIZE_ONLY);
+
+        mDirectoryManager.scanAllPackages();
+
+        ((ContactsProvider2)getProvider()).onAccountsUpdated(
+                new Account[]{new Account("account-name1", "account-type1")});
+
+        Cursor cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
+        assertEquals(3, cursor.getCount());
+
+        cursor.moveToPosition(2);
+        assertDirectoryRow(cursor, "test.package1", "authority1", "account-name1", "account-type1",
+                "display-name1", 1, Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+
+        cursor.close();
+    }
+
+    public void testNotifyDirectoryChange() throws Exception {
+        mPackageManager.setInstalledPackages(
+                Lists.newArrayList(createProviderPackage("test.package1", "authority1")));
+
+        MockContactDirectoryProvider provider1 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority1");
+
+        MatrixCursor response1 = provider1.createResponseCursor();
+        addDirectoryRow(response1, "account-name1", "account-type1", "display-name1", 1,
+                Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+
+        mDirectoryManager.scanAllPackages();
+
+        // Pretend to replace the package with a different provider inside
+        MatrixCursor response2 = provider1.createResponseCursor();
+        addDirectoryRow(response2, "account-name2", "account-type2", "display-name2", 2,
+                Directory.EXPORT_SUPPORT_ANY_ACCOUNT, Directory.SHORTCUT_SUPPORT_FULL,
+                Directory.PHOTO_SUPPORT_FULL);
+
+        ContactsContract.Directory.notifyDirectoryChange(mResolver);
+
+        Cursor cursor = mResolver.query(Directory.CONTENT_URI, null, null, null, null);
+        assertEquals(3, cursor.getCount());
+
+        cursor.moveToPosition(2);
+        assertDirectoryRow(cursor, "test.package1", "authority1", "account-name2", "account-type2",
+                "display-name2", 2, Directory.EXPORT_SUPPORT_ANY_ACCOUNT,
+                Directory.SHORTCUT_SUPPORT_FULL, Directory.PHOTO_SUPPORT_FULL);
+
+        cursor.close();
+    }
+
+    public void testForwardingToDirectoryProvider() throws Exception {
+        mPackageManager.setInstalledPackages(
+                Lists.newArrayList(createProviderPackage("test.package1", "authority1")));
+
+        MockContactDirectoryProvider provider1 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority1");
+
+        MatrixCursor response1 = provider1.createResponseCursor();
+        addDirectoryRow(response1, "account-name1", "account-type1", "display-name1", 1,
+                Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+
+        mDirectoryManager.scanAllPackages();
+
+        Cursor cursor = mResolver.query(
+                Directory.CONTENT_URI, new String[] { Directory._ID }, null, null, null);
+        cursor.moveToPosition(2);
+        long directoryId = cursor.getLong(0);
+        cursor.close();
+
+        Uri contentUri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
+                ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
+
+        // The request should be forwarded to TestProvider, which will simply
+        // package arguments and return them to us for verification
+        cursor = mResolver.query(contentUri,
+                new String[]{"f1", "f2"}, "query", new String[]{"s1", "s2"}, "so");
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        assertEquals("[f1, f2]", cursor.getString(cursor.getColumnIndex("projection")));
+        assertEquals("query", cursor.getString(cursor.getColumnIndex("selection")));
+        assertEquals("[s1, s2]", cursor.getString(cursor.getColumnIndex("selectionArgs")));
+        assertEquals("so", cursor.getString(cursor.getColumnIndex("sortOrder")));
+        assertEquals("account-name1", cursor.getString(cursor.getColumnIndex("accountName")));
+        assertEquals("account-type1", cursor.getString(cursor.getColumnIndex("accountType")));
+        cursor.close();
+    }
+
+    public void testProjectionPopulated() throws Exception {
+        mPackageManager.setInstalledPackages(
+                Lists.newArrayList(createProviderPackage("test.package1", "authority1")));
+
+        MockContactDirectoryProvider provider1 = (MockContactDirectoryProvider) addProvider(
+                MockContactDirectoryProvider.class, "authority1");
+
+        MatrixCursor response1 = provider1.createResponseCursor();
+        addDirectoryRow(response1, "account-name1", "account-type1", "display-name1", 1,
+                Directory.EXPORT_SUPPORT_NONE, Directory.SHORTCUT_SUPPORT_NONE,
+                Directory.PHOTO_SUPPORT_NONE);
+
+        mDirectoryManager.scanAllPackages();
+
+        Cursor cursor = mResolver.query(
+                Directory.CONTENT_URI, new String[] { Directory._ID }, null, null, null);
+        cursor.moveToPosition(2);
+        long directoryId = cursor.getLong(0);
+        cursor.close();
+
+        Uri contentUri = AggregationExceptions.CONTENT_URI.buildUpon().appendQueryParameter(
+                ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
+
+        // The request should be forwarded to TestProvider, which will return an empty cursor
+        // but the projection should be correctly populated by ContactProvider
+        assertProjection(contentUri, new String[]{
+                AggregationExceptionColumns._ID,
+                AggregationExceptions.TYPE,
+                AggregationExceptions.RAW_CONTACT_ID1,
+                AggregationExceptions.RAW_CONTACT_ID2,
+        });
+    }
+
+    protected PackageInfo createProviderPackage(String packageName, String authority) {
+        PackageInfo providerPackage = new PackageInfo();
+        providerPackage.packageName = packageName;
+        ProviderInfo providerInfo = new ProviderInfo();
+        providerInfo.packageName = providerPackage.packageName;
+        providerInfo.authority = authority;
+        providerInfo.metaData = new Bundle();
+        providerInfo.metaData.putBoolean("android.content.ContactDirectory", true);
+        providerPackage.providers = new ProviderInfo[] { providerInfo };
+        return providerPackage;
+    }
+
+    protected void addDirectoryRow(MatrixCursor cursor, String accountName, String accountType,
+            String displayName, int typeResourceId, int exportSupport, int shortcutSupport,
+            int photoSupport) {
+        Object[] row = new Object[cursor.getColumnCount()];
+        row[cursor.getColumnIndex(Directory.ACCOUNT_NAME)] = accountName;
+        row[cursor.getColumnIndex(Directory.ACCOUNT_TYPE)] = accountType;
+        row[cursor.getColumnIndex(Directory.DISPLAY_NAME)] = displayName;
+        row[cursor.getColumnIndex(Directory.TYPE_RESOURCE_ID)] = typeResourceId;
+        row[cursor.getColumnIndex(Directory.EXPORT_SUPPORT)] = exportSupport;
+        row[cursor.getColumnIndex(Directory.SHORTCUT_SUPPORT)] = shortcutSupport;
+        row[cursor.getColumnIndex(Directory.PHOTO_SUPPORT)] = photoSupport;
+        cursor.addRow(row);
+    }
+
+    protected void assertDirectoryRow(Cursor cursor, String packageName, String authority,
+            String accountName, String accountType, String displayName, int typeResourceId,
+            int exportSupport, int shortcutSupport, int photoSupport) {
+        ContentValues values = new ContentValues();
+        values.put(Directory.PACKAGE_NAME, packageName);
+        values.put(Directory.DIRECTORY_AUTHORITY, authority);
+        values.put(Directory.ACCOUNT_NAME, accountName);
+        values.put(Directory.ACCOUNT_TYPE, accountType);
+        values.put(Directory.DISPLAY_NAME, displayName);
+        values.put(Directory.TYPE_RESOURCE_ID, typeResourceId);
+        values.put(Directory.EXPORT_SUPPORT, exportSupport);
+        values.put(Directory.SHORTCUT_SUPPORT, shortcutSupport);
+        values.put(Directory.PHOTO_SUPPORT, photoSupport);
+
+        assertCursorValues(cursor, values);
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
index 0a0955e..2e1f7d3 100644
--- a/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
+++ b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
@@ -95,7 +95,7 @@
         assertEquals(allKeys, new HashSet<String>(Arrays.asList(expectedKeys)));
     }
 
-    private void testChineseStyleNameWithDifferentLocale() throws Exception {
+    public void testChineseStyleNameWithDifferentLocale() throws Exception {
         mContactLocaleUtils.setLocale(Locale.ENGLISH);
         assertTrue(mContactLocaleUtils.getSortKey(CHINESE_NAME,
                 FullNameStyle.CHINESE).equalsIgnoreCase("DU \u675C JUAN \u9D51"));
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 6e0e47a..95b4a7d 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -23,11 +23,12 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Binder;
 import android.provider.BaseColumns;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds;
@@ -41,11 +42,9 @@
 import android.test.RenamingDelegatingContext;
 import android.test.mock.MockContentResolver;
 import android.test.mock.MockContext;
-import android.test.mock.MockPackageManager;
 import android.test.mock.MockResources;
 import android.util.TypedValue;
 
-import java.util.HashMap;
 import java.util.Locale;
 
 /**
@@ -94,7 +93,9 @@
     public ContentProvider addProvider(Class<? extends ContentProvider> providerClass,
             String authority) throws Exception {
         ContentProvider provider = providerClass.newInstance();
-        provider.attachInfo(mProviderContext, null);
+        ProviderInfo info = new ProviderInfo();
+        info.authority = authority;
+        provider.attachInfo(mProviderContext, info);
         resolver.addProvider(authority, provider);
         return provider;
     }
@@ -112,7 +113,7 @@
     private static class RestrictionMockContext extends MockContext {
         private final Context mOverallContext;
         private final String mReportedPackageName;
-        private final RestrictionMockPackageManager mPackageManager;
+        private final ContactsMockPackageManager mPackageManager;
         private final ContentResolver mResolver;
         private final Resources mRes;
 
@@ -125,7 +126,7 @@
             mReportedPackageName = reportedPackageName;
             mResolver = resolver;
 
-            mPackageManager = new RestrictionMockPackageManager();
+            mPackageManager = new ContactsMockPackageManager();
             mPackageManager.addPackage(1000, PACKAGE_GREY);
             mPackageManager.addPackage(2000, PACKAGE_RED);
             mPackageManager.addPackage(3000, PACKAGE_GREEN);
@@ -157,6 +158,13 @@
         public ContentResolver getContentResolver() {
             return mResolver;
         }
+
+        @Override
+        public ApplicationInfo getApplicationInfo() {
+            ApplicationInfo ai = new ApplicationInfo();
+            ai.packageName = "contactsTestPackage";
+            return ai;
+        }
     }
 
     private static class RestrictionMockResources extends MockResources {
@@ -211,54 +219,19 @@
         public CharSequence getText(int id) throws NotFoundException {
             return mRes.getText(id);
         }
+
+        @Override
+        public String getResourceName(int resid) throws NotFoundException {
+            return String.valueOf(resid);
+        }
     }
 
-    private static String sCallingPackage = null;
+    static String sCallingPackage = null;
 
     void ensureCallingPackage() {
         sCallingPackage = this.packageName;
     }
 
-    /**
-     * Mock {@link PackageManager} that knows about a specific set of packages
-     * to help test security models. Because {@link Binder#getCallingUid()}
-     * can't be mocked, you'll have to find your mock-UID manually using your
-     * {@link Context#getPackageName()}.
-     */
-    private static class RestrictionMockPackageManager extends MockPackageManager {
-        private final HashMap<Integer, String> mForward = new HashMap<Integer, String>();
-        private final HashMap<String, Integer> mReverse = new HashMap<String, Integer>();
-
-        public RestrictionMockPackageManager() {
-        }
-
-        /**
-         * Add a UID-to-package mapping, which is then stored internally.
-         */
-        public void addPackage(int packageUid, String packageName) {
-            mForward.put(packageUid, packageName);
-            mReverse.put(packageName, packageUid);
-        }
-
-        @Override
-        public String getNameForUid(int uid) {
-            return "name-for-uid";
-        }
-
-        @Override
-        public String[] getPackagesForUid(int uid) {
-            return new String[] { sCallingPackage };
-        }
-
-        @Override
-        public ApplicationInfo getApplicationInfo(String packageName, int flags) {
-            ApplicationInfo info = new ApplicationInfo();
-            Integer uid = mReverse.get(packageName);
-            info.uid = (uid != null) ? uid : -1;
-            return info;
-        }
-    }
-
     public long createContact(boolean isRestricted, String name) {
         ensureCallingPackage();
         long contactId = createContact(isRestricted);
diff --git a/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java b/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
new file mode 100644
index 0000000..03a86c2
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/ContactsMockPackageManager.java
@@ -0,0 +1,102 @@
+/*
+ * 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 android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.Binder;
+import android.test.mock.MockPackageManager;
+import android.test.mock.MockResources;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Mock {@link PackageManager} that knows about a specific set of packages
+ * to help test security models. Because {@link Binder#getCallingUid()}
+ * can't be mocked, you'll have to find your mock-UID manually using your
+ * {@link Context#getPackageName()}.
+ */
+public class ContactsMockPackageManager extends MockPackageManager {
+    private final HashMap<Integer, String> mForward = new HashMap<Integer, String>();
+    private final HashMap<String, Integer> mReverse = new HashMap<String, Integer>();
+    private List<PackageInfo> mPackages;
+
+    public ContactsMockPackageManager() {
+    }
+
+    /**
+     * Add a UID-to-package mapping, which is then stored internally.
+     */
+    public void addPackage(int packageUid, String packageName) {
+        mForward.put(packageUid, packageName);
+        mReverse.put(packageName, packageUid);
+    }
+
+    @Override
+    public String getNameForUid(int uid) {
+        return "name-for-uid";
+    }
+
+    @Override
+    public String[] getPackagesForUid(int uid) {
+        if (mPackages != null) {
+            return new String[] { mPackages.get(0).packageName };
+        } else {
+            return new String[] { ContactsActor.sCallingPackage };
+        }
+    }
+
+    @Override
+    public ApplicationInfo getApplicationInfo(String packageName, int flags) {
+        ApplicationInfo info = new ApplicationInfo();
+        Integer uid = mReverse.get(packageName);
+        info.uid = (uid != null) ? uid : -1;
+        return info;
+    }
+
+    public void setInstalledPackages(List<PackageInfo> packages) {
+        this.mPackages = packages;
+    }
+
+    @Override
+    public List<PackageInfo> getInstalledPackages(int flags) {
+        return mPackages;
+    }
+
+    @Override
+    public PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException {
+        for (PackageInfo info : mPackages) {
+            if (info.packageName.equals(packageName)) {
+                return info;
+            }
+        }
+        throw new NameNotFoundException();
+    }
+
+    @Override
+    public Resources getResourcesForApplication(String appPackageName) {
+        return new MockResources() {
+            @Override
+            public String getResourceName(int resid) throws NotFoundException {
+                return String.valueOf(resid);
+            }
+        };
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index 5b40485..cfcb937 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -17,6 +17,7 @@
 package com.android.providers.contacts;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
 import com.google.android.collect.Lists;
 
@@ -30,24 +31,8 @@
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.RemoteException;
 import android.provider.ContactsContract;
-import android.provider.LiveFolders;
-import android.provider.OpenableColumns;
 import android.provider.ContactsContract.AggregationExceptions;
-import android.provider.ContactsContract.ContactCounts;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.DisplayNameSources;
-import android.provider.ContactsContract.Groups;
-import android.provider.ContactsContract.PhoneLookup;
-import android.provider.ContactsContract.PhoneticNameStyle;
-import android.provider.ContactsContract.ProviderStatus;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.RawContactsEntity;
-import android.provider.ContactsContract.SearchSnippetColumns;
-import android.provider.ContactsContract.Settings;
-import android.provider.ContactsContract.StatusUpdates;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Im;
@@ -57,16 +42,33 @@
 import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.PhoneticNameStyle;
+import android.provider.ContactsContract.ProviderStatus;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.provider.ContactsContract.SearchSnippetColumns;
+import android.provider.ContactsContract.Settings;
+import android.provider.ContactsContract.StatusUpdates;
+import android.provider.LiveFolders;
+import android.provider.OpenableColumns;
 import android.test.MoreAsserts;
 import android.test.suitebuilder.annotation.LargeTest;
 
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.text.Collator;
 import java.util.Arrays;
 import java.util.Locale;
 
-
 /**
  * Unit tests for {@link ContactsProvider2}.
  *
@@ -79,6 +81,515 @@
 @LargeTest
 public class ContactsProvider2Test extends BaseContactsProvider2Test {
 
+    public void testContactsProjection() {
+        assertProjection(Contacts.CONTENT_URI, new String[]{
+                Contacts._ID,
+                Contacts.DISPLAY_NAME_PRIMARY,
+                Contacts.DISPLAY_NAME_ALTERNATIVE,
+                Contacts.DISPLAY_NAME_SOURCE,
+                Contacts.PHONETIC_NAME,
+                Contacts.PHONETIC_NAME_STYLE,
+                Contacts.SORT_KEY_PRIMARY,
+                Contacts.SORT_KEY_ALTERNATIVE,
+                Contacts.LAST_TIME_CONTACTED,
+                Contacts.TIMES_CONTACTED,
+                Contacts.STARRED,
+                Contacts.IN_VISIBLE_GROUP,
+                Contacts.PHOTO_ID,
+                Contacts.PHOTO_URI,
+                Contacts.PHOTO_THUMBNAIL_URI,
+                Contacts.CUSTOM_RINGTONE,
+                Contacts.HAS_PHONE_NUMBER,
+                Contacts.SEND_TO_VOICEMAIL,
+                Contacts.LOOKUP_KEY,
+                Contacts.NAME_RAW_CONTACT_ID,
+                Contacts.CONTACT_PRESENCE,
+                Contacts.CONTACT_CHAT_CAPABILITY,
+                Contacts.CONTACT_STATUS,
+                Contacts.CONTACT_STATUS_TIMESTAMP,
+                Contacts.CONTACT_STATUS_RES_PACKAGE,
+                Contacts.CONTACT_STATUS_LABEL,
+                Contacts.CONTACT_STATUS_ICON,
+        });
+    }
+
+    public void testContactsWithSnippetProjection() {
+        assertProjection(Contacts.CONTENT_FILTER_URI.buildUpon().appendPath("nothing").build(),
+            new String[]{
+                Contacts._ID,
+                Contacts.DISPLAY_NAME_PRIMARY,
+                Contacts.DISPLAY_NAME_ALTERNATIVE,
+                Contacts.DISPLAY_NAME_SOURCE,
+                Contacts.PHONETIC_NAME,
+                Contacts.PHONETIC_NAME_STYLE,
+                Contacts.SORT_KEY_PRIMARY,
+                Contacts.SORT_KEY_ALTERNATIVE,
+                Contacts.LAST_TIME_CONTACTED,
+                Contacts.TIMES_CONTACTED,
+                Contacts.STARRED,
+                Contacts.IN_VISIBLE_GROUP,
+                Contacts.PHOTO_ID,
+                Contacts.PHOTO_URI,
+                Contacts.PHOTO_THUMBNAIL_URI,
+                Contacts.CUSTOM_RINGTONE,
+                Contacts.HAS_PHONE_NUMBER,
+                Contacts.SEND_TO_VOICEMAIL,
+                Contacts.LOOKUP_KEY,
+                Contacts.NAME_RAW_CONTACT_ID,
+                Contacts.CONTACT_PRESENCE,
+                Contacts.CONTACT_CHAT_CAPABILITY,
+                Contacts.CONTACT_STATUS,
+                Contacts.CONTACT_STATUS_TIMESTAMP,
+                Contacts.CONTACT_STATUS_RES_PACKAGE,
+                Contacts.CONTACT_STATUS_LABEL,
+                Contacts.CONTACT_STATUS_ICON,
+
+                SearchSnippetColumns.SNIPPET_MIMETYPE,
+                SearchSnippetColumns.SNIPPET_DATA_ID,
+                SearchSnippetColumns.SNIPPET_DATA1,
+                SearchSnippetColumns.SNIPPET_DATA2,
+                SearchSnippetColumns.SNIPPET_DATA3,
+                SearchSnippetColumns.SNIPPET_DATA4,
+        });
+    }
+
+    public void testRawContactsProjection() {
+        assertProjection(RawContacts.CONTENT_URI, new String[]{
+                RawContacts._ID,
+                RawContacts.CONTACT_ID,
+                RawContacts.ACCOUNT_NAME,
+                RawContacts.ACCOUNT_TYPE,
+                RawContacts.SOURCE_ID,
+                RawContacts.VERSION,
+                RawContacts.DIRTY,
+                RawContacts.DELETED,
+                RawContacts.DISPLAY_NAME_PRIMARY,
+                RawContacts.DISPLAY_NAME_ALTERNATIVE,
+                RawContacts.DISPLAY_NAME_SOURCE,
+                RawContacts.PHONETIC_NAME,
+                RawContacts.PHONETIC_NAME_STYLE,
+                RawContacts.NAME_VERIFIED,
+                RawContacts.SORT_KEY_PRIMARY,
+                RawContacts.SORT_KEY_ALTERNATIVE,
+                RawContacts.TIMES_CONTACTED,
+                RawContacts.LAST_TIME_CONTACTED,
+                RawContacts.CUSTOM_RINGTONE,
+                RawContacts.SEND_TO_VOICEMAIL,
+                RawContacts.STARRED,
+                RawContacts.AGGREGATION_MODE,
+                RawContacts.SYNC1,
+                RawContacts.SYNC2,
+                RawContacts.SYNC3,
+                RawContacts.SYNC4,
+        });
+    }
+
+    public void testDataProjection() {
+        assertProjection(Data.CONTENT_URI, new String[]{
+                Data._ID,
+                Data.RAW_CONTACT_ID,
+                Data.DATA_VERSION,
+                Data.IS_PRIMARY,
+                Data.IS_SUPER_PRIMARY,
+                Data.RES_PACKAGE,
+                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.SYNC1,
+                Data.SYNC2,
+                Data.SYNC3,
+                Data.SYNC4,
+                Data.CONTACT_ID,
+                Data.PRESENCE,
+                Data.CHAT_CAPABILITY,
+                Data.STATUS,
+                Data.STATUS_TIMESTAMP,
+                Data.STATUS_RES_PACKAGE,
+                Data.STATUS_LABEL,
+                Data.STATUS_ICON,
+                RawContacts.ACCOUNT_NAME,
+                RawContacts.ACCOUNT_TYPE,
+                RawContacts.SOURCE_ID,
+                RawContacts.VERSION,
+                RawContacts.DIRTY,
+                RawContacts.NAME_VERIFIED,
+                Contacts._ID,
+                Contacts.DISPLAY_NAME_PRIMARY,
+                Contacts.DISPLAY_NAME_ALTERNATIVE,
+                Contacts.DISPLAY_NAME_SOURCE,
+                Contacts.PHONETIC_NAME,
+                Contacts.PHONETIC_NAME_STYLE,
+                Contacts.SORT_KEY_PRIMARY,
+                Contacts.SORT_KEY_ALTERNATIVE,
+                Contacts.LAST_TIME_CONTACTED,
+                Contacts.TIMES_CONTACTED,
+                Contacts.STARRED,
+                Contacts.IN_VISIBLE_GROUP,
+                Contacts.PHOTO_ID,
+                Contacts.PHOTO_URI,
+                Contacts.PHOTO_THUMBNAIL_URI,
+                Contacts.CUSTOM_RINGTONE,
+                Contacts.SEND_TO_VOICEMAIL,
+                Contacts.LOOKUP_KEY,
+                Contacts.NAME_RAW_CONTACT_ID,
+                Contacts.HAS_PHONE_NUMBER,
+                Contacts.CONTACT_PRESENCE,
+                Contacts.CONTACT_CHAT_CAPABILITY,
+                Contacts.CONTACT_STATUS,
+                Contacts.CONTACT_STATUS_TIMESTAMP,
+                Contacts.CONTACT_STATUS_RES_PACKAGE,
+                Contacts.CONTACT_STATUS_LABEL,
+                Contacts.CONTACT_STATUS_ICON,
+                GroupMembership.GROUP_SOURCE_ID,
+        });
+    }
+
+    public void testDistinctDataProjection() {
+        assertProjection(Phone.CONTENT_FILTER_URI.buildUpon().appendPath("123").build(),
+            new String[]{
+                Data._ID,
+                Data.DATA_VERSION,
+                Data.IS_PRIMARY,
+                Data.IS_SUPER_PRIMARY,
+                Data.RES_PACKAGE,
+                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.SYNC1,
+                Data.SYNC2,
+                Data.SYNC3,
+                Data.SYNC4,
+                Data.CONTACT_ID,
+                Data.PRESENCE,
+                Data.CHAT_CAPABILITY,
+                Data.STATUS,
+                Data.STATUS_TIMESTAMP,
+                Data.STATUS_RES_PACKAGE,
+                Data.STATUS_LABEL,
+                Data.STATUS_ICON,
+                Contacts._ID,
+                Contacts.DISPLAY_NAME_PRIMARY,
+                Contacts.DISPLAY_NAME_ALTERNATIVE,
+                Contacts.DISPLAY_NAME_SOURCE,
+                Contacts.PHONETIC_NAME,
+                Contacts.PHONETIC_NAME_STYLE,
+                Contacts.SORT_KEY_PRIMARY,
+                Contacts.SORT_KEY_ALTERNATIVE,
+                Contacts.LAST_TIME_CONTACTED,
+                Contacts.TIMES_CONTACTED,
+                Contacts.STARRED,
+                Contacts.IN_VISIBLE_GROUP,
+                Contacts.PHOTO_ID,
+                Contacts.PHOTO_URI,
+                Contacts.PHOTO_THUMBNAIL_URI,
+                Contacts.HAS_PHONE_NUMBER,
+                Contacts.CUSTOM_RINGTONE,
+                Contacts.SEND_TO_VOICEMAIL,
+                Contacts.LOOKUP_KEY,
+                Contacts.CONTACT_PRESENCE,
+                Contacts.CONTACT_CHAT_CAPABILITY,
+                Contacts.CONTACT_STATUS,
+                Contacts.CONTACT_STATUS_TIMESTAMP,
+                Contacts.CONTACT_STATUS_RES_PACKAGE,
+                Contacts.CONTACT_STATUS_LABEL,
+                Contacts.CONTACT_STATUS_ICON,
+                GroupMembership.GROUP_SOURCE_ID,
+        });
+    }
+
+    public void testEntityProjection() {
+        assertProjection(
+            Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, 0),
+                    Contacts.Entity.CONTENT_DIRECTORY),
+            new String[]{
+                Contacts.Entity._ID,
+                Contacts.Entity.DATA_ID,
+                Contacts.Entity.RAW_CONTACT_ID,
+                Data.DATA_VERSION,
+                Data.IS_PRIMARY,
+                Data.IS_SUPER_PRIMARY,
+                Data.RES_PACKAGE,
+                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.SYNC1,
+                Data.SYNC2,
+                Data.SYNC3,
+                Data.SYNC4,
+                Data.CONTACT_ID,
+                Data.PRESENCE,
+                Data.CHAT_CAPABILITY,
+                Data.STATUS,
+                Data.STATUS_TIMESTAMP,
+                Data.STATUS_RES_PACKAGE,
+                Data.STATUS_LABEL,
+                Data.STATUS_ICON,
+                RawContacts.ACCOUNT_NAME,
+                RawContacts.ACCOUNT_TYPE,
+                RawContacts.SOURCE_ID,
+                RawContacts.VERSION,
+                RawContacts.DELETED,
+                RawContacts.DIRTY,
+                RawContacts.NAME_VERIFIED,
+                RawContacts.SYNC1,
+                RawContacts.SYNC2,
+                RawContacts.SYNC3,
+                RawContacts.SYNC4,
+                RawContacts.IS_RESTRICTED,
+                Contacts._ID,
+                Contacts.DISPLAY_NAME_PRIMARY,
+                Contacts.DISPLAY_NAME_ALTERNATIVE,
+                Contacts.DISPLAY_NAME_SOURCE,
+                Contacts.PHONETIC_NAME,
+                Contacts.PHONETIC_NAME_STYLE,
+                Contacts.SORT_KEY_PRIMARY,
+                Contacts.SORT_KEY_ALTERNATIVE,
+                Contacts.LAST_TIME_CONTACTED,
+                Contacts.TIMES_CONTACTED,
+                Contacts.STARRED,
+                Contacts.IN_VISIBLE_GROUP,
+                Contacts.PHOTO_ID,
+                Contacts.PHOTO_URI,
+                Contacts.PHOTO_THUMBNAIL_URI,
+                Contacts.CUSTOM_RINGTONE,
+                Contacts.SEND_TO_VOICEMAIL,
+                Contacts.LOOKUP_KEY,
+                Contacts.NAME_RAW_CONTACT_ID,
+                Contacts.HAS_PHONE_NUMBER,
+                Contacts.CONTACT_PRESENCE,
+                Contacts.CONTACT_CHAT_CAPABILITY,
+                Contacts.CONTACT_STATUS,
+                Contacts.CONTACT_STATUS_TIMESTAMP,
+                Contacts.CONTACT_STATUS_RES_PACKAGE,
+                Contacts.CONTACT_STATUS_LABEL,
+                Contacts.CONTACT_STATUS_ICON,
+                GroupMembership.GROUP_SOURCE_ID,
+        });
+    }
+
+    public void testRawEntityProjection() {
+        assertProjection(RawContactsEntity.CONTENT_URI, new String[]{
+                RawContacts.Entity.DATA_ID,
+                RawContacts._ID,
+                RawContacts.CONTACT_ID,
+                RawContacts.ACCOUNT_NAME,
+                RawContacts.ACCOUNT_TYPE,
+                RawContacts.SOURCE_ID,
+                RawContacts.VERSION,
+                RawContacts.DIRTY,
+                RawContacts.NAME_VERIFIED,
+                RawContacts.DELETED,
+                RawContacts.IS_RESTRICTED,
+                RawContacts.SYNC1,
+                RawContacts.SYNC2,
+                RawContacts.SYNC3,
+                RawContacts.SYNC4,
+                RawContacts.STARRED,
+                Data.DATA_VERSION,
+                Data.IS_PRIMARY,
+                Data.IS_SUPER_PRIMARY,
+                Data.RES_PACKAGE,
+                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.SYNC1,
+                Data.SYNC2,
+                Data.SYNC3,
+                Data.SYNC4,
+                GroupMembership.GROUP_SOURCE_ID,
+        });
+    }
+
+    public void testPhoneLookupProjection() {
+        assertProjection(PhoneLookup.CONTENT_FILTER_URI.buildUpon().appendPath("123").build(),
+            new String[]{
+                PhoneLookup._ID,
+                PhoneLookup.LOOKUP_KEY,
+                PhoneLookup.DISPLAY_NAME,
+                PhoneLookup.LAST_TIME_CONTACTED,
+                PhoneLookup.TIMES_CONTACTED,
+                PhoneLookup.STARRED,
+                PhoneLookup.IN_VISIBLE_GROUP,
+                PhoneLookup.PHOTO_ID,
+                PhoneLookup.PHOTO_URI,
+                PhoneLookup.PHOTO_THUMBNAIL_URI,
+                PhoneLookup.CUSTOM_RINGTONE,
+                PhoneLookup.HAS_PHONE_NUMBER,
+                PhoneLookup.SEND_TO_VOICEMAIL,
+                PhoneLookup.NUMBER,
+                PhoneLookup.TYPE,
+                PhoneLookup.LABEL,
+                PhoneLookup.NORMALIZED_NUMBER,
+        });
+    }
+
+    public void testGroupsProjection() {
+        assertProjection(Groups.CONTENT_URI, new String[]{
+                Groups._ID,
+                Groups.ACCOUNT_NAME,
+                Groups.ACCOUNT_TYPE,
+                Groups.SOURCE_ID,
+                Groups.DIRTY,
+                Groups.VERSION,
+                Groups.RES_PACKAGE,
+                Groups.TITLE,
+                Groups.TITLE_RES,
+                Groups.GROUP_VISIBLE,
+                Groups.SYSTEM_ID,
+                Groups.DELETED,
+                Groups.NOTES,
+                Groups.SHOULD_SYNC,
+                Groups.FAVORITES,
+                Groups.AUTO_ADD,
+                Groups.SYNC1,
+                Groups.SYNC2,
+                Groups.SYNC3,
+                Groups.SYNC4,
+        });
+    }
+
+    public void testGroupsSummaryProjection() {
+        assertProjection(Groups.CONTENT_SUMMARY_URI, new String[]{
+                Groups._ID,
+                Groups.ACCOUNT_NAME,
+                Groups.ACCOUNT_TYPE,
+                Groups.SOURCE_ID,
+                Groups.DIRTY,
+                Groups.VERSION,
+                Groups.RES_PACKAGE,
+                Groups.TITLE,
+                Groups.TITLE_RES,
+                Groups.GROUP_VISIBLE,
+                Groups.SYSTEM_ID,
+                Groups.DELETED,
+                Groups.NOTES,
+                Groups.SHOULD_SYNC,
+                Groups.FAVORITES,
+                Groups.AUTO_ADD,
+                Groups.SYNC1,
+                Groups.SYNC2,
+                Groups.SYNC3,
+                Groups.SYNC4,
+                Groups.SUMMARY_COUNT,
+                Groups.SUMMARY_WITH_PHONES,
+        });
+    }
+
+    public void testAggregateExceptionProjection() {
+        assertProjection(AggregationExceptions.CONTENT_URI, new String[]{
+                AggregationExceptionColumns._ID,
+                AggregationExceptions.TYPE,
+                AggregationExceptions.RAW_CONTACT_ID1,
+                AggregationExceptions.RAW_CONTACT_ID2,
+        });
+    }
+
+    public void testSettingsProjection() {
+        assertProjection(Settings.CONTENT_URI, new String[]{
+                Settings.ACCOUNT_NAME,
+                Settings.ACCOUNT_TYPE,
+                Settings.UNGROUPED_VISIBLE,
+                Settings.SHOULD_SYNC,
+                Settings.ANY_UNSYNCED,
+                Settings.UNGROUPED_COUNT,
+                Settings.UNGROUPED_WITH_PHONES,
+        });
+    }
+
+    public void testStatusUpdatesProjection() {
+        assertProjection(StatusUpdates.CONTENT_URI, new String[]{
+                PresenceColumns.RAW_CONTACT_ID,
+                StatusUpdates.DATA_ID,
+                StatusUpdates.IM_ACCOUNT,
+                StatusUpdates.IM_HANDLE,
+                StatusUpdates.PROTOCOL,
+                StatusUpdates.CUSTOM_PROTOCOL,
+                StatusUpdates.PRESENCE,
+                StatusUpdates.CHAT_CAPABILITY,
+                StatusUpdates.STATUS,
+                StatusUpdates.STATUS_TIMESTAMP,
+                StatusUpdates.STATUS_RES_PACKAGE,
+                StatusUpdates.STATUS_ICON,
+                StatusUpdates.STATUS_LABEL,
+        });
+    }
+
+    public void testLiveFoldersProjection() {
+        assertProjection(
+            Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "live_folders/contacts"),
+            new String[]{
+                LiveFolders._ID,
+                LiveFolders.NAME,
+        });
+    }
+
+    public void testDirectoryProjection() {
+        assertProjection(Directory.CONTENT_URI, new String[]{
+                Directory._ID,
+                Directory.PACKAGE_NAME,
+                Directory.TYPE_RESOURCE_ID,
+                Directory.DISPLAY_NAME,
+                Directory.DIRECTORY_AUTHORITY,
+                Directory.ACCOUNT_TYPE,
+                Directory.ACCOUNT_NAME,
+                Directory.EXPORT_SUPPORT,
+                Directory.SHORTCUT_SUPPORT,
+                Directory.PHOTO_SUPPORT,
+        });
+    }
+
     public void testRawContactsInsert() {
         ContentValues values = new ContentValues();
 
@@ -106,6 +617,169 @@
         assertNetworkNotified(true);
     }
 
+    public void testDataDirectoryWithLookupUri() {
+        ContentValues values = new ContentValues();
+
+        long rawContactId = createRawContactWithName();
+        insertPhoneNumber(rawContactId, "555-GOOG-411");
+        insertEmail(rawContactId, "google@android.com");
+
+        long contactId = queryContactId(rawContactId);
+        String lookupKey = queryLookupKey(contactId);
+
+        // Complete and valid lookup URI
+        Uri lookupUri = ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
+        Uri dataUri = Uri.withAppendedPath(lookupUri, Contacts.Data.CONTENT_DIRECTORY);
+
+        assertDataRows(dataUri, values);
+
+        // Complete but stale lookup URI
+        lookupUri = ContactsContract.Contacts.getLookupUri(contactId + 1, lookupKey);
+        dataUri = Uri.withAppendedPath(lookupUri, Contacts.Data.CONTENT_DIRECTORY);
+        assertDataRows(dataUri, values);
+
+        // Incomplete lookup URI (lookup key only, no contact ID)
+        dataUri = Uri.withAppendedPath(Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI,
+                lookupKey), Contacts.Data.CONTENT_DIRECTORY);
+        assertDataRows(dataUri, values);
+    }
+
+    private void assertDataRows(Uri dataUri, ContentValues values) {
+        Cursor cursor = mResolver.query(dataUri, new String[]{ Data.DATA1 }, null, null, Data._ID);
+        assertEquals(3, cursor.getCount());
+        cursor.moveToFirst();
+        values.put(Data.DATA1, "John Doe");
+        assertCursorValues(cursor, values);
+
+        cursor.moveToNext();
+        values.put(Data.DATA1, "555-GOOG-411");
+        assertCursorValues(cursor, values);
+
+        cursor.moveToNext();
+        values.put(Data.DATA1, "google@android.com");
+        assertCursorValues(cursor, values);
+
+        cursor.close();
+    }
+
+    public void testContactEntitiesWithIdBasedUri() {
+        ContentValues values = new ContentValues();
+        Account account1 = new Account("act1", "actype1");
+        Account account2 = new Account("act2", "actype2");
+
+        long rawContactId1 = createRawContactWithName(account1);
+        insertImHandle(rawContactId1, Im.PROTOCOL_GOOGLE_TALK, null, "gtalk");
+        insertStatusUpdate(Im.PROTOCOL_GOOGLE_TALK, null, "gtalk", StatusUpdates.IDLE, "Busy", 90,
+                StatusUpdates.CAPABILITY_HAS_CAMERA);
+
+        long rawContactId2 = createRawContact(account2);
+        setAggregationException(
+                AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
+
+        long contactId = queryContactId(rawContactId1);
+
+        Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
+
+        assertEntityRows(entityUri, contactId, rawContactId1, rawContactId2);
+    }
+
+    public void testContactEntitiesWithLookupUri() {
+        ContentValues values = new ContentValues();
+        Account account1 = new Account("act1", "actype1");
+        Account account2 = new Account("act2", "actype2");
+
+        long rawContactId1 = createRawContactWithName(account1);
+        insertImHandle(rawContactId1, Im.PROTOCOL_GOOGLE_TALK, null, "gtalk");
+        insertStatusUpdate(Im.PROTOCOL_GOOGLE_TALK, null, "gtalk", StatusUpdates.IDLE, "Busy", 90,
+                StatusUpdates.CAPABILITY_HAS_CAMERA);
+
+        long rawContactId2 = createRawContact(account2);
+        setAggregationException(
+                AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
+
+        long contactId = queryContactId(rawContactId1);
+        String lookupKey = queryLookupKey(contactId);
+
+        // First try with a matching contact ID
+        Uri contactLookupUri = ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
+        Uri entityUri = Uri.withAppendedPath(contactLookupUri, Contacts.Entity.CONTENT_DIRECTORY);
+        assertEntityRows(entityUri, contactId, rawContactId1, rawContactId2);
+
+        // Now try with a contact ID mismatch
+        contactLookupUri = ContactsContract.Contacts.getLookupUri(contactId + 1, lookupKey);
+        entityUri = Uri.withAppendedPath(contactLookupUri, Contacts.Entity.CONTENT_DIRECTORY);
+        assertEntityRows(entityUri, contactId, rawContactId1, rawContactId2);
+
+        // Now try without an ID altogether
+        contactLookupUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+        entityUri = Uri.withAppendedPath(contactLookupUri, Contacts.Entity.CONTENT_DIRECTORY);
+        assertEntityRows(entityUri, contactId, rawContactId1, rawContactId2);
+    }
+
+    private void assertEntityRows(Uri entityUri, long contactId, long rawContactId1,
+            long rawContactId2) {
+        ContentValues values = new ContentValues();
+
+        Cursor cursor = mResolver.query(entityUri, null, null, null,
+                Contacts.Entity.RAW_CONTACT_ID + "," + Contacts.Entity.DATA_ID);
+        assertEquals(3, cursor.getCount());
+
+        // First row - name
+        cursor.moveToFirst();
+        values.put(Contacts.Entity.CONTACT_ID, contactId);
+        values.put(Contacts.Entity.RAW_CONTACT_ID, rawContactId1);
+        values.put(Contacts.Entity.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        values.put(Contacts.Entity.DATA1, "John Doe");
+        values.put(Contacts.Entity.ACCOUNT_NAME, "act1");
+        values.put(Contacts.Entity.ACCOUNT_TYPE, "actype1");
+        values.put(Contacts.Entity.DISPLAY_NAME, "John Doe");
+        values.put(Contacts.Entity.DISPLAY_NAME_ALTERNATIVE, "Doe, John");
+        values.put(Contacts.Entity.NAME_RAW_CONTACT_ID, rawContactId1);
+        values.put(Contacts.Entity.CONTACT_CHAT_CAPABILITY, StatusUpdates.CAPABILITY_HAS_CAMERA);
+        values.put(Contacts.Entity.CONTACT_PRESENCE, StatusUpdates.IDLE);
+        values.put(Contacts.Entity.CONTACT_STATUS, "Busy");
+        values.putNull(Contacts.Entity.PRESENCE);
+        assertCursorValues(cursor, values);
+
+        // Second row - IM
+        cursor.moveToNext();
+        values.put(Contacts.Entity.CONTACT_ID, contactId);
+        values.put(Contacts.Entity.RAW_CONTACT_ID, rawContactId1);
+        values.put(Contacts.Entity.MIMETYPE, Im.CONTENT_ITEM_TYPE);
+        values.put(Contacts.Entity.DATA1, "gtalk");
+        values.put(Contacts.Entity.ACCOUNT_NAME, "act1");
+        values.put(Contacts.Entity.ACCOUNT_TYPE, "actype1");
+        values.put(Contacts.Entity.DISPLAY_NAME, "John Doe");
+        values.put(Contacts.Entity.DISPLAY_NAME_ALTERNATIVE, "Doe, John");
+        values.put(Contacts.Entity.NAME_RAW_CONTACT_ID, rawContactId1);
+        values.put(Contacts.Entity.CONTACT_CHAT_CAPABILITY, StatusUpdates.CAPABILITY_HAS_CAMERA);
+        values.put(Contacts.Entity.CONTACT_PRESENCE, StatusUpdates.IDLE);
+        values.put(Contacts.Entity.CONTACT_STATUS, "Busy");
+        values.put(Contacts.Entity.PRESENCE, StatusUpdates.IDLE);
+        assertCursorValues(cursor, values);
+
+        // Third row - second raw contact, not data
+        cursor.moveToNext();
+        values.put(Contacts.Entity.CONTACT_ID, contactId);
+        values.put(Contacts.Entity.RAW_CONTACT_ID, rawContactId2);
+        values.putNull(Contacts.Entity.MIMETYPE);
+        values.putNull(Contacts.Entity.DATA_ID);
+        values.putNull(Contacts.Entity.DATA1);
+        values.put(Contacts.Entity.ACCOUNT_NAME, "act2");
+        values.put(Contacts.Entity.ACCOUNT_TYPE, "actype2");
+        values.put(Contacts.Entity.DISPLAY_NAME, "John Doe");
+        values.put(Contacts.Entity.DISPLAY_NAME_ALTERNATIVE, "Doe, John");
+        values.put(Contacts.Entity.NAME_RAW_CONTACT_ID, rawContactId1);
+        values.put(Contacts.Entity.CONTACT_CHAT_CAPABILITY, StatusUpdates.CAPABILITY_HAS_CAMERA);
+        values.put(Contacts.Entity.CONTACT_PRESENCE, StatusUpdates.IDLE);
+        values.put(Contacts.Entity.CONTACT_STATUS, "Busy");
+        values.putNull(Contacts.Entity.PRESENCE);
+        assertCursorValues(cursor, values);
+
+        cursor.close();
+    }
+
     public void testDataInsert() {
         long rawContactId = createRawContactWithName("John", "Doe");
 
@@ -237,16 +911,77 @@
         values.put(PhoneLookup.SEND_TO_VOICEMAIL, 1);
         assertStoredValues(lookupUri1, values);
 
-        // The strict comparation, adopted in Donut, does not allow the behavior like
-        // "8004664411 == 4664411", while the loose comparation, which had been used in Cupcake
-        // and reverted back into the default in Eclair, allows it. Hmm...
-        final boolean useStrictComparation =
-            mContext.getResources().getBoolean(
-                    com.android.internal.R.bool.config_use_strict_phone_number_comparation);
-        final int expectedResult = (useStrictComparation ? 0 : 1);
-
+        // In the context that 8004664411 is a valid number, "4664411" as a
+        // call id should not match to "8004664411"
         Uri lookupUri2 = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, "4664411");
-        assertEquals(expectedResult, getCount(lookupUri2, null, null));
+        assertEquals(0, getCount(lookupUri2, null, null));
+    }
+
+    public void testPhoneLookupUseCases() {
+        ContentValues values = new ContentValues();
+        Uri rawContactUri;
+        long rawContactId;
+        Uri lookupUri2;
+
+        values.put(RawContacts.CUSTOM_RINGTONE, "d");
+        values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+        // International format in contacts
+        rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
+        rawContactId = ContentUris.parseId(rawContactUri);
+
+        insertStructuredName(rawContactId, "Hot", "Tamale");
+        insertPhoneNumber(rawContactId, "+1-650-861-0000");
+
+        values.clear();
+
+        // match with international format
+        lookupUri2 = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, "+1 650 861 0000");
+        assertEquals(1, getCount(lookupUri2, null, null));
+
+        // match with national format
+        lookupUri2 = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, "650 861 0000");
+        assertEquals(1, getCount(lookupUri2, null, null));
+
+        // National format in contacts
+        values.clear();
+        values.put(RawContacts.CUSTOM_RINGTONE, "d");
+        values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+        rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
+        rawContactId = ContentUris.parseId(rawContactUri);
+
+        insertStructuredName(rawContactId, "Hot1", "Tamale");
+        insertPhoneNumber(rawContactId, "650-861-0001");
+
+        values.clear();
+
+        // match with international format
+        lookupUri2 = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, "+1 650 861 0001");
+        assertEquals(2, getCount(lookupUri2, null, null));
+
+        // match with national format
+        lookupUri2 = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, "650 861 0001");
+        assertEquals(2, getCount(lookupUri2, null, null));
+
+        // Local format in contacts
+        values.clear();
+        values.put(RawContacts.CUSTOM_RINGTONE, "d");
+        values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+        rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
+        rawContactId = ContentUris.parseId(rawContactUri);
+
+        insertStructuredName(rawContactId, "Hot2", "Tamale");
+        insertPhoneNumber(rawContactId, "861-0002");
+
+        values.clear();
+
+        // match with international format
+        lookupUri2 = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, "+1 650 861 0002");
+        assertEquals(1, getCount(lookupUri2, null, null));
+
+        // match with national format
+        lookupUri2 = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, "650 861 0002");
+        assertEquals(1, getCount(lookupUri2, null, null));
     }
 
     public void testPhoneUpdate() {
@@ -367,6 +1102,93 @@
         assertEquals(0, getCount(filterUri5, null, null));
     }
 
+    public void testEmailFilterSortOrder() {
+
+        // Adding contacts from the end to beginning of the expected order.
+
+        // Never contacted
+        insertContactWithEmail("never", false);
+        insertContactWithEmail("starred-never", true);
+
+        // Contacted a long time ago
+        insertContactWithEmail("a-longago", 10, 1800, false);
+        insertContactWithEmail("b-longago", 20, 1000, false);
+        insertContactWithEmail("c-longago", 30, 2000, false);
+
+        // Contacted fairly recently
+        insertContactWithEmail("a-recent", 10, 18, false);
+        insertContactWithEmail("b-recent", 20, 10, false);
+        insertContactWithEmail("c-recent", 30, 20, false);
+
+        // Contacted very recently
+        insertContactWithEmail("a-current", 10, 1, false);
+        insertContactWithEmail("b-current", 20, 0, false);
+        insertContactWithEmail("c-current", 30, 2, false);
+
+        // Starred
+        insertContactWithEmail("starred-longago", 10, 100, true);
+        insertContactWithEmail("starred-current", 10, 10, true);
+        insertContactWithEmail("starred-recent", 10, 1, true);
+
+        Uri filterUri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "findme");
+        Cursor cursor = mResolver.query(filterUri, new String[]{Contacts.DISPLAY_NAME},
+                null, null, null);
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "starred-recent");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "starred-current");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "starred-longago");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "starred-never");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "c-current");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "b-current");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "a-current");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "c-recent");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "b-recent");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "a-recent");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "c-longago");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "b-longago");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "a-longago");
+        cursor.moveToNext();
+        assertCursorValue(cursor, Contacts.DISPLAY_NAME, "never");
+        cursor.close();
+    }
+
+    private void insertContactWithEmail(String name, boolean starred) {
+        long rawContactId = createRawContactWithName(name, null);
+        long contactId = queryContactId(rawContactId);
+        if (starred) {
+            storeValue(Contacts.CONTENT_URI, contactId, Contacts.STARRED, 1);
+        }
+        insertEmail(rawContactId, "findme" + name + "@acme.com");
+    }
+
+    private void insertContactWithEmail(
+            String name, int timesContacted, int lastTimeContactedDays, boolean starred) {
+        long rawContactId = createRawContactWithName(name, null);
+        long contactId = queryContactId(rawContactId);
+        ContentValues values = new ContentValues();
+        values.put(Contacts.TIMES_CONTACTED, timesContacted);
+        values.put(Contacts.LAST_TIME_CONTACTED,
+                System.currentTimeMillis() - (lastTimeContactedDays * 24 * 60 * 60 * 1000l));
+        if (starred) {
+            values.put(Contacts.STARRED, 1);
+        }
+        mResolver.update(
+                ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), values, null, null);
+        insertEmail(rawContactId, "findme" + name + "@acme.com");
+    }
+
     public void testPostalsQuery() {
         long rawContactId = createRawContactWithName("Alice", "Nextore");
         Uri dataUri = insertPostalAddress(rawContactId, "1600 Amphiteatre Ave, Mountain View");
@@ -390,7 +1212,7 @@
         ContentValues values = new ContentValues();
         long contactId = createContact(values, "John", "Doe",
                 "18004664411", "goog411@acme.com", StatusUpdates.INVISIBLE, 4, 1, 0,
-                StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO_PLAYBACK_ONLY);
+                StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO);
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
 
         assertStoredValues(contactUri, values);
@@ -413,7 +1235,7 @@
         ContentValues values = new ContentValues();
         long rawContactId = createRawContact(values, "18004664411",
                 "goog411@acme.com", StatusUpdates.INVISIBLE, 4, 1, 0,
-                StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO_PLAYBACK_ONLY |
+                StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO |
                 StatusUpdates.CAPABILITY_HAS_VOICE);
 
         ContentValues nameValues = new ContentValues();
@@ -455,7 +1277,7 @@
         ContentValues values1 = new ContentValues();
         createContact(values1, "Noah", "Tever", "18004664411",
                 "a@acme.com", StatusUpdates.OFFLINE, 0, 0, 0,
-                StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO_PLAYBACK_ONLY);
+                StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO);
         ContentValues values2 = new ContentValues();
         createContact(values2, "Sam", "Times", "18004664412",
                 "b@acme.com", StatusUpdates.INVISIBLE, 3, 0, 0,
@@ -463,11 +1285,11 @@
         ContentValues values3 = new ContentValues();
         createContact(values3, "Lotta", "Calling", "18004664413",
                 "c@acme.com", StatusUpdates.AWAY, 5, 0, 0,
-                StatusUpdates.CAPABILITY_HAS_VIDEO_PLAYBACK_ONLY);
+                StatusUpdates.CAPABILITY_HAS_VIDEO);
         ContentValues values4 = new ContentValues();
         createContact(values4, "Fay", "Veritt", "18004664414",
                 "d@acme.com", StatusUpdates.AVAILABLE, 0, 1, 0,
-                StatusUpdates.CAPABILITY_HAS_VIDEO_PLAYBACK_ONLY | StatusUpdates.CAPABILITY_HAS_VOICE);
+                StatusUpdates.CAPABILITY_HAS_VIDEO | StatusUpdates.CAPABILITY_HAS_VOICE);
 
         Cursor c = mResolver.query(Contacts.CONTENT_STREQUENT_URI, null, null, null,
                 Contacts._ID);
@@ -528,7 +1350,6 @@
         insertStructuredName(rawContactId, "John", "Doe");
         Uri photoUri = insertPhoto(rawContactId);
         long photoId = ContentUris.parseId(photoUri);
-        values.put(Contacts.PHOTO_ID, photoId);
         insertPhoneNumber(rawContactId, "18004664411");
         insertPhoneNumber(rawContactId, "18004664412");
         insertEmail(rawContactId, "goog411@acme.com");
@@ -1238,7 +2059,7 @@
 
         // Match on IM (custom)
         insertStatusUpdate(Im.PROTOCOL_CUSTOM, "my_im_proto", "my_im", StatusUpdates.IDLE, "Idle",
-                StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO_PLAYBACK_ONLY);
+                StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO);
 
         // Match on Email
         insertStatusUpdate(Im.PROTOCOL_GOOGLE_TALK, null, "m@acme.com", StatusUpdates.AWAY, "Away",
@@ -1399,7 +2220,7 @@
         ContentValues values = new ContentValues();
         values.put(StatusUpdates.PROTOCOL, protocol);
         values.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
-        values.put(StatusUpdates.PRESENCE_STATUS, presence);
+        values.put(StatusUpdates.PRESENCE, presence);
         values.put(StatusUpdates.STATUS, status);
         assertCursorValues(c, values);
     }
@@ -1533,7 +2354,7 @@
                 null, Contacts.IN_VISIBLE_GROUP, expectedValue);
     }
 
-    public void testContentEntityIterator() throws RemoteException {
+    public void testContentEntityIterator() {
         // create multiple contacts and check that the selected ones are returned
         long id;
 
@@ -1794,8 +2615,8 @@
         ContactsProvider2 cp = (ContactsProvider2) getProvider();
         cp.onAccountsUpdated(new Account[]{mAccount, mAccountTwo});
         assertEquals(1, getCount(RawContacts.CONTENT_URI, null, null));
-        assertStoredValue(rawContact3, RawContacts.ACCOUNT_NAME, "account1");
-        assertStoredValue(rawContact3, RawContacts.ACCOUNT_TYPE, "account type1");
+        assertStoredValue(rawContact3, RawContacts.ACCOUNT_NAME, null);
+        assertStoredValue(rawContact3, RawContacts.ACCOUNT_TYPE, null);
 
         long rawContactId1 = createRawContact(mAccount);
         insertEmail(rawContactId1, "account1@email.com");
@@ -1849,7 +2670,6 @@
         // The photo should be the remaining one
         assertStoredValue(Contacts.CONTENT_URI, contactId,
                 Contacts.PHOTO_ID, ContentUris.parseId(photoUri1));
-
     }
 
     public void testContactDeletion() {
@@ -2016,21 +2836,36 @@
         Uri twigUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI,
                 queryContactId(rawContactId)), Contacts.Photo.CONTENT_DIRECTORY);
 
+        assertStoredValue(
+                ContentUris.withAppendedId(Contacts.CONTENT_URI, queryContactId(rawContactId)),
+                Contacts.PHOTO_URI, twigUri.toString());
+
         long twigId = Long.parseLong(getStoredValue(twigUri, Data._ID));
         assertEquals(ContentUris.parseId(photoUri), twigId);
     }
 
-    public void testOpenAssertFileDescriptorForPhoto() throws Exception {
+    public void testInputStreamForPhoto() throws Exception {
         long rawContactId = createRawContact();
         Uri photoUri = insertPhoto(rawContactId);
-        AssetFileDescriptor fd = mResolver.openAssetFileDescriptor(photoUri, "r");
-        assertEquals(loadTestPhoto().length, fd.getLength());
+        assertInputStreamContent(loadTestPhoto(), mResolver.openInputStream(photoUri));
 
         Uri contactPhotoUri = Uri.withAppendedPath(
                 ContentUris.withAppendedId(Contacts.CONTENT_URI, queryContactId(rawContactId)),
                 Contacts.Photo.CONTENT_DIRECTORY);
-        fd = mResolver.openAssetFileDescriptor(contactPhotoUri, "r");
-        assertEquals(loadTestPhoto().length, fd.getLength());
+        assertInputStreamContent(loadTestPhoto(), mResolver.openInputStream(contactPhotoUri));
+    }
+
+    private static void assertInputStreamContent(byte[] expected, InputStream is)
+            throws IOException {
+        try {
+            byte[] observed = new byte[expected.length];
+            int count = is.read(observed);
+            assertEquals(expected.length, count);
+            assertEquals(-1, is.read());
+            MoreAsserts.assertEquals(expected, observed);
+        } finally {
+            is.close();
+        }
     }
 
     public void testSuperPrimaryPhoto() {
@@ -2048,6 +2883,8 @@
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
                 queryContactId(rawContactId1));
         assertStoredValue(contactUri, Contacts.PHOTO_ID, photoId1);
+        assertStoredValue(contactUri, Contacts.PHOTO_URI,
+                Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY));
 
         setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
                 rawContactId1, rawContactId2);
@@ -2466,6 +3303,380 @@
         }
     }
 
+    public void testAutoGroupMembership() {
+        long g1 = createGroup(mAccount, "g1", "t1", 0, true /* autoAdd */, false /* favorite */);
+        long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false /* favorite */);
+        long g3 = createGroup(mAccountTwo, "g3", "t3", 0, true /* autoAdd */, false /* favorite */);
+        long g4 = createGroup(mAccountTwo, "g4", "t4", 0, false /* autoAdd */, false/* favorite */);
+        long r1 = createRawContact(mAccount);
+        long r2 = createRawContact(mAccountTwo);
+        long r3 = createRawContact(null);
+
+        Cursor c = queryGroupMemberships(mAccount);
+        try {
+            assertTrue(c.moveToNext());
+            assertEquals(g1, c.getLong(0));
+            assertEquals(r1, c.getLong(1));
+            assertFalse(c.moveToNext());
+        } finally {
+            c.close();
+        }
+
+        c = queryGroupMemberships(mAccountTwo);
+        try {
+            assertTrue(c.moveToNext());
+            assertEquals(g3, c.getLong(0));
+            assertEquals(r2, c.getLong(1));
+            assertFalse(c.moveToNext());
+        } finally {
+            c.close();
+        }
+    }
+
+    public void testNoAutoAddMembershipAfterGroupCreation() {
+        long r1 = createRawContact(mAccount);
+        long r2 = createRawContact(mAccount);
+        long r3 = createRawContact(mAccount);
+        long r4 = createRawContact(mAccountTwo);
+        long r5 = createRawContact(mAccountTwo);
+        long r6 = createRawContact(null);
+
+        assertNoRowsAndClose(queryGroupMemberships(mAccount));
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+        long g1 = createGroup(mAccount, "g1", "t1", 0, true /* autoAdd */, false /* favorite */);
+        long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false /* favorite */);
+        long g3 = createGroup(mAccountTwo, "g3", "t3", 0, true /* autoAdd */, false/* favorite */);
+
+        assertNoRowsAndClose(queryGroupMemberships(mAccount));
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+    }
+
+    // create some starred and non-starred contacts, some associated with account, some not
+    // favorites group created
+    // the starred contacts should be added to group
+    // favorites group removed
+    // no change to starred status
+    public void testFavoritesMembershipAfterGroupCreation() {
+        long r1 = createRawContact(mAccount, RawContacts.STARRED, "1");
+        long r2 = createRawContact(mAccount);
+        long r3 = createRawContact(mAccount, RawContacts.STARRED, "1");
+        long r4 = createRawContact(mAccountTwo, RawContacts.STARRED, "1");
+        long r5 = createRawContact(mAccountTwo);
+        long r6 = createRawContact(null, RawContacts.STARRED, "1");
+        long r7 = createRawContact(null);
+
+        assertNoRowsAndClose(queryGroupMemberships(mAccount));
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+        long g1 = createGroup(mAccount, "g1", "t1", 0, false /* autoAdd */, true /* favorite */);
+        long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false /* favorite */);
+        long g3 = createGroup(mAccountTwo, "g3", "t3", 0, false /* autoAdd */, false/* favorite */);
+
+        assertTrue(queryRawContactIsStarred(r1));
+        assertFalse(queryRawContactIsStarred(r2));
+        assertTrue(queryRawContactIsStarred(r3));
+        assertTrue(queryRawContactIsStarred(r4));
+        assertFalse(queryRawContactIsStarred(r5));
+        assertTrue(queryRawContactIsStarred(r6));
+        assertFalse(queryRawContactIsStarred(r7));
+
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+        Cursor c = queryGroupMemberships(mAccount);
+        try {
+            assertTrue(c.moveToNext());
+            assertEquals(g1, c.getLong(0));
+            assertEquals(r1, c.getLong(1));
+            assertTrue(c.moveToNext());
+            assertEquals(g1, c.getLong(0));
+            assertEquals(r3, c.getLong(1));
+            assertFalse(c.moveToNext());
+        } finally {
+            c.close();
+        }
+
+        updateItem(RawContacts.CONTENT_URI, r6,
+                RawContacts.ACCOUNT_NAME, mAccount.name,
+                RawContacts.ACCOUNT_TYPE, mAccount.type);
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+        c = queryGroupMemberships(mAccount);
+        try {
+            assertTrue(c.moveToNext());
+            assertEquals(g1, c.getLong(0));
+            assertEquals(r1, c.getLong(1));
+            assertTrue(c.moveToNext());
+            assertEquals(g1, c.getLong(0));
+            assertEquals(r3, c.getLong(1));
+            assertTrue(c.moveToNext());
+            assertEquals(g1, c.getLong(0));
+            assertEquals(r6, c.getLong(1));
+            assertFalse(c.moveToNext());
+        } finally {
+            c.close();
+        }
+
+        mResolver.delete(ContentUris.withAppendedId(Groups.CONTENT_URI, g1), null, null);
+
+        assertNoRowsAndClose(queryGroupMemberships(mAccount));
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+        assertTrue(queryRawContactIsStarred(r1));
+        assertFalse(queryRawContactIsStarred(r2));
+        assertTrue(queryRawContactIsStarred(r3));
+        assertTrue(queryRawContactIsStarred(r4));
+        assertFalse(queryRawContactIsStarred(r5));
+        assertTrue(queryRawContactIsStarred(r6));
+        assertFalse(queryRawContactIsStarred(r7));
+    }
+
+    public void testFavoritesGroupMembershipChangeAfterStarChange() {
+        long g1 = createGroup(mAccount, "g1", "t1", 0, false /* autoAdd */, true /* favorite */);
+        long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false/* favorite */);
+        long g4 = createGroup(mAccountTwo, "g4", "t4", 0, false /* autoAdd */, true /* favorite */);
+        long g5 = createGroup(mAccountTwo, "g5", "t5", 0, false /* autoAdd */, false/* favorite */);
+        long r1 = createRawContact(mAccount, RawContacts.STARRED, "1");
+        long r2 = createRawContact(mAccount);
+        long r3 = createRawContact(mAccountTwo);
+
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+        Cursor c = queryGroupMemberships(mAccount);
+        try {
+            assertTrue(c.moveToNext());
+            assertEquals(g1, c.getLong(0));
+            assertEquals(r1, c.getLong(1));
+            assertFalse(c.moveToNext());
+        } finally {
+            c.close();
+        }
+
+        // remove the star from r1
+        assertEquals(1, updateItem(RawContacts.CONTENT_URI, r1, RawContacts.STARRED, "0"));
+
+        // Since no raw contacts are starred, there should be no group memberships.
+        assertNoRowsAndClose(queryGroupMemberships(mAccount));
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+        // mark r1 as starred
+        assertEquals(1, updateItem(RawContacts.CONTENT_URI, r1, RawContacts.STARRED, "1"));
+        // Now that r1 is starred it should have a membership in the one groups from mAccount
+        // that is marked as a favorite.
+        // There should be no memberships in mAccountTwo since it has no starred raw contacts.
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+        c = queryGroupMemberships(mAccount);
+        try {
+            assertTrue(c.moveToNext());
+            assertEquals(g1, c.getLong(0));
+            assertEquals(r1, c.getLong(1));
+            assertFalse(c.moveToNext());
+        } finally {
+            c.close();
+        }
+
+        // remove the star from r1
+        assertEquals(1, updateItem(RawContacts.CONTENT_URI, r1, RawContacts.STARRED, "0"));
+        // Since no raw contacts are starred, there should be no group memberships.
+        assertNoRowsAndClose(queryGroupMemberships(mAccount));
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+        Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, queryContactId(r1));
+        assertNotNull(contactUri);
+
+        // mark r1 as starred via its contact lookup uri
+        assertEquals(1, updateItem(contactUri, Contacts.STARRED, "1"));
+        // Now that r1 is starred it should have a membership in the one groups from mAccount
+        // that is marked as a favorite.
+        // There should be no memberships in mAccountTwo since it has no starred raw contacts.
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+        c = queryGroupMemberships(mAccount);
+        try {
+            assertTrue(c.moveToNext());
+            assertEquals(g1, c.getLong(0));
+            assertEquals(r1, c.getLong(1));
+            assertFalse(c.moveToNext());
+        } finally {
+            c.close();
+        }
+
+        // remove the star from r1
+        updateItem(contactUri, Contacts.STARRED, "0");
+        // Since no raw contacts are starred, there should be no group memberships.
+        assertNoRowsAndClose(queryGroupMemberships(mAccount));
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+    }
+
+    public void testStarChangedAfterGroupMembershipChange() {
+        long g1 = createGroup(mAccount, "g1", "t1", 0, false /* autoAdd */, true /* favorite */);
+        long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false/* favorite */);
+        long g4 = createGroup(mAccountTwo, "g4", "t4", 0, false /* autoAdd */, true /* favorite */);
+        long g5 = createGroup(mAccountTwo, "g5", "t5", 0, false /* autoAdd */, false/* favorite */);
+        long r1 = createRawContact(mAccount);
+        long r2 = createRawContact(mAccount);
+        long r3 = createRawContact(mAccountTwo);
+
+        assertFalse(queryRawContactIsStarred(r1));
+        assertFalse(queryRawContactIsStarred(r2));
+        assertFalse(queryRawContactIsStarred(r3));
+
+        Cursor c;
+
+        // add r1 to one favorites group
+        // r1's star should automatically be set
+        // r1 should automatically be added to the other favorites group
+        Uri urir1g1 = insertGroupMembership(r1, g1);
+        assertTrue(queryRawContactIsStarred(r1));
+        assertFalse(queryRawContactIsStarred(r2));
+        assertFalse(queryRawContactIsStarred(r3));
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+        c = queryGroupMemberships(mAccount);
+        try {
+            assertTrue(c.moveToNext());
+            assertEquals(g1, c.getLong(0));
+            assertEquals(r1, c.getLong(1));
+            assertFalse(c.moveToNext());
+        } finally {
+            c.close();
+        }
+
+        // remove r1 from one favorites group
+        mResolver.delete(urir1g1, null, null);
+        // r1's star should no longer be set
+        assertFalse(queryRawContactIsStarred(r1));
+        assertFalse(queryRawContactIsStarred(r2));
+        assertFalse(queryRawContactIsStarred(r3));
+        // there should be no membership rows
+        assertNoRowsAndClose(queryGroupMemberships(mAccount));
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+        // add r3 to the one favorites group for that account
+        // r3's star should automatically be set
+        Uri urir3g4 = insertGroupMembership(r3, g4);
+        assertFalse(queryRawContactIsStarred(r1));
+        assertFalse(queryRawContactIsStarred(r2));
+        assertTrue(queryRawContactIsStarred(r3));
+        assertNoRowsAndClose(queryGroupMemberships(mAccount));
+        c = queryGroupMemberships(mAccountTwo);
+        try {
+            assertTrue(c.moveToNext());
+            assertEquals(g4, c.getLong(0));
+            assertEquals(r3, c.getLong(1));
+            assertFalse(c.moveToNext());
+        } finally {
+            c.close();
+        }
+
+        // remove r3 from the favorites group
+        mResolver.delete(urir3g4, null, null);
+        // r3's star should automatically be cleared
+        assertFalse(queryRawContactIsStarred(r1));
+        assertFalse(queryRawContactIsStarred(r2));
+        assertFalse(queryRawContactIsStarred(r3));
+        assertNoRowsAndClose(queryGroupMemberships(mAccount));
+        assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+    }
+
+    public void testReadOnlyRawContact() {
+        long rawContactId = createRawContact();
+        Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+        storeValue(rawContactUri, RawContacts.CUSTOM_RINGTONE, "first");
+        storeValue(rawContactUri, RawContacts.RAW_CONTACT_IS_READ_ONLY, 1);
+
+        storeValue(rawContactUri, RawContacts.CUSTOM_RINGTONE, "second");
+        assertStoredValue(rawContactUri, RawContacts.CUSTOM_RINGTONE, "first");
+
+        Uri syncAdapterUri = rawContactUri.buildUpon()
+                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "1")
+                .build();
+        storeValue(syncAdapterUri, RawContacts.CUSTOM_RINGTONE, "third");
+        assertStoredValue(rawContactUri, RawContacts.CUSTOM_RINGTONE, "third");
+    }
+
+    public void testReadOnlyDataRow() {
+        long rawContactId = createRawContact();
+        Uri emailUri = insertEmail(rawContactId, "email");
+        Uri phoneUri = insertPhoneNumber(rawContactId, "555-1111");
+
+        storeValue(emailUri, Data.IS_READ_ONLY, "1");
+        storeValue(emailUri, Email.ADDRESS, "changed");
+        storeValue(phoneUri, Phone.NUMBER, "555-2222");
+        assertStoredValue(emailUri, Email.ADDRESS, "email");
+        assertStoredValue(phoneUri, Phone.NUMBER, "555-2222");
+
+        Uri syncAdapterUri = emailUri.buildUpon()
+                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "1")
+                .build();
+        storeValue(syncAdapterUri, Email.ADDRESS, "changed");
+        assertStoredValue(emailUri, Email.ADDRESS, "changed");
+    }
+
+    public void testContactWithReadOnlyRawContact() {
+        long rawContactId1 = createRawContact();
+        Uri rawContactUri1 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1);
+        storeValue(rawContactUri1, RawContacts.CUSTOM_RINGTONE, "first");
+
+        long rawContactId2 = createRawContact();
+        Uri rawContactUri2 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId2);
+        storeValue(rawContactUri2, RawContacts.CUSTOM_RINGTONE, "second");
+        storeValue(rawContactUri2, RawContacts.RAW_CONTACT_IS_READ_ONLY, 1);
+
+        setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
+                rawContactId1, rawContactId2);
+
+        long contactId = queryContactId(rawContactId1);
+
+        Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+        storeValue(contactUri, Contacts.CUSTOM_RINGTONE, "rt");
+        assertStoredValue(contactUri, Contacts.CUSTOM_RINGTONE, "rt");
+        assertStoredValue(rawContactUri1, RawContacts.CUSTOM_RINGTONE, "rt");
+        assertStoredValue(rawContactUri2, RawContacts.CUSTOM_RINGTONE, "second");
+    }
+
+    public void testNameParsingQuery() {
+        Uri uri = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name")
+                .appendQueryParameter(StructuredName.DISPLAY_NAME, "Mr. John Q. Doe Jr.").build();
+        Cursor cursor = mResolver.query(uri, null, null, null, null);
+        ContentValues values = new ContentValues();
+        values.put(StructuredName.DISPLAY_NAME, "Mr. John Q. Doe Jr.");
+        values.put(StructuredName.PREFIX, "Mr");
+        values.put(StructuredName.GIVEN_NAME, "John");
+        values.put(StructuredName.MIDDLE_NAME, "Q.");
+        values.put(StructuredName.FAMILY_NAME, "Doe");
+        values.put(StructuredName.SUFFIX, "Jr.");
+        values.put(StructuredName.FULL_NAME_STYLE, FullNameStyle.WESTERN);
+        assertTrue(cursor.moveToFirst());
+        assertCursorValues(cursor, values);
+        cursor.close();
+    }
+
+    public void testNameConcatenationQuery() {
+        Uri uri = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name")
+                .appendQueryParameter(StructuredName.PREFIX, "Mr")
+                .appendQueryParameter(StructuredName.GIVEN_NAME, "John")
+                .appendQueryParameter(StructuredName.MIDDLE_NAME, "Q.")
+                .appendQueryParameter(StructuredName.FAMILY_NAME, "Doe")
+                .appendQueryParameter(StructuredName.SUFFIX, "Jr.")
+                .build();
+        Cursor cursor = mResolver.query(uri, null, null, null, null);
+        ContentValues values = new ContentValues();
+        values.put(StructuredName.DISPLAY_NAME, "John Q. Doe, Jr.");
+        values.put(StructuredName.PREFIX, "Mr");
+        values.put(StructuredName.GIVEN_NAME, "John");
+        values.put(StructuredName.MIDDLE_NAME, "Q.");
+        values.put(StructuredName.FAMILY_NAME, "Doe");
+        values.put(StructuredName.SUFFIX, "Jr.");
+        values.put(StructuredName.FULL_NAME_STYLE, FullNameStyle.WESTERN);
+        assertTrue(cursor.moveToFirst());
+        assertCursorValues(cursor, values);
+        cursor.close();
+    }
+
+    private Cursor queryGroupMemberships(Account account) {
+        Cursor c = mResolver.query(maybeAddAccountQueryParameters(Data.CONTENT_URI, account),
+                new String[]{GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID},
+                Data.MIMETYPE + "=?", new String[]{GroupMembership.CONTENT_ITEM_TYPE},
+                GroupMembership.GROUP_SOURCE_ID);
+        return c;
+    }
+
     private String readToEnd(FileInputStream inputStream) {
         try {
             int ch;
diff --git a/tests/src/com/android/providers/contacts/DirectoryTest.java b/tests/src/com/android/providers/contacts/DirectoryTest.java
new file mode 100644
index 0000000..5831612
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/DirectoryTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.accounts.Account;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.test.suitebuilder.annotation.LargeTest;
+
+
+/**
+ * Unit tests for {@link ContactsProvider2}, directory functionality.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -e class com.android.providers.contacts.DirectoryTest -w \
+ *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class DirectoryTest extends BaseContactsProvider2Test {
+
+    public void testDefaultDirectory() {
+        ContentValues values = new ContentValues();
+        Uri defaultDirectoryUri =
+            ContentUris.withAppendedId(Directory.CONTENT_URI, Directory.DEFAULT);
+
+        values.put(Directory.PACKAGE_NAME, "contactsTestPackage");
+        values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
+        values.put(Directory.TYPE_RESOURCE_ID, R.string.default_directory);
+        values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
+        values.putNull(Directory.ACCOUNT_NAME);
+        values.putNull(Directory.ACCOUNT_TYPE);
+        values.putNull(Directory.DISPLAY_NAME);
+
+        assertStoredValues(defaultDirectoryUri, values);
+    }
+
+    public void testInvisibleLocalDirectory() {
+        ContentValues values = new ContentValues();
+        Uri defaultDirectoryUri =
+            ContentUris.withAppendedId(Directory.CONTENT_URI, Directory.LOCAL_INVISIBLE);
+
+        values.put(Directory.PACKAGE_NAME, "contactsTestPackage");
+        values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
+        values.put(Directory.TYPE_RESOURCE_ID, R.string.local_invisible_directory);
+        values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
+        values.putNull(Directory.ACCOUNT_NAME);
+        values.putNull(Directory.ACCOUNT_TYPE);
+        values.putNull(Directory.DISPLAY_NAME);
+
+        assertStoredValues(defaultDirectoryUri, values);
+    }
+
+    public void testForwardingToLocalContacts() {
+        long contactId = queryContactId(createRawContactWithName("John", "Doe"));
+
+        Uri contentUri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
+                ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)).build();
+
+        Cursor cursor = mResolver.query(contentUri,
+                new String[]{Contacts._ID, Contacts.DISPLAY_NAME}, null, null, null);
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        assertEquals(contactId, cursor.getLong(0));
+        assertEquals("John Doe", cursor.getString(1));
+        cursor.close();
+    }
+
+    public void testForwardingToLocalInvisibleContacts() {
+
+        // Visible because there is no account
+        long contactId1 = queryContactId(createRawContactWithName("Bob", "Parr"));
+
+        Account account = new Account("accountName", "accountType");
+        long groupId = createGroup(account, "sid", "def",
+                0 /* visible */,  true /* auto-add */, false /* fav */);
+        long contactId2 = queryContactId(createRawContactWithName("Helen", "Parr",
+                account));
+
+        Uri contentUri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
+                ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.LOCAL_INVISIBLE))
+                .build();
+
+        Cursor cursor = mResolver.query(contentUri,
+                new String[]{Contacts._ID, Contacts.DISPLAY_NAME}, null, null, null);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
+
+        // Hide by removing from the default group
+        mResolver.delete(Data.CONTENT_URI,
+                Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
+                new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) });
+
+        cursor = mResolver.query(contentUri,
+                new String[]{Contacts._ID, Contacts.DISPLAY_NAME}, null, null, null);
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        assertEquals(contactId2, cursor.getLong(0));
+        assertEquals("Helen Parr", cursor.getString(1));
+        cursor.close();
+
+        Uri filterUri = Contacts.CONTENT_FILTER_URI.buildUpon().appendEncodedPath("parr")
+                .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+                        String.valueOf(Directory.LOCAL_INVISIBLE)).build();
+
+        cursor = mResolver.query(filterUri,
+                new String[]{Contacts._ID, Contacts.DISPLAY_NAME}, null, null, null);
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        cursor.moveToFirst();
+        assertEquals(contactId2, cursor.getLong(0));
+        assertEquals("Helen Parr", cursor.getString(1));
+        cursor.close();
+    }
+}
+
diff --git a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
index 3919770..50ba389 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactImporterPerformanceTest.java
@@ -18,6 +18,7 @@
 
 import android.content.ContentProvider;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.res.Resources;
 import android.os.Debug;
 import android.provider.CallLog;
@@ -72,6 +73,13 @@
             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_imp.");
diff --git a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
index 85ed6a7..d2e4365 100644
--- a/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
+++ b/tests/src/com/android/providers/contacts/LegacyContactsProviderTest.java
@@ -392,19 +392,22 @@
         values.put(Phones.ISPRIMARY, 1);
 
         Uri uri = mResolver.insert(Phones.CONTENT_URI, values);
-
+        ContentValues expectedResults[] = new ContentValues[2];
         // Adding another value to assert
-        values.put(Phones.NUMBER_KEY, "11446640081");
-
+        expectedResults[0] = new ContentValues(values);
+        expectedResults[0].put(Phones.NUMBER_KEY, "+18004664411");
+        expectedResults[1] = values;
+        expectedResults[1].put(Phones.NUMBER_KEY, "18004664411");
         // The result is joined with People
-        putContactValues(values);
-        assertStoredValues(uri, values);
+        putContactValues(expectedResults[0]);
+        putContactValues(expectedResults[1]);
+        assertStoredValues(uri, expectedResults);
         assertSelection(Phones.CONTENT_URI, values, "phones",
                 Phones._ID, ContentUris.parseId(uri));
 
         // Access the phone through People
         Uri twigUri = Uri.withAppendedPath(personUri, People.Phones.CONTENT_DIRECTORY);
-        assertStoredValues(twigUri, values);
+        assertStoredValues(twigUri, expectedResults);
 
         // Now the person should be joined with Phone
         values.clear();
@@ -443,7 +446,12 @@
 
         mResolver.update(uri, values, null, null);
 
-        assertStoredValues(uri, values);
+        ContentValues[] expectedValues = new ContentValues[2];
+        expectedValues[0] = values;
+        expectedValues[0].put(Phones.NUMBER_KEY, "18005554663");
+        expectedValues[1] = new ContentValues(values);
+        expectedValues[1].put(Phones.NUMBER_KEY, "+18005554663");
+        assertStoredValues(uri, expectedValues);
     }
 
     public void testPhonesFilterQuery() {
@@ -456,13 +464,18 @@
         values.put(Phones.PERSON_ID, personId);
         values.put(Phones.TYPE, Phones.TYPE_CUSTOM);
         values.put(Phones.LABEL, "Directory");
-        values.put(Phones.NUMBER, "1-800-4664-411");
+        values.put(Phones.NUMBER, "800-4664-411");
         values.put(Phones.ISPRIMARY, 1);
 
         Uri uri = mResolver.insert(Phones.CONTENT_URI, values);
 
-        Uri filterUri1 = Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, "8004664411");
-        assertStoredValues(filterUri1, values);
+        Uri filterUri1 = Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, "18004664411");
+        ContentValues[] expectedValues = new ContentValues[2];
+        expectedValues[0] = values;
+        expectedValues[0].put(Phones.NUMBER_KEY, "8004664411");
+        expectedValues[1] = new ContentValues(values);
+        expectedValues[1].put(Phones.NUMBER_KEY, "+18004664411");
+        assertStoredValues(filterUri1, expectedValues);
 
         Uri filterUri2 = Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, "7773334444");
         assertEquals(0, getCount(filterUri2, null, null));
diff --git a/tests/src/com/android/providers/contacts/NameLookupBuilderTest.java b/tests/src/com/android/providers/contacts/NameLookupBuilderTest.java
index 1de34c0..91ab761 100644
--- a/tests/src/com/android/providers/contacts/NameLookupBuilderTest.java
+++ b/tests/src/com/android/providers/contacts/NameLookupBuilderTest.java
@@ -200,6 +200,40 @@
                 "(6:C)", mBuilder.inserted());
     }
 
+    public void testKoreanName() {
+        // Only run this test when Chinese collation is supported.
+        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.KOREA)) {
+            return;
+        }
+
+        // Lee Sang Il
+        mBuilder.insertNameLookup(0, 0, "\uC774\uC0C1\uC77C", FullNameStyle.KOREAN);
+        assertEquals(
+                "(0:\uC774\uC0C1\uC77C)" + // Lee Sang Il
+                "(2:\uC774\uC0C1\uC77C)" + // Lee Sang Il
+                "(6:\uC0C1\uC77C)" + // Sang Il : given name
+                "(7:\u1109\u110B)" + // SIOS IEUNG : consonants of given name
+                "(7:\u110B\u1109\u110B)", // RIEUL SIOS IEUNG : consonants of fullname
+                mBuilder.inserted());
+    }
+
+    public void testKoreanNameWithTwoCharactersFamilyName() {
+        // Only run this test when Chinese collation is supported.
+        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.KOREA)) {
+            return;
+        }
+
+        // Sun Woo Young Nyeu
+        mBuilder.insertNameLookup(0, 0, "\uC120\uC6B0\uC6A9\uB140", FullNameStyle.KOREAN);
+        assertEquals(
+                "(0:\uC120\uC6B0\uC6A9\uB140)" + // Sun Woo Young Nyeu
+                "(2:\uC120\uC6B0\uC6A9\uB140)" + // Sun Woo Young Nyeu
+                "(6:\uC6A9\uB140)" + // Young Nyeu : given name
+                "(7:\u110B\u1102)" + // IEUNG NIEUN : consonants of given name
+                "(7:\u1109\u110B\u110B\u1102)", // SIOS IEUNG IEUNG NIEUN : consonants of fullname
+                mBuilder.inserted());
+    }
+
     public void testMultiwordName() {
         mBuilder.insertNameLookup(0, 0, "Jo Jeffrey John Jessy Longname", FullNameStyle.UNDEFINED);
         String actual = mBuilder.inserted();
diff --git a/tests/src/com/android/providers/contacts/NameSplitterTest.java b/tests/src/com/android/providers/contacts/NameSplitterTest.java
index 87c897e..d656b04 100644
--- a/tests/src/com/android/providers/contacts/NameSplitterTest.java
+++ b/tests/src/com/android/providers/contacts/NameSplitterTest.java
@@ -372,4 +372,14 @@
 
         assertEquals(expectedPhoneticNameStyle, name.phoneticNameStyle);
     }
+
+    public void testSplitKoreanName() {
+        createNameSplitter(Locale.KOREA);
+
+        // Lee - Sang Il
+        assertSplitName("\uC774\uC0C1\uC77C", null, "\uC0C1\uC77C", null, "\uC774", null);
+        // Dok Go - Young Jae
+        assertSplitName("\uB3C5\uACE0\uC601\uC7AC",
+                null, "\uC601\uC7AC", null, "\uB3C5\uACE0", null);
+    }
 }
diff --git a/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java b/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
index 6209e54..3fbf548 100644
--- a/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
+++ b/tests/src/com/android/providers/contacts/RestrictionExceptionsTest.java
@@ -18,6 +18,7 @@
 
 import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
 import static com.android.providers.contacts.ContactsActor.PACKAGE_RED;
+
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
@@ -35,6 +36,8 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 
 /**
@@ -190,9 +193,7 @@
         final Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
         final AssetFileDescriptor file = mRed.resolver.openAssetFileDescriptor(shareUri, "r");
         final InputStream in = file.createInputStream();
-        final byte[] buf = new byte[in.available()];
-        in.read(buf);
-        in.close();
+        final byte[] buf = readInputStream(in);
         final String card = new String(buf);
         assertNotSame(0, card.length());
 
@@ -201,6 +202,20 @@
         assertTrue(card.indexOf(PHONE_GREY) == -1);
     }
 
+    private static byte[] readInputStream(InputStream in) throws IOException {
+        try {
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            byte[] buf = new byte[4096];
+            int count;
+            while ((count = in.read(buf)) != -1) {
+                out.write(buf, 0, count);
+            }
+            return out.toByteArray();
+        } finally {
+            in.close();
+        }
+    }
+
     public void testContactsLiveFolder() {
         final long greyContact = mGrey.createContact(true, GENERIC_NAME);
         final long greyPhone = mGrey.createPhone(greyContact, PHONE_GREY);
diff --git a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
index 0248094..a280bb2 100644
--- a/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
+++ b/tests/src/com/android/providers/contacts/SynchronousContactsProvider2.java
@@ -38,7 +38,12 @@
     @Override
     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
         if (mDbHelper == null) {
-            mDbHelper = new ContactsDatabaseHelper(context);
+            mDbHelper = new ContactsDatabaseHelper(context) {
+                @Override
+                protected String getCountryIso() {
+                    return "US";
+                }
+            };
         }
         return mDbHelper;
     }
@@ -89,6 +94,10 @@
     }
 
     @Override
+    protected void startContactDirectoryManager() {
+    }
+
+    @Override
     protected Account getDefaultAccount() {
         if (mAccount == null) {
             mAccount = new Account("androidtest@gmail.com", "com.google");
@@ -162,4 +171,9 @@
         // We have an explicit test for data conversion - no need to do it every time
         return false;
     }
+
+    @Override
+    protected String getCurrentCountryIso() {
+        return "us";
+    }
 }