Rebuilding contact name lookup during an upgrade from Eclair.

Bug: 2476275
Change-Id: Icda3e0d57ba053978fc0bc8c7e961db2b8b5a69c
diff --git a/src/com/android/providers/contacts/CommonNicknameCache.java b/src/com/android/providers/contacts/CommonNicknameCache.java
index a5ad7a7..0276743 100644
--- a/src/com/android/providers/contacts/CommonNicknameCache.java
+++ b/src/com/android/providers/contacts/CommonNicknameCache.java
@@ -66,7 +66,7 @@
      * we should expect the combination of the Bloom filter and cache will
      * prevent around 98-99% of unnecessary queries from running.
      */
-    public void preloadNicknameBloomFilter() {
+    private void preloadNicknameBloomFilter() {
         mNicknameBloomFilter = new BitSet(NICKNAME_BLOOM_FILTER_SIZE + 1);
         Cursor cursor = mDb.query(NicknameLookupPreloadQuery.TABLE,
                 NicknameLookupPreloadQuery.COLUMNS,
@@ -88,6 +88,10 @@
      * Returns nickname cluster IDs or null. Maintains cache.
      */
     protected String[] getCommonNicknameClusters(String normalizedName) {
+        if (mNicknameBloomFilter == null) {
+            preloadNicknameBloomFilter();
+        }
+
         int hashCode = normalizedName.hashCode();
         if (!mNicknameBloomFilter.get(hashCode & NICKNAME_BLOOM_FILTER_SIZE)) {
             return null;
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index b413531..5916b6e 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -713,16 +713,6 @@
 //                RawContactsColumns.AGGREGATION_NEEDED +
 //        ");");
 
-        db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
-                RawContactsColumns.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 + "," +
-                RawContacts.SORT_KEY_ALTERNATIVE +
-        ");");
-
         // Package name mapping table
         db.execSQL("CREATE TABLE " + Tables.PACKAGES + " (" +
                 PackagesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
@@ -1039,6 +1029,18 @@
                 NameLookupColumns.RAW_CONTACT_ID + ", " +
                 NameLookupColumns.DATA_ID +
         ");");
+
+        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 +
+        ");");
     }
 
     private static void createContactsViews(SQLiteDatabase db) {
@@ -1331,6 +1333,7 @@
         Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion);
 
         boolean upgradeViewsAndTriggers = false;
+        boolean upgradeNameLookup = false;
 
         if (oldVersion == 99) {
             upgradeViewsAndTriggers = true;
@@ -1372,6 +1375,7 @@
 
         if (oldVersion == 105) {
             upgradeToVersion202(db);
+            upgradeNameLookup = true;
             oldVersion = 202;
         }
 
@@ -1430,6 +1434,10 @@
             mReopenDatabase = true;
         }
 
+        if (upgradeNameLookup) {
+            rebuildNameLookup(db);
+        }
+
         if (oldVersion != newVersion) {
             throw new IllegalStateException(
                     "error upgrading the database to version " + newVersion);
@@ -1951,6 +1959,246 @@
         }
     }
 
+    /**
+     * Regenerate all rows in the nickname_lookup and name_lookup tables,
+     * because sort key generation algorithm has been modified, e.g. as a result of
+     * locale change.
+     */
+    private void rebuildNameLookup(SQLiteDatabase db) {
+        db.execSQL("DELETE FROM " + Tables.NICKNAME_LOOKUP);
+        loadNicknameLookupTable(db);
+
+        db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
+        db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP);
+
+        SQLiteStatement nameLookupInsert = db.compileStatement(
+                "INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
+                        + NameLookupColumns.RAW_CONTACT_ID + ","
+                        + NameLookupColumns.DATA_ID + ","
+                        + NameLookupColumns.NAME_TYPE + ","
+                        + NameLookupColumns.NORMALIZED_NAME +
+                ") VALUES (?,?,?,?)");
+
+        insertStructuredNameLookup(db, nameLookupInsert);
+        insertOrganizationLookup(db, nameLookupInsert);
+        insertEmailLookup(db, nameLookupInsert);
+        insertNicknameLookup(db, nameLookupInsert);
+
+        createContactsIndexes(db);
+    }
+
+    private static final class StructuredNameQuery {
+        public static final String TABLE = Tables.DATA;
+
+        public static final String SELECTION =
+                DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL";
+
+        public static final String COLUMNS[] = {
+                StructuredName._ID,
+                StructuredName.RAW_CONTACT_ID,
+                StructuredName.DISPLAY_NAME,
+        };
+
+        public static final int ID = 0;
+        public static final int RAW_CONTACT_ID = 1;
+        public static final int DISPLAY_NAME = 2;
+    }
+
+    private class StructuredNameLookupBuilder extends NameLookupBuilder {
+
+        private final SQLiteStatement mNameLookupInsert;
+        private final CommonNicknameCache mCommonNicknameCache;
+
+        public StructuredNameLookupBuilder(NameSplitter splitter,
+                CommonNicknameCache commonNicknameCache, SQLiteStatement nameLookupInsert) {
+            super(splitter);
+            this.mCommonNicknameCache = commonNicknameCache;
+            this.mNameLookupInsert = nameLookupInsert;
+        }
+
+        @Override
+        protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
+                String name) {
+            if (!TextUtils.isEmpty(name)) {
+                ContactsDatabaseHelper.this.insertNormalizedNameLookup(mNameLookupInsert,
+                        rawContactId, dataId, lookupType, name);
+            }
+        }
+
+        @Override
+        protected String[] getCommonNicknameClusters(String normalizedName) {
+            return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
+        }
+    }
+
+    /**
+     * Inserts name lookup rows for all structured names in the database.
+     */
+    private void insertStructuredNameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
+        NameLookupBuilder nameLookupBuilder = new StructuredNameLookupBuilder(createNameSplitter(),
+                new CommonNicknameCache(db), nameLookupInsert);
+        final long mimeTypeId = lookupMimeTypeId(db, StructuredName.CONTENT_ITEM_TYPE);
+        Cursor cursor = db.query(StructuredNameQuery.TABLE, StructuredNameQuery.COLUMNS,
+                StructuredNameQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)},
+                null, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                long dataId = cursor.getLong(StructuredNameQuery.ID);
+                long rawContactId = cursor.getLong(StructuredNameQuery.RAW_CONTACT_ID);
+                String name = cursor.getString(StructuredNameQuery.DISPLAY_NAME);
+                nameLookupBuilder.insertNameLookup(rawContactId, dataId, name);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private static final class OrganizationQuery {
+        public static final String TABLE = Tables.DATA;
+
+        public static final String SELECTION =
+                DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL";
+
+        public static final String COLUMNS[] = {
+                Organization._ID,
+                Organization.RAW_CONTACT_ID,
+                Organization.COMPANY,
+                Organization.TITLE,
+        };
+
+        public static final int ID = 0;
+        public static final int RAW_CONTACT_ID = 1;
+        public static final int COMPANY = 2;
+        public static final int TITLE = 3;
+    }
+
+    /**
+     * Inserts name lookup rows for all organizations in the database.
+     */
+    private void insertOrganizationLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
+        final long mimeTypeId = lookupMimeTypeId(db, Organization.CONTENT_ITEM_TYPE);
+        Cursor cursor = db.query(OrganizationQuery.TABLE, OrganizationQuery.COLUMNS,
+                OrganizationQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)},
+                null, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                long dataId = cursor.getLong(OrganizationQuery.ID);
+                long rawContactId = cursor.getLong(OrganizationQuery.RAW_CONTACT_ID);
+                String organization = cursor.getString(OrganizationQuery.COMPANY);
+                String title = cursor.getString(OrganizationQuery.TITLE);
+                insertNameLookup(nameLookupInsert, rawContactId, dataId,
+                        NameLookupType.ORGANIZATION, organization);
+                insertNameLookup(nameLookupInsert, rawContactId, dataId,
+                        NameLookupType.ORGANIZATION, title);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private static final class EmailQuery {
+        public static final String TABLE = Tables.DATA;
+
+        public static final String SELECTION =
+                DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL";
+
+        public static final String COLUMNS[] = {
+                Email._ID,
+                Email.RAW_CONTACT_ID,
+                Email.ADDRESS,
+        };
+
+        public static final int ID = 0;
+        public static final int RAW_CONTACT_ID = 1;
+        public static final int ADDRESS = 2;
+    }
+
+    /**
+     * Inserts name lookup rows for all email addresses in the database.
+     */
+    private void insertEmailLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
+        final long mimeTypeId = lookupMimeTypeId(db, Email.CONTENT_ITEM_TYPE);
+        Cursor cursor = db.query(EmailQuery.TABLE, EmailQuery.COLUMNS,
+                EmailQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)},
+                null, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                long dataId = cursor.getLong(EmailQuery.ID);
+                long rawContactId = cursor.getLong(EmailQuery.RAW_CONTACT_ID);
+                String address = cursor.getString(EmailQuery.ADDRESS);
+                address = extractHandleFromEmailAddress(address);
+                insertNameLookup(nameLookupInsert, rawContactId, dataId,
+                        NameLookupType.EMAIL_BASED_NICKNAME, address);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private static final class NicknameQuery {
+        public static final String TABLE = Tables.DATA;
+
+        public static final String SELECTION =
+                DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL";
+
+        public static final String COLUMNS[] = {
+                Nickname._ID,
+                Nickname.RAW_CONTACT_ID,
+                Nickname.NAME,
+        };
+
+        public static final int ID = 0;
+        public static final int RAW_CONTACT_ID = 1;
+        public static final int NAME = 2;
+    }
+
+    /**
+     * Inserts name lookup rows for all nicknames in the database.
+     */
+    private void insertNicknameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
+        final long mimeTypeId = lookupMimeTypeId(db, Nickname.CONTENT_ITEM_TYPE);
+        Cursor cursor = db.query(NicknameQuery.TABLE, NicknameQuery.COLUMNS,
+                NicknameQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)},
+                null, null, null);
+        try {
+            while (cursor.moveToNext()) {
+                long dataId = cursor.getLong(NicknameQuery.ID);
+                long rawContactId = cursor.getLong(NicknameQuery.RAW_CONTACT_ID);
+                String nickname = cursor.getString(NicknameQuery.NAME);
+                insertNameLookup(nameLookupInsert, rawContactId, dataId,
+                        NameLookupType.NICKNAME, nickname);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
+     */
+    public void insertNameLookup(SQLiteStatement stmt, long rawContactId, long dataId,
+            int lookupType, String name) {
+        if (TextUtils.isEmpty(name)) {
+            return;
+        }
+
+        String normalized = NameNormalizer.normalize(name);
+        if (TextUtils.isEmpty(normalized)) {
+            return;
+        }
+
+        insertNormalizedNameLookup(stmt, rawContactId, dataId, lookupType, normalized);
+    }
+
+    private void insertNormalizedNameLookup(SQLiteStatement stmt, long rawContactId, long dataId,
+            int lookupType, String normalizedName) {
+        stmt.bindLong(1, rawContactId);
+        stmt.bindLong(2, dataId);
+        stmt.bindLong(3, lookupType);
+        stmt.bindString(4, normalizedName);
+        stmt.executeInsert();
+    }
+
     public String extractHandleFromEmailAddress(String email) {
         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
         if (tokens.length == 0) {
@@ -2078,6 +2326,15 @@
         // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
     }
 
+    public NameSplitter createNameSplitter() {
+        return new NameSplitter(
+                mContext.getString(com.android.internal.R.string.common_name_prefixes),
+                mContext.getString(com.android.internal.R.string.common_last_name_prefixes),
+                mContext.getString(com.android.internal.R.string.common_name_suffixes),
+                mContext.getString(com.android.internal.R.string.common_name_conjunctions),
+                Locale.getDefault());
+    }
+
     /**
      * Return the {@link ApplicationInfo#uid} for the given package name.
      */
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 76966e8..1a41ff5 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -28,7 +28,6 @@
 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.NicknameLookupColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
@@ -105,21 +104,16 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
-import android.text.util.Rfc822Token;
-import android.text.util.Rfc822Tokenizer;
 import android.util.Log;
 
 import java.io.ByteArrayOutputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.lang.ref.SoftReference;
 import java.util.ArrayList;
-import java.util.BitSet;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -1767,6 +1761,7 @@
         mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
 
         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
+        initForDefaultLocale();
 
         mCommonNicknameCache = new CommonNicknameCache(db);
 
@@ -1817,8 +1812,6 @@
                         " LIMIT 1)" +
                 " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
 
-        initByLocale(context);
-
         mNameLookupInsert = db.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
                 + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + ","
                 + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME
@@ -1903,21 +1896,13 @@
         mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
         mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE);
         mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
-        mCommonNicknameCache.preloadNicknameBloomFilter();
+
         return (db != null);
     }
 
-    private void initByLocale(Context context) {
+    private void initForDefaultLocale() {
         mCurrentLocale = getLocale();
-        if (mCurrentLocale == null) {
-            return;
-        }
-        mNameSplitter = new NameSplitter(
-                context.getString(com.android.internal.R.string.common_name_prefixes),
-                context.getString(com.android.internal.R.string.common_last_name_prefixes),
-                context.getString(com.android.internal.R.string.common_name_suffixes),
-                context.getString(com.android.internal.R.string.common_name_conjunctions),
-                mCurrentLocale);
+        mNameSplitter = mDbHelper.createNameSplitter();
         mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
         mPostalSplitter = new PostalSplitter(mCurrentLocale);
     }
@@ -1925,8 +1910,9 @@
     @Override
     public void onConfigurationChanged (Configuration newConfig) {
         if (newConfig != null && mCurrentLocale != null
-                && mCurrentLocale.equals(newConfig.locale)) {
-            initByLocale(getContext());
+                && !mCurrentLocale.equals(newConfig.locale)) {
+            initForDefaultLocale();
+            // TODO rebuild name lookup for the new locale
         }
     }
     protected void verifyAccounts() {
@@ -5181,7 +5167,6 @@
 
     public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name) {
         mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name);
-        mNameLookupBuilder.insertNameShorthandLookup(rawContactId, dataId, name);
     }
 
     private class StructuredNameLookupBuilder extends NameLookupBuilder {
diff --git a/src/com/android/providers/contacts/NameLookupBuilder.java b/src/com/android/providers/contacts/NameLookupBuilder.java
index 5635653..c78e414 100644
--- a/src/com/android/providers/contacts/NameLookupBuilder.java
+++ b/src/com/android/providers/contacts/NameLookupBuilder.java
@@ -19,7 +19,6 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
 
 import android.provider.ContactsContract.FullNameStyle;
-import android.text.TextUtils;
 
 import java.util.Arrays;
 import java.util.Comparator;
@@ -102,6 +101,7 @@
 
         insertNameVariants(rawContactId, dataId, 0, tokenCount, !tooManyTokens, true);
         insertNicknamePermutations(rawContactId, dataId, 0, tokenCount);
+        insertNameShorthandLookup(rawContactId, dataId, name);
     }
 
     protected String normalizeName(String name) {
@@ -201,15 +201,13 @@
         }
     }
 
-    public void insertNameShorthandLookup(long rawContactId, long dataId, String name) {
-        if (!TextUtils.isEmpty(name)) {
-            Iterator<String> it = ContactLocaleUtils.getNameLookupKeys(name, FullNameStyle.CHINESE);
-            if (it != null) {
-                while (it.hasNext()) {
-                    String key = it.next();
-                    insertNameLookup(rawContactId, dataId, NameLookupType.NAME_SHORTHAND,
-                            normalizeName(key));
-                }
+    private void insertNameShorthandLookup(long rawContactId, long dataId, String name) {
+        Iterator<String> it = ContactLocaleUtils.getNameLookupKeys(name, FullNameStyle.CHINESE);
+        if (it != null) {
+            while (it.hasNext()) {
+                String key = it.next();
+                insertNameLookup(rawContactId, dataId, NameLookupType.NAME_SHORTHAND,
+                        normalizeName(key));
             }
         }
     }