am c085b3ee: Avoid long running upgrade work on the main thread
diff --git a/Android.mk b/Android.mk
index 5e7b6b9..ade9f46 100644
--- a/Android.mk
+++ b/Android.mk
@@ -10,7 +10,7 @@
 
 LOCAL_JAVA_LIBRARIES := ext
 
-LOCAL_STATIC_JAVA_LIBRARIES += android-common
+LOCAL_STATIC_JAVA_LIBRARIES += android-common com.android.vcard
 
 LOCAL_PACKAGE_NAME := ContactsProvider
 LOCAL_CERTIFICATE := shared
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java
index 5f80115..8fbc8de 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -1088,9 +1088,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;
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index edd73ff..85067cb 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -74,7 +74,7 @@
 /* package */ class ContactsDatabaseHelper extends SQLiteOpenHelper {
     private static final String TAG = "ContactsDatabaseHelper";
 
-    static final int DATABASE_VERSION = 309;
+    static final int DATABASE_VERSION = 401;
 
     private static final String DATABASE_NAME = "contacts2.db";
     private static final String DATABASE_PRESENCE = "presence_db";
@@ -99,6 +99,7 @@
         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 DATA_JOIN_MIMETYPES = "data "
                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id)";
@@ -214,6 +215,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 {
@@ -271,7 +277,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;
@@ -470,12 +475,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;
@@ -541,34 +540,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)," +
@@ -651,17 +622,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 +
         ");");
@@ -704,7 +670,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, " +
@@ -851,6 +816,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, " +
@@ -893,6 +860,10 @@
                     Settings.ACCOUNT_TYPE + ") ON CONFLICT REPLACE" +
         ");");
 
+        db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" +
+                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 + " (" +
@@ -991,6 +962,11 @@
                                 + "=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.CONTACTS
                 + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
                 + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
@@ -1066,13 +1042,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 +
         ");");
     }
@@ -1149,9 +1123,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 + ","
@@ -1163,7 +1135,8 @@
                 + contactNameColumns + ", "
                 + Contacts.LOOKUP_KEY + ", "
                 + Contacts.PHOTO_ID + ", "
-                + Contacts.NAME_RAW_CONTACT_ID + ","
+                + Contacts.NAME_RAW_CONTACT_ID + ", "
+                + Clauses.CONTACT_VISIBLE + " AS " + Contacts.IN_VISIBLE_GROUP + ", "
                 + ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
                 + " FROM " + Tables.DATA
@@ -1228,7 +1201,10 @@
                         + " AS " + Contacts.STARRED + ", "
                 + ContactsColumns.CONCRETE_TIMES_CONTACTED
                         + " AS " + Contacts.TIMES_CONTACTED + ", "
-                + ContactsColumns.LAST_STATUS_UPDATE_ID;
+                + ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
+                + Contacts.NAME_RAW_CONTACT_ID + ", "
+                + Clauses.CONTACT_VISIBLE + " AS " + Contacts.IN_VISIBLE_GROUP;
+
 
         String contactsSelect = "SELECT "
                 + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
@@ -1257,6 +1233,8 @@
                 + Groups.DELETED + ","
                 + Groups.GROUP_VISIBLE + ","
                 + Groups.SHOULD_SYNC + ","
+                + Groups.AUTO_ADD + ","
+                + Groups.FAVORITES + ","
                 + Groups.SYNC1 + ","
                 + Groups.SYNC2 + ","
                 + Groups.SYNC3 + ","
@@ -1488,6 +1466,24 @@
             oldVersion = 309;
         }
 
+        if (oldVersion == 309) {
+            // Add column NAME_RAW_CONTACT_ID
+            upgradeViewsAndTriggers = true;
+            oldVersion = 310;
+        }
+
+        if (oldVersion == 310) {
+            upgradeViewsAndTriggers = true;
+            upgradeToVersion311(db);
+            oldVersion = 311;
+        }
+
+        if (oldVersion == 311) {
+            upgradeViewsAndTriggers = true;
+            upgradeToVersion401(db);
+            oldVersion = 401;
+        }
+
         if (upgradeViewsAndTriggers) {
             createContactsViews(db);
             createGroupsView(db);
@@ -1562,8 +1558,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(
@@ -1605,7 +1600,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 + ")" +
@@ -1613,7 +1608,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" +
         ");");
 
@@ -1657,12 +1652,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 +
         ");");
     }
@@ -2103,13 +2098,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 upgradeToVersion311(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 rebuildNameLookup(SQLiteDatabase db) {
@@ -2397,6 +2399,20 @@
         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");
+    }
+
     public String extractHandleFromEmailAddress(String email) {
         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
         if (tokens.length == 0) {
@@ -2452,8 +2468,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");
@@ -2661,67 +2675,39 @@
      * 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();
-        }
+        updateContactVisibility("");
     }
 
     /**
      * Update {@link Contacts#IN_VISIBLE_GROUP} 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();
+        updateContactVisibility(" AND " + Contacts._ID + "=" + contactId);
+    }
 
-        mVisibleSpecificUpdateRawContacts.bindLong(1, contactId);
-        mVisibleSpecificUpdateRawContacts.execute();
+    private void updateContactVisibility(String selection) {
+        SQLiteDatabase db = getWritableDatabase();
+
+        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);
     }
 
     /**
@@ -2939,7 +2925,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) {
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index b7085ca..40be58f 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -16,29 +16,6 @@
 
 package com.android.providers.contacts;
 
-import com.android.internal.content.SyncStateContentProviderHelper;
-import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
-import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
-import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
-import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
-import com.google.android.collect.Lists;
-import com.google.android.collect.Maps;
-import com.google.android.collect.Sets;
-
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.OnAccountsUpdateListener;
@@ -77,8 +54,6 @@
 import android.os.MemoryFile;
 import android.os.RemoteException;
 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;
@@ -114,6 +89,33 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
+
+import com.android.internal.content.SyncStateContentProviderHelper;
+import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
+import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
+import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
+import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
+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.android.vcard.VCardComposer.HandlerForOutputStream;
+
 import java.io.ByteArrayOutputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -146,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,
@@ -193,6 +194,8 @@
     private static final int CONTACTS_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 RAW_CONTACTS = 2002;
     private static final int RAW_CONTACTS_ID = 2003;
@@ -242,6 +245,33 @@
 
     private static final int PROVIDER_STATUS = 16001;
 
+    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) "
@@ -435,9 +465,13 @@
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
                 AGGREGATION_SUGGESTIONS);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_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/as_vcard/*", CONTACTS_AS_VCARD);
         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
                 CONTACTS_AS_MULTI_VCARD);
@@ -528,6 +562,7 @@
         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);
+        sContactsProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID);
 
         // Handle projections for Contacts-level statuses
         addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE,
@@ -868,6 +903,8 @@
         columns.put(Groups.DELETED, Groups.DELETED);
         columns.put(Groups.NOTES, Groups.NOTES);
         columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC);
+        columns.put(Groups.FAVORITES, Groups.FAVORITES);
+        columns.put(Groups.AUTO_ADD, Groups.AUTO_ADD);
         columns.put(Groups.SYNC1, Groups.SYNC1);
         columns.put(Groups.SYNC2, Groups.SYNC2);
         columns.put(Groups.SYNC3, Groups.SYNC3);
@@ -1688,6 +1725,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);
         }
@@ -1696,6 +1743,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;
         }
@@ -1704,18 +1754,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;
         }
@@ -2396,7 +2474,7 @@
             }
 
             case RAW_CONTACTS: {
-                id = insertRawContact(uri, values);
+                id = insertRawContact(uri, values, callerIsSyncAdapter);
                 mSyncToNetwork |= !callerIsSyncAdapter;
                 break;
             }
@@ -2523,9 +2601,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);
@@ -2547,9 +2626,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
      *
@@ -2986,12 +3125,41 @@
         }
         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);
+            while (c.moveToNext()) {
+                if (c.getLong(1) != 0) {
+                    final long rawContactId = c.getLong(0);
+                    insertDataGroupMembership(rawContactId, result);
+                    setRawContactDirty(rawContactId);
+                }
+            }
+        }
+
         if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
             mVisibleTouched = true;
         }
@@ -3093,7 +3261,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);
@@ -3221,7 +3389,7 @@
 
             case CONTACTS_ID: {
                 long contactId = ContentUris.parseId(uri);
-                return deleteContact(contactId);
+                return deleteContact(contactId, callerIsSyncAdapter);
             }
 
             case CONTACTS_LOOKUP: {
@@ -3233,7 +3401,7 @@
                 }
                 final String lookupKey = pathSegments.get(2);
                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
-                return deleteContact(contactId);
+                return deleteContact(contactId, callerIsSyncAdapter);
             }
 
             case CONTACTS_LOOKUP_ID: {
@@ -3258,7 +3426,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
@@ -3375,7 +3543,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,
@@ -3383,7 +3551,7 @@
         try {
             while (c.moveToNext()) {
                 long rawContactId = c.getLong(0);
-                markRawContactAsDeleted(rawContactId);
+                markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
             }
         } finally {
             c.close();
@@ -3401,7 +3569,7 @@
             return count;
         } else {
             mDbHelper.removeContactIfSingleton(rawContactId);
-            return markRawContactAsDeleted(rawContactId);
+            return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
         }
     }
 
@@ -3416,7 +3584,7 @@
       return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
     }
 
-    private int markRawContactAsDeleted(long rawContactId) {
+    private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) {
         mSyncToNetwork = true;
 
         mValues.clear();
@@ -3425,7 +3593,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
@@ -3462,12 +3630,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;
             }
 
@@ -3481,7 +3649,7 @@
                 }
                 final String lookupKey = pathSegments.get(2);
                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
-                count = updateContactOptions(contactId, values);
+                count = updateContactOptions(contactId, values, callerIsSyncAdapter);
                 break;
             }
 
@@ -3517,7 +3685,7 @@
 
             case RAW_CONTACTS: {
                 selection = appendAccountToSelection(uri, selection);
-                count = updateRawContacts(values, selection, selectionArgs);
+                count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
                 break;
             }
 
@@ -3526,10 +3694,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;
             }
@@ -3692,7 +3862,8 @@
         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");
@@ -3705,7 +3876,7 @@
         try {
             while (cursor.moveToNext()) {
                 long rawContactId = cursor.getLong(0);
-                updateRawContact(rawContactId, values);
+                updateRawContact(rawContactId, values, callerIsSyncAdapter);
                 count++;
             }
         } finally {
@@ -3715,7 +3886,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)
@@ -3751,8 +3923,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);
             }
@@ -3834,7 +4028,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,
@@ -3842,7 +4036,7 @@
         try {
             while (cursor.moveToNext()) {
                 long contactId = cursor.getLong(0);
-                updateContactOptions(contactId, values);
+                updateContactOptions(contactId, values, callerIsSyncAdapter);
                 count++;
             }
         } finally {
@@ -3852,7 +4046,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,
@@ -3879,6 +4074,21 @@
         mSelectionArgs1[0] = String.valueOf(contactId);
         mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", 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.
         mValues.clear();
@@ -3949,44 +4159,18 @@
         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();
-        }
-
-        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) {
         // 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});
@@ -4000,38 +4184,39 @@
                 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);
+                }
+
                 // 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 +
@@ -4054,64 +4239,12 @@
                 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();
@@ -4122,15 +4255,13 @@
     /**
      * 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)));
                 }
             }
@@ -4237,6 +4368,48 @@
                 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);
+                    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(" AND " + Data.CONTACT_ID + "=?"
+                            + " AND " + Data.LOOKUP_KEY + "=?");
+                    Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
+                            groupBy, limit);
+                    if (c.getCount() != 0) {
+                        return c;
+                    }
+
+                    c.close();
+
+                    // 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));
@@ -5147,25 +5320,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);
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/NameNormalizer.java b/src/com/android/providers/contacts/NameNormalizer.java
index f40a632..6dfe8bd 100644
--- a/src/com/android/providers/contacts/NameNormalizer.java
+++ b/src/com/android/providers/contacts/NameNormalizer.java
@@ -16,8 +16,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;
 
 /**
  * Converts a name to a normalized form by removing all non-letter characters and normalizing
@@ -27,14 +29,14 @@
 
     private static final RuleBasedCollator sCompressingCollator;
     static {
-        sCompressingCollator = (RuleBasedCollator)Collator.getInstance(null);
+        sCompressingCollator = (RuleBasedCollator)Collator.getInstance(Locale.getDefault());
         sCompressingCollator.setStrength(Collator.PRIMARY);
         sCompressingCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
     }
 
     private static final RuleBasedCollator sComplexityCollator;
     static {
-        sComplexityCollator = (RuleBasedCollator)Collator.getInstance(null);
+        sComplexityCollator = (RuleBasedCollator)Collator.getInstance(Locale.getDefault());
         sComplexityCollator.setStrength(Collator.TERTIARY);
         sComplexityCollator.setAttribute(CollationAttribute.CASE_FIRST,
                 CollationAttribute.VALUE_LOWER_FIRST);
@@ -45,7 +47,8 @@
      * of names.  It ignores non-letter characters and removes accents.
      */
     public static String normalize(String name) {
-        return Hex.encodeHex(sCompressingCollator.getSortKey(lettersAndDigitsOnly(name)), true);
+        CollationKey key = sCompressingCollator.getCollationKey(lettersAndDigitsOnly(name));
+        return Hex.encodeHex(key.toByteArray(), true);
     }
 
     /**
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/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index de410a9..09c223a 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -171,24 +171,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));
     }
@@ -419,6 +441,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());
@@ -427,7 +459,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));
@@ -577,6 +609,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);
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index a8beec0..f6b4708 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -106,6 +106,51 @@
         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 testDataInsert() {
         long rawContactId = createRawContactWithName("John", "Doe");
 
@@ -1738,8 +1783,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");
@@ -2409,6 +2454,285 @@
         }
     }
 
+    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));
+    }
+
+    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;