am 44c3d51b: am 13d744f2: am 466d8411: am 8d54a6e0: resolved conflicts for merge of b8fb609b to jb-mr1-dev

* commit '44c3d51be8deee28d01e3e3f739a2f6b1fc608fa':
  Do not allow updates to the _data column.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 281f98b..6ea3f0e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -17,6 +17,10 @@
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.BIND_DIRECTORY_SEARCH" />
+    <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
+    <uses-permission android:name="com.android.voicemail.permission.READ_WRITE_ALL_VOICEMAIL" />
 
     <application android:process="android.process.acore"
         android:label="@string/app_label"
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index 6682d21..05d46fa 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -26,7 +26,7 @@
     <string name="default_directory" msgid="93961630309570294">"Anwani"</string>
     <string name="local_invisible_directory" msgid="705244318477396120">"Nyingineyo"</string>
     <string name="read_write_all_voicemail_label" msgid="4557216100818257560">"Fikia barua zote za sauti"</string>
-    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Inaruhusu programu kuhifadhi na kutoa jumbe zote za sauti ambazo kifaa hiki kinaweza kufikia."</string>
+    <string name="read_write_all_voicemail_description" msgid="8029809937805761356">"Inaruhusu programu kuhifadhi na kutoa mawasiliano yote ya sauti ambayo kifaa hiki kinaweza kufikia."</string>
     <string name="voicemail_from_column" msgid="435732568832121444">"Barua ya sauti kutoka "</string>
     <string name="debug_dump_title" msgid="4916885724165570279">"Nakili hifadhidata ya anwani"</string>
     <string name="debug_dump_database_message" msgid="406438635002392290">"Unakaribia 1) kuunda nakala ya hifadhidata yako ambayo inajumuisha maelezo yote yanayohusiana na anwani na kumbukumbu zote za simu katika hifadhi ya ndani, na 2) uitume kwa barua pepe. Kumbuka kufuta nakala pindi tu unapoinakili kwa ufanisi kutoka kwenye kifaa au barua pepe imepokewa."</string>
diff --git a/src/com/android/providers/contacts/AccountWithDataSet.java b/src/com/android/providers/contacts/AccountWithDataSet.java
index b7484cd..bfae112 100644
--- a/src/com/android/providers/contacts/AccountWithDataSet.java
+++ b/src/com/android/providers/contacts/AccountWithDataSet.java
@@ -19,7 +19,7 @@
 import android.accounts.Account;
 import android.text.TextUtils;
 
-import com.android.internal.util.Objects;
+import com.google.common.base.Objects;
 
 /**
  * Account information that includes the data set, if any.
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java
index 3480c79..9d5ea93 100644
--- a/src/com/android/providers/contacts/CallLogProvider.java
+++ b/src/com/android/providers/contacts/CallLogProvider.java
@@ -20,6 +20,7 @@
 import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
 import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClause;
 
+import android.app.AppOpsManager;
 import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -94,6 +95,7 @@
 
     @Override
     public boolean onCreate() {
+        setAppOps(AppOpsManager.OP_READ_CALL_LOG, AppOpsManager.OP_WRITE_CALL_LOG);
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
             Log.d(Constants.PERFORMANCE_TAG, "CallLogProvider.onCreate start");
         }
diff --git a/src/com/android/providers/contacts/ContactLocaleUtils.java b/src/com/android/providers/contacts/ContactLocaleUtils.java
index 0e7b292..c63f7fc 100644
--- a/src/com/android/providers/contacts/ContactLocaleUtils.java
+++ b/src/com/android/providers/contacts/ContactLocaleUtils.java
@@ -17,78 +17,341 @@
 package com.android.providers.contacts;
 
 import android.provider.ContactsContract.FullNameStyle;
-import android.util.SparseArray;
+import android.provider.ContactsContract.PhoneticNameStyle;
+import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.providers.contacts.HanziToPinyin.Token;
 
+import java.lang.Character.UnicodeBlock;
+import java.util.Arrays;
 import java.util.ArrayList;
+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;
+import java.util.Set;
+
+import libcore.icu.AlphabeticIndex;
+import libcore.icu.AlphabeticIndex.ImmutableIndex;
+import libcore.icu.Transliterator;
 
 /**
- * This utility class provides customized sort key and name lookup key according the locale.
+ * This utility class provides specialized handling for locale specific
+ * information: labels, name lookup keys.
  */
 public class ContactLocaleUtils {
+    public static final String TAG = "ContactLocale";
+
+    public static final Locale LOCALE_ARABIC = new Locale("ar");
+    public static final Locale LOCALE_GREEK = new Locale("el");
+    public static final Locale LOCALE_HEBREW = new Locale("he");
+    // Ukrainian labels are superset of Russian
+    public static final Locale LOCALE_UKRAINIAN = new Locale("uk");
+    public static final Locale LOCALE_THAI = new Locale("th");
 
     /**
-     * This class is the default implementation.
-     * <p>
-     * It should be the base class for other locales' implementation.
+     * This class is the default implementation and should be the base class
+     * for other locales.
+     *
+     * sortKey: same as name
+     * nameLookupKeys: none
+     * labels: uses ICU AlphabeticIndex for labels and extends by labeling
+     *     phone numbers "#".  Eg English labels are: [A-Z], #, " "
      */
-    public class ContactLocaleUtilsBase {
-        public String getSortKey(String displayName) {
-            return displayName;
+    private static class ContactLocaleUtilsBase {
+        private static final String EMPTY_STRING = "";
+        private static final String NUMBER_STRING = "#";
+
+        protected final ImmutableIndex mAlphabeticIndex;
+        private final int mAlphabeticIndexBucketCount;
+        private final int mNumberBucketIndex;
+
+        public ContactLocaleUtilsBase(Locale locale) {
+            // AlphabeticIndex.getBucketLabel() uses a binary search across
+            // the entire label set so care should be taken about growing this
+            // set too large. The following set determines for which locales
+            // we will show labels other than your primary locale. General rules
+            // of thumb for adding a locale: should be a supported locale; and
+            // should not be included if from a name it is not deterministic
+            // which way to label it (so eg Chinese cannot be added because
+            // the labeling of a Chinese character varies between Simplified,
+            // Traditional, and Japanese locales). Use English only for all
+            // Latin based alphabets. Ukrainian is chosen for Cyrillic because
+            // its alphabet is a superset of Russian.
+            mAlphabeticIndex = new AlphabeticIndex(locale)
+                .setMaxLabelCount(300)
+                .addLabels(Locale.ENGLISH)
+                .addLabels(Locale.JAPANESE)
+                .addLabels(Locale.KOREAN)
+                .addLabels(LOCALE_THAI)
+                .addLabels(LOCALE_ARABIC)
+                .addLabels(LOCALE_HEBREW)
+                .addLabels(LOCALE_GREEK)
+                .addLabels(LOCALE_UKRAINIAN)
+                .getImmutableIndex();
+            mAlphabeticIndexBucketCount = mAlphabeticIndex.getBucketCount();
+            mNumberBucketIndex = mAlphabeticIndexBucketCount - 1;
         }
+
+        public String getSortKey(String name) {
+            return name;
+        }
+
+        /**
+         * Returns the bucket index for the specified string. AlphabeticIndex
+         * sorts strings into buckets numbered in order from 0 to N, where the
+         * exact value of N depends on how many representative index labels are
+         * used in a particular locale. This routine adds one additional bucket
+         * for phone numbers. It attempts to detect phone numbers and shifts
+         * the bucket indexes returned by AlphabeticIndex in order to make room
+         * for the new # bucket, so the returned range becomes 0 to N+1.
+         */
+        public int getBucketIndex(String name) {
+            boolean prefixIsNumeric = false;
+            final int length = name.length();
+            int offset = 0;
+            while (offset < length) {
+                int codePoint = Character.codePointAt(name, offset);
+                // Ignore standard phone number separators and identify any
+                // string that otherwise starts with a number.
+                if (Character.isDigit(codePoint)) {
+                    prefixIsNumeric = true;
+                    break;
+                } else if (!Character.isSpaceChar(codePoint) &&
+                           codePoint != '+' && codePoint != '(' &&
+                           codePoint != ')' && codePoint != '.' &&
+                           codePoint != '-' && codePoint != '#') {
+                    break;
+                }
+                offset += Character.charCount(codePoint);
+            }
+            if (prefixIsNumeric) {
+                return mNumberBucketIndex;
+            }
+
+            final int bucket = mAlphabeticIndex.getBucketIndex(name);
+            if (bucket < 0) {
+                return -1;
+            }
+            if (bucket >= mNumberBucketIndex) {
+                return bucket + 1;
+            }
+            return bucket;
+        }
+
+        /**
+         * Returns the number of buckets in use (one more than AlphabeticIndex
+         * uses, because this class adds a bucket for phone numbers).
+         */
+        public int getBucketCount() {
+            return mAlphabeticIndexBucketCount + 1;
+        }
+
+        /**
+         * Returns the label for the specified bucket index if a valid index,
+         * otherwise returns an empty string. '#' is returned for the phone
+         * number bucket; for all others, the AlphabeticIndex label is returned.
+         */
+        public String getBucketLabel(int bucketIndex) {
+            if (bucketIndex < 0 || bucketIndex >= getBucketCount()) {
+                return EMPTY_STRING;
+            } else if (bucketIndex == mNumberBucketIndex) {
+                return NUMBER_STRING;
+            } else if (bucketIndex > mNumberBucketIndex) {
+                --bucketIndex;
+            }
+            return mAlphabeticIndex.getBucketLabel(bucketIndex);
+        }
+
         @SuppressWarnings("unused")
-        public Iterator<String> getNameLookupKeys(String name) {
+        public Iterator<String> getNameLookupKeys(String name, int nameStyle) {
             return null;
         }
+
+        public ArrayList<String> getLabels() {
+            final int bucketCount = getBucketCount();
+            final ArrayList<String> labels = new ArrayList<String>(bucketCount);
+            for(int i = 0; i < bucketCount; ++i) {
+                labels.add(getBucketLabel(i));
+            }
+            return labels;
+        }
     }
 
     /**
-     * The classes to generate the Chinese style sort and search keys.
-     * <p>
-     * The sorting key is generated as each Chinese character' pinyin proceeding with
-     * space and character itself. If the character's pinyin unable to find, the character
-     * itself will be used.
-     * <p>
-     * The below additional name lookup keys will be generated.
-     * a. Chinese character's pinyin and pinyin's initial character.
-     * b. Latin word and the initial character for Latin word.
-     * The name lookup keys are generated to make sure the name can be found by from any
-     * initial character.
+     * Japanese specific locale overrides.
+     *
+     * sortKey: unchanged (same as name)
+     * nameLookupKeys: unchanged (none)
+     * labels: extends default labels by labeling unlabeled CJ characters
+     *     with the Japanese character 他 ("misc"). Japanese labels are:
+     *     あ, か, さ, た, な, は, ま, や, ら, わ, 他, [A-Z], #, " "
      */
-    private class ChineseContactUtils extends ContactLocaleUtilsBase {
+    private static class JapaneseContactUtils extends ContactLocaleUtilsBase {
+        // \u4ed6 is Japanese character 他 ("misc")
+        private static final String JAPANESE_MISC_LABEL = "\u4ed6";
+        private final int mMiscBucketIndex;
+
+        public JapaneseContactUtils(Locale locale) {
+            super(locale);
+            // Determine which bucket AlphabeticIndex is lumping unclassified
+            // Japanese characters into by looking up the bucket index for
+            // a representative Kanji/CJK unified ideograph (\u65e5 is the
+            // character '日').
+            mMiscBucketIndex = super.getBucketIndex("\u65e5");
+        }
+
+        // Set of UnicodeBlocks for unified CJK (Chinese) characters and
+        // Japanese characters. This includes all code blocks that might
+        // contain a character used in Japanese (which is why unified CJK
+        // blocks are included but Korean Hangul and jamo are not).
+        private static final Set<Character.UnicodeBlock> CJ_BLOCKS;
+        static {
+            Set<UnicodeBlock> set = new HashSet<UnicodeBlock>();
+            set.add(UnicodeBlock.HIRAGANA);
+            set.add(UnicodeBlock.KATAKANA);
+            set.add(UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS);
+            set.add(UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS);
+            set.add(UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS);
+            set.add(UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A);
+            set.add(UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B);
+            set.add(UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION);
+            set.add(UnicodeBlock.CJK_RADICALS_SUPPLEMENT);
+            set.add(UnicodeBlock.CJK_COMPATIBILITY);
+            set.add(UnicodeBlock.CJK_COMPATIBILITY_FORMS);
+            set.add(UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS);
+            set.add(UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT);
+            CJ_BLOCKS = Collections.unmodifiableSet(set);
+        }
+
+        /**
+         * Helper routine to identify unlabeled Chinese or Japanese characters
+         * to put in a 'misc' bucket.
+         *
+         * @return true if the specified Unicode code point is Chinese or
+         *              Japanese
+         */
+        private static boolean isChineseOrJapanese(int codePoint) {
+            return CJ_BLOCKS.contains(UnicodeBlock.of(codePoint));
+        }
+
+        /**
+         * Returns the bucket index for the specified string. Adds an
+         * additional 'misc' bucket for Kanji characters to the base class set.
+         */
         @Override
-        public String getSortKey(String displayName) {
-            ArrayList<Token> tokens = HanziToPinyin.getInstance().get(displayName);
-            if (tokens != null && tokens.size() > 0) {
-                StringBuilder sb = new StringBuilder();
-                for (Token token : tokens) {
-                    // Put Chinese character's pinyin, then proceed with the
-                    // character itself.
-                    if (Token.PINYIN == token.type) {
-                        if (sb.length() > 0) {
-                            sb.append(' ');
-                        }
-                        sb.append(token.target);
-                        sb.append(' ');
-                        sb.append(token.source);
-                    } else {
-                        if (sb.length() > 0) {
-                            sb.append(' ');
-                        }
-                        sb.append(token.source);
-                    }
-                }
-                return sb.toString();
+        public int getBucketIndex(String name) {
+            final int bucketIndex = super.getBucketIndex(name);
+            if ((bucketIndex == mMiscBucketIndex &&
+                 !isChineseOrJapanese(Character.codePointAt(name, 0))) ||
+                bucketIndex > mMiscBucketIndex) {
+                return bucketIndex + 1;
             }
-            return super.getSortKey(displayName);
+            return bucketIndex;
+        }
+
+        /**
+         * Returns the number of buckets in use (one more than the base class
+         * uses, because this class adds a bucket for Kanji).
+         */
+        @Override
+        public int getBucketCount() {
+            return super.getBucketCount() + 1;
+        }
+
+        /**
+         * Returns the label for the specified bucket index if a valid index,
+         * otherwise returns an empty string. '他' is returned for unclassified
+         * Kanji; for all others, the label determined by the base class is
+         * returned.
+         */
+        @Override
+        public String getBucketLabel(int bucketIndex) {
+            if (bucketIndex == mMiscBucketIndex) {
+                return JAPANESE_MISC_LABEL;
+            } else if (bucketIndex > mMiscBucketIndex) {
+                --bucketIndex;
+            }
+            return super.getBucketLabel(bucketIndex);
         }
 
         @Override
-        public Iterator<String> getNameLookupKeys(String name) {
+        public Iterator<String> getNameLookupKeys(String name, int nameStyle) {
+            // Hiragana and Katakana will be positively identified as Japanese.
+            if (nameStyle == PhoneticNameStyle.JAPANESE) {
+                return getRomajiNameLookupKeys(name);
+            }
+            return null;
+        }
+
+        private static boolean mInitializedTransliterator;
+        private static Transliterator mJapaneseTransliterator;
+
+        private static Transliterator getJapaneseTransliterator() {
+            synchronized(JapaneseContactUtils.class) {
+                if (!mInitializedTransliterator) {
+                    mInitializedTransliterator = true;
+                    Transliterator t = null;
+                    try {
+                        t = new Transliterator("Hiragana-Latin; Katakana-Latin;"
+                                + " Latin-Ascii");
+                    } catch (RuntimeException e) {
+                        Log.w(TAG, "Hiragana/Katakana-Latin transliterator data"
+                                + " is missing");
+                    }
+                    mJapaneseTransliterator = t;
+                }
+                return mJapaneseTransliterator;
+            }
+        }
+
+        public static Iterator<String> getRomajiNameLookupKeys(String name) {
+            final Transliterator t = getJapaneseTransliterator();
+            if (t == null) {
+                return null;
+            }
+            final String romajiName = t.transliterate(name);
+            if (TextUtils.isEmpty(romajiName) ||
+                    TextUtils.equals(name, romajiName)) {
+                return null;
+            }
+            final HashSet<String> keys = new HashSet<String>();
+            keys.add(romajiName);
+            return keys.iterator();
+        }
+    }
+
+    /**
+     * Simplified Chinese specific locale overrides. Uses ICU Transliterator
+     * for generating pinyin transliteration.
+     *
+     * sortKey: unchanged (same as name)
+     * nameLookupKeys: adds additional name lookup keys
+     *     - Chinese character's pinyin and pinyin's initial character.
+     *     - Latin word and initial character.
+     * labels: unchanged
+     *     Simplified Chinese labels are the same as English: [A-Z], #, " "
+     */
+    private static class SimplifiedChineseContactUtils
+        extends ContactLocaleUtilsBase {
+        public SimplifiedChineseContactUtils(Locale locale) {
+            super(locale);
+        }
+
+        @Override
+        public Iterator<String> getNameLookupKeys(String name, int nameStyle) {
+            if (nameStyle != FullNameStyle.JAPANESE &&
+                    nameStyle != FullNameStyle.KOREAN) {
+                return getPinyinNameLookupKeys(name);
+            }
+            return null;
+        }
+
+        public static Iterator<String> getPinyinNameLookupKeys(String name) {
             // TODO : Reduce the object allocation.
             HashSet<String> keys = new HashSet<String>();
             ArrayList<Token> tokens = HanziToPinyin.getInstance().get(name);
@@ -96,11 +359,14 @@
             final StringBuilder keyPinyin = new StringBuilder();
             final StringBuilder keyInitial = new StringBuilder();
             // There is no space among the Chinese Characters, the variant name
-            // lookup key wouldn't work for Chinese. The keyOrignal is used to
+            // lookup key wouldn't work for Chinese. The keyOriginal is used to
             // build the lookup keys for itself.
-            final StringBuilder keyOrignal = new StringBuilder();
+            final StringBuilder keyOriginal = new StringBuilder();
             for (int i = tokenCount - 1; i >= 0; i--) {
                 final Token token = tokens.get(i);
+                if (Token.UNKNOWN == token.type) {
+                    continue;
+                }
                 if (Token.PINYIN == token.type) {
                     keyPinyin.insert(0, token.target);
                     keyInitial.insert(0, token.target.charAt(0));
@@ -109,14 +375,14 @@
                     if (keyPinyin.length() > 0) {
                         keyPinyin.insert(0, ' ');
                     }
-                    if (keyOrignal.length() > 0) {
-                        keyOrignal.insert(0, ' ');
+                    if (keyOriginal.length() > 0) {
+                        keyOriginal.insert(0, ' ');
                     }
                     keyPinyin.insert(0, token.source);
                     keyInitial.insert(0, token.source.charAt(0));
                 }
-                keyOrignal.insert(0, token.source);
-                keys.add(keyOrignal.toString());
+                keyOriginal.insert(0, token.source);
+                keys.add(keyOriginal.toString());
                 keys.add(keyPinyin.toString());
                 keys.add(keyInitial.toString());
             }
@@ -129,84 +395,89 @@
     private static final String KOREAN_LANGUAGE = Locale.KOREAN.getLanguage().toLowerCase();
 
     private static ContactLocaleUtils sSingleton;
-    private final SparseArray<ContactLocaleUtilsBase> mUtils =
-            new SparseArray<ContactLocaleUtilsBase>();
 
-    private final ContactLocaleUtilsBase mBase = new ContactLocaleUtilsBase();
+    private final Locale mLocale;
+    private final String mLanguage;
+    private final ContactLocaleUtilsBase mUtils;
 
-    private String mLanguage;
-
-    private ContactLocaleUtils() {
-        setLocale(null);
-    }
-
-    public void setLocale(Locale currentLocale) {
-        if (currentLocale == null) {
-            mLanguage = Locale.getDefault().getLanguage().toLowerCase();
+    private ContactLocaleUtils(Locale locale) {
+        if (locale == null) {
+            mLocale = Locale.getDefault();
         } else {
-            mLanguage = currentLocale.getLanguage().toLowerCase();
+            mLocale = locale;
         }
-    }
-
-    public String getSortKey(String displayName, int nameStyle) {
-        return getForSort(Integer.valueOf(nameStyle)).getSortKey(displayName);
-    }
-
-    public Iterator<String> getNameLookupKeys(String name, int nameStyle) {
-        return getForNameLookup(Integer.valueOf(nameStyle)).getNameLookupKeys(name);
-    }
-
-    /**
-     *  Determine which utility should be used for generating NameLookupKey.
-     *  <p>
-     *  a. For Western style name, if the current language is Chinese, the
-     *     ChineseContactUtils should be used.
-     *  b. For Chinese and CJK style name if current language is neither Japanese or Korean,
-     *     the ChineseContactUtils should be used.
-     */
-    private ContactLocaleUtilsBase getForNameLookup(Integer nameStyle) {
-        int nameStyleInt = nameStyle.intValue();
-        Integer adjustedUtil = Integer.valueOf(getAdjustedStyle(nameStyleInt));
-        if (CHINESE_LANGUAGE.equals(mLanguage) && nameStyleInt == FullNameStyle.WESTERN) {
-            adjustedUtil = Integer.valueOf(FullNameStyle.CHINESE);
+        mLanguage = mLocale.getLanguage().toLowerCase();
+        if (mLanguage.equals(JAPANESE_LANGUAGE)) {
+            mUtils = new JapaneseContactUtils(mLocale);
+        } else if (mLocale.equals(Locale.CHINA)) {
+            mUtils = new SimplifiedChineseContactUtils(mLocale);
+        } else {
+            mUtils = new ContactLocaleUtilsBase(mLocale);
         }
-        return get(adjustedUtil);
+        Log.i(TAG, "AddressBook Labels [" + mLocale.toString() + "]: "
+              + getLabels().toString());
     }
 
-    private synchronized ContactLocaleUtilsBase get(Integer nameStyle) {
-        ContactLocaleUtilsBase utils = mUtils.get(nameStyle);
-        if (utils == null) {
-            if (nameStyle.intValue() == FullNameStyle.CHINESE) {
-                utils = new ChineseContactUtils();
-                mUtils.put(nameStyle, utils);
-            }
-        }
-        return (utils == null) ? mBase : utils;
+    public boolean isLocale(Locale locale) {
+        return mLocale.equals(locale);
     }
 
-    /**
-     *  Determine the which utility should be used for generating sort key.
-     *  <p>
-     *  For Chinese and CJK style name if current language is neither Japanese or Korean,
-     *  the ChineseContactUtils should be used.
-     */
-    private ContactLocaleUtilsBase getForSort(Integer nameStyle) {
-        return get(Integer.valueOf(getAdjustedStyle(nameStyle.intValue())));
-    }
-
-    public static synchronized ContactLocaleUtils getIntance() {
+    public static synchronized ContactLocaleUtils getInstance() {
         if (sSingleton == null) {
-            sSingleton = new ContactLocaleUtils();
+            sSingleton = new ContactLocaleUtils(null);
         }
         return sSingleton;
     }
 
-    private int getAdjustedStyle(int nameStyle) {
-        if (nameStyle == FullNameStyle.CJK  && !JAPANESE_LANGUAGE.equals(mLanguage) &&
-                !KOREAN_LANGUAGE.equals(mLanguage)) {
-            return FullNameStyle.CHINESE;
-        } else {
-            return nameStyle;
+    public static synchronized void setLocale(Locale locale) {
+        if (sSingleton == null || !sSingleton.isLocale(locale)) {
+            sSingleton = new ContactLocaleUtils(locale);
         }
     }
+
+    public String getSortKey(String name, int nameStyle) {
+        return mUtils.getSortKey(name);
+    }
+
+    public int getBucketIndex(String name) {
+        return mUtils.getBucketIndex(name);
+    }
+
+    public int getBucketCount() {
+        return mUtils.getBucketCount();
+    }
+
+    public String getBucketLabel(int bucketIndex) {
+        return mUtils.getBucketLabel(bucketIndex);
+    }
+
+    public String getLabel(String name) {
+        return getBucketLabel(getBucketIndex(name));
+    }
+
+    public ArrayList<String> getLabels() {
+        return mUtils.getLabels();
+    }
+
+    /**
+     *  Determine which utility should be used for generating NameLookupKey.
+     *  (ie, whether we generate Pinyin lookup keys or not)
+     *
+     *  Hiragana and Katakana are tagged as JAPANESE; Kanji is unclassified
+     *  and tagged as CJK. For Hiragana/Katakana names, generate Romaji
+     *  lookup keys when not in a Chinese or Korean locale.
+     *
+     *  Otherwise, use the default behavior of that locale:
+     *  a. For Japan, generate Romaji lookup keys for Hiragana/Katakana.
+     *  b. For Simplified Chinese locale, generate Pinyin lookup keys.
+     */
+    public Iterator<String> getNameLookupKeys(String name, int nameStyle) {
+        if (nameStyle == FullNameStyle.JAPANESE &&
+                !CHINESE_LANGUAGE.equals(mLanguage) &&
+                !KOREAN_LANGUAGE.equals(mLanguage)) {
+            return JapaneseContactUtils.getRomajiNameLookupKeys(name);
+        }
+        return mUtils.getNameLookupKeys(name, nameStyle);
+    }
+
 }
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index ad602b8..1c0fd7f 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -19,6 +19,7 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
@@ -77,6 +78,9 @@
 
 import com.android.common.content.SyncStateContentProviderHelper;
 import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
+import com.android.providers.contacts.database.ContactsTableUtil;
+import com.android.providers.contacts.database.DeletedContactsTableUtil;
+import com.android.providers.contacts.database.MoreDatabaseUtils;
 import com.android.providers.contacts.util.NeededForTesting;
 import com.google.android.collect.Sets;
 
@@ -84,6 +88,8 @@
 import java.util.Locale;
 import java.util.Set;
 
+import libcore.icu.ICU;
+
 /**
  * Database helper for contacts. Designed as a singleton to make sure that all
  * {@link android.content.ContentProvider} users get the same reference.
@@ -107,13 +113,14 @@
      *   700-799 Jelly Bean
      * </pre>
      */
-    static final int DATABASE_VERSION = 706;
+    static final int DATABASE_VERSION = 710;
 
     private static final String DATABASE_NAME = "contacts2.db";
     private static final String DATABASE_PRESENCE = "presence_db";
 
     public interface Tables {
         public static final String CONTACTS = "contacts";
+        public static final String DELETED_CONTACTS = "deleted_contacts";
         public static final String RAW_CONTACTS = "raw_contacts";
         public static final String STREAM_ITEMS = "stream_items";
         public static final String STREAM_ITEM_PHOTOS = "stream_item_photos";
@@ -359,6 +366,12 @@
                 + Contacts.SEND_TO_VOICEMAIL;
         public static final String CONCRETE_LOOKUP_KEY = Tables.CONTACTS + "."
                 + Contacts.LOOKUP_KEY;
+        public static final String CONCRETE_CONTACT_LAST_UPDATED_TIMESTAMP = Tables.CONTACTS + "."
+                + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP;
+        public static final String PHONEBOOK_LABEL_PRIMARY = "phonebook_label";
+        public static final String PHONEBOOK_BUCKET_PRIMARY = "phonebook_bucket";
+        public static final String PHONEBOOK_LABEL_ALTERNATIVE = "phonebook_label_alt";
+        public static final String PHONEBOOK_BUCKET_ALTERNATIVE = "phonebook_bucket_alt";
     }
 
     public interface RawContactsColumns {
@@ -403,7 +416,15 @@
         public static final String CONCRETE_CONTACT_ID =
                 Tables.RAW_CONTACTS + "." + RawContacts.CONTACT_ID;
         public static final String CONCRETE_NAME_VERIFIED =
-                Tables.RAW_CONTACTS + "." + RawContacts.NAME_VERIFIED;
+            Tables.RAW_CONTACTS + "." + RawContacts.NAME_VERIFIED;
+        public static final String PHONEBOOK_LABEL_PRIMARY =
+            ContactsColumns.PHONEBOOK_LABEL_PRIMARY;
+        public static final String PHONEBOOK_BUCKET_PRIMARY =
+            ContactsColumns.PHONEBOOK_BUCKET_PRIMARY;
+        public static final String PHONEBOOK_LABEL_ALTERNATIVE =
+            ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE;
+        public static final String PHONEBOOK_BUCKET_ALTERNATIVE =
+            ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE;
     }
 
     public interface ViewRawContactsColumns {
@@ -694,6 +715,8 @@
         String DIRECTORY_SCAN_COMPLETE = "directoryScanComplete";
         String AGGREGATION_ALGORITHM = "aggregation_v2";
         String KNOWN_ACCOUNTS = "known_accounts";
+        String ICU_VERSION = "icu_version";
+        String LOCALE = "locale";
     }
 
     /** In-memory cache of previously found MIME-type mappings */
@@ -945,16 +968,14 @@
                 Contacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
                 Contacts.HAS_PHONE_NUMBER + " INTEGER NOT NULL DEFAULT 0," +
                 Contacts.LOOKUP_KEY + " TEXT," +
-                ContactsColumns.LAST_STATUS_UPDATE_ID + " INTEGER REFERENCES data(_id)" +
+                ContactsColumns.LAST_STATUS_UPDATE_ID + " INTEGER REFERENCES data(_id)," +
+                Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " INTEGER" +
         ");");
 
-        db.execSQL("CREATE INDEX contacts_has_phone_index ON " + Tables.CONTACTS + " (" +
-                Contacts.HAS_PHONE_NUMBER +
-        ");");
+        ContactsTableUtil.createIndexes(db);
 
-        db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" +
-                Contacts.NAME_RAW_CONTACT_ID +
-        ");");
+        // deleted_contacts table
+        DeletedContactsTableUtil.create(db);
 
         // Raw_contacts table
         db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" +
@@ -980,11 +1001,18 @@
                 RawContacts.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " +
                         DisplayNameSources.UNDEFINED + "," +
                 RawContacts.PHONETIC_NAME + " TEXT," +
+                // TODO: PHONETIC_NAME_STYLE should be INTEGER. There is a
+                // mismatch between how the column is created here (TEXT) and
+                // how it is created in upgradeToVersion205 (INTEGER).
                 RawContacts.PHONETIC_NAME_STYLE + " TEXT," +
                 RawContacts.SORT_KEY_PRIMARY + " TEXT COLLATE " +
                         ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," +
+                RawContactsColumns.PHONEBOOK_LABEL_PRIMARY + " TEXT," +
+                RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY + " INTEGER," +
                 RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE " +
                         ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," +
+                RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + " TEXT," +
+                RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + " INTEGER," +
                 RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0," +
                 RawContacts.SYNC1 + " TEXT, " +
                 RawContacts.SYNC2 + " TEXT, " +
@@ -1316,6 +1344,12 @@
 
         ContentResolver.requestSync(null /* all accounts */,
                 ContactsContract.AUTHORITY, new Bundle());
+
+        // Only send broadcasts for regular contacts db.
+        if (dbForProfile() == 0) {
+            mContext.sendBroadcast(new Intent(ContactsContract.Intents.CONTACTS_DATABASE_CREATED),
+                    android.Manifest.permission.READ_CONTACTS);
+        }
     }
 
     protected void initializeAutoIncrementSequences(SQLiteDatabase db) {
@@ -1585,7 +1619,8 @@
                 + Contacts.PHOTO_FILE_ID + ", "
                 + "CAST(" + Clauses.CONTACT_VISIBLE + " AS INTEGER) AS "
                         + Contacts.IN_VISIBLE_GROUP + ", "
-                + ContactsColumns.LAST_STATUS_UPDATE_ID;
+                + ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
+                + ContactsColumns.CONCRETE_CONTACT_LAST_UPDATED_TIMESTAMP;
 
         String contactOptionColumns =
                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE
@@ -1612,8 +1647,16 @@
                         + " AS " + Contacts.PHONETIC_NAME_STYLE + ", "
                 + "name_raw_contact." + RawContacts.SORT_KEY_PRIMARY
                         + " AS " + Contacts.SORT_KEY_PRIMARY + ", "
+                + "name_raw_contact." + RawContactsColumns.PHONEBOOK_LABEL_PRIMARY
+                        + " AS " + ContactsColumns.PHONEBOOK_LABEL_PRIMARY + ", "
+                + "name_raw_contact." + RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY
+                        + " AS " + ContactsColumns.PHONEBOOK_BUCKET_PRIMARY + ", "
                 + "name_raw_contact." + RawContacts.SORT_KEY_ALTERNATIVE
-                        + " AS " + Contacts.SORT_KEY_ALTERNATIVE;
+                        + " AS " + Contacts.SORT_KEY_ALTERNATIVE + ", "
+                + "name_raw_contact." + RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE
+                        + " AS " + ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + ", "
+                + "name_raw_contact." + RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE
+                        + " AS " + ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE;
 
         String dataSelect = "SELECT "
                 + DataColumns.CONCRETE_ID + " AS " + Data._ID + ","
@@ -1670,7 +1713,11 @@
                 + RawContacts.PHONETIC_NAME  + ", "
                 + RawContacts.PHONETIC_NAME_STYLE  + ", "
                 + RawContacts.SORT_KEY_PRIMARY  + ", "
+                + RawContactsColumns.PHONEBOOK_LABEL_PRIMARY  + ", "
+                + RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY  + ", "
                 + RawContacts.SORT_KEY_ALTERNATIVE + ", "
+                + RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE  + ", "
+                + RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE  + ", "
                 + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ", "
                 + rawContactOptionColumns + ", "
                 + syncColumns
@@ -1912,6 +1959,16 @@
     }
 
     @Override
+    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        Log.i(TAG, "ContactsProvider cannot proceed because downgrading your database is not " +
+                "supported. To continue, please either re-upgrade to your previous Android " +
+                "version, or clear all application data in Contacts Storage (this will result " +
+                "in the loss of all local contacts that are not synced). To avoid data loss, " +
+                "your contacts database will not be wiped automatically.");
+        super.onDowngrade(db, oldVersion, newVersion);
+    }
+
+    @Override
     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
         if (oldVersion < 99) {
             Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion
@@ -1948,6 +2005,7 @@
         boolean upgradeSearchIndex = false;
         boolean rescanDirectories = false;
         boolean rebuildSqliteStats = false;
+        boolean upgradeLocaleSpecificData = false;
 
         if (oldVersion == 99) {
             upgradeViewsAndTriggers = true;
@@ -2398,6 +2456,31 @@
             oldVersion = 706;
         }
 
+        if (oldVersion < 707) {
+            upgradeToVersion707(db);
+            upgradeViewsAndTriggers = true;
+            oldVersion = 707;
+        }
+
+        if (oldVersion < 708) {
+            // Sort keys, phonebook labels and buckets, and search keys have
+            // changed so force a rebuild.
+            upgradeLocaleSpecificData = true;
+            oldVersion = 708;
+        }
+        if (oldVersion < 709) {
+            // Added secondary locale phonebook labels; changed Japanese
+            // and Chinese sort keys.
+            upgradeLocaleSpecificData = true;
+            oldVersion = 709;
+        }
+
+        if (oldVersion < 710) {
+            upgradeToVersion710(db);
+            upgradeViewsAndTriggers = true;
+            oldVersion = 710;
+        }
+
         if (upgradeViewsAndTriggers) {
             createContactsViews(db);
             createGroupsView(db);
@@ -2411,14 +2494,21 @@
             LegacyApiSupport.createViews(db);
         }
 
+        if (upgradeLocaleSpecificData) {
+            upgradeLocaleData(db, false /* we build stats table later */);
+            // Name lookups are rebuilt as part of the full locale rebuild
+            upgradeNameLookup = false;
+            upgradeSearchIndex = true;
+            rebuildSqliteStats = true;
+        }
+
         if (upgradeNameLookup) {
             rebuildNameLookup(db, false /* we build stats table later */);
             rebuildSqliteStats = true;
         }
 
         if (upgradeSearchIndex) {
-            createSearchIndexTable(db, false /* we build stats table later */);
-            setProperty(db, SearchIndexManager.PROPERTY_SEARCH_INDEX_VERSION, "0");
+            rebuildSearchIndex(db, false /* we build stats table later */);
             rebuildSqliteStats = true;
         }
 
@@ -2708,8 +2798,7 @@
                 sortKey = sortKeyAlternative = phoneticName;
             } else if (name.fullNameStyle == FullNameStyle.CHINESE ||
                     name.fullNameStyle == FullNameStyle.CJK) {
-                sortKey = sortKeyAlternative = ContactLocaleUtils.getIntance()
-                        .getSortKey(displayName, name.fullNameStyle);
+                sortKey = sortKeyAlternative = displayName;
             }
 
             if (sortKey == null) {
@@ -2766,20 +2855,7 @@
                 organizationUpdate.bindLong(2, dataId);
                 organizationUpdate.execute();
 
-                String sortKey = null;
-                if (phoneticName == null && company != null) {
-                    int nameStyle = splitter.guessFullNameStyle(company);
-                    nameStyle = splitter.getAdjustedFullNameStyle(nameStyle);
-                    if (nameStyle == FullNameStyle.CHINESE ||
-                            nameStyle == FullNameStyle.CJK ) {
-                        sortKey = ContactLocaleUtils.getIntance()
-                                .getSortKey(company, nameStyle);
-                    }
-                }
-
-                if (sortKey == null) {
-                    sortKey = company;
-                }
+                String sortKey = company;
 
                 updateRawContact205(rawContactUpdate, rawContactId, company,
                         company, phoneticNameStyle, phoneticName, sortKey, sortKey);
@@ -2997,25 +3073,81 @@
         createContactsIndexes(db, rebuildSqliteStats);
     }
 
+    protected void rebuildSearchIndex() {
+        rebuildSearchIndex(getWritableDatabase(), true);
+    }
+
+    private void rebuildSearchIndex(SQLiteDatabase db, boolean rebuildSqliteStats) {
+        createSearchIndexTable(db, rebuildSqliteStats);
+        setProperty(db, SearchIndexManager.PROPERTY_SEARCH_INDEX_VERSION, "0");
+    }
+
     /**
-     * Regenerates all locale-sensitive data: nickname_lookup, name_lookup and sort keys.
+     * Checks whether the current ICU code version matches that used to build
+     * the locale specific data in the ContactsDB.
      */
-    public void setLocale(ContactsProvider2 provider, Locale locale) {
-        Log.i(TAG, "Switching to locale " + locale);
+    public boolean needsToUpdateLocaleData(Locale locale) {
+        final String dbLocale = getProperty(DbProperties.LOCALE, "");
+        if (!dbLocale.equals(locale.toString())) {
+            return true;
+        }
+        final String curICUVersion = ICU.getIcuVersion();
+        final String dbICUVersion = getProperty(DbProperties.ICU_VERSION,
+                "(unknown)");
+        if (!curICUVersion.equals(dbICUVersion)) {
+            Log.i(TAG, "ICU version has changed. Current version is "
+                    + curICUVersion + "; DB was built with " + dbICUVersion);
+            return true;
+        }
+        return false;
+    }
+
+    private void upgradeLocaleData(SQLiteDatabase db, boolean rebuildSqliteStats) {
+        final Locale locale = Locale.getDefault();
+        Log.i(TAG, "Upgrading locale data for " + locale
+                + " (ICU v" + ICU.getIcuVersion() + ")");
+        final long start = SystemClock.elapsedRealtime();
+        initializeCache(db);
+        rebuildLocaleData(db, locale, rebuildSqliteStats);
+        Log.i(TAG, "Locale update completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
+    }
+
+    private void rebuildLocaleData(SQLiteDatabase db, Locale locale,
+            boolean rebuildSqliteStats) {
+        db.execSQL("DROP INDEX raw_contact_sort_key1_index");
+        db.execSQL("DROP INDEX raw_contact_sort_key2_index");
+        db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
+
+        loadNicknameLookupTable(db);
+        insertNameLookup(db);
+        rebuildSortKeys(db);
+        createContactsIndexes(db, rebuildSqliteStats);
+
+        FastScrollingIndexCache.getInstance(mContext).invalidate();
+        // Update the ICU version used to generate the locale derived data
+        // so we can tell when we need to rebuild with new ICU versions.
+        setProperty(db, DbProperties.ICU_VERSION, ICU.getIcuVersion());
+        setProperty(db, DbProperties.LOCALE, locale.toString());
+    }
+
+    /**
+     * Regenerates all locale-sensitive data if needed:
+     * nickname_lookup, name_lookup and sort keys. Invalidates the fast
+     * scrolling index cache.
+     */
+    public void setLocale(Locale locale) {
+        if (!needsToUpdateLocaleData(locale)) {
+            return;
+        }
+        Log.i(TAG, "Switching to locale " + locale
+                + " (ICU v" + ICU.getIcuVersion() + ")");
 
         final long start = SystemClock.elapsedRealtime();
         SQLiteDatabase db = getWritableDatabase();
         db.setLocale(locale);
         db.beginTransaction();
         try {
-            db.execSQL("DROP INDEX raw_contact_sort_key1_index");
-            db.execSQL("DROP INDEX raw_contact_sort_key2_index");
-            db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
-
-            loadNicknameLookupTable(db);
-            insertNameLookup(db);
-            rebuildSortKeys(db, provider);
-            createContactsIndexes(db, true);
+            rebuildLocaleData(db, locale, true);
             db.setTransactionSuccessful();
         } finally {
             db.endTransaction();
@@ -3027,7 +3159,7 @@
     /**
      * Regenerates sort keys for all contacts.
      */
-    private void rebuildSortKeys(SQLiteDatabase db, ContactsProvider2 provider) {
+    private void rebuildSortKeys(SQLiteDatabase db) {
         Cursor cursor = db.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
                 null, null, null, null, null);
         try {
@@ -3800,6 +3932,40 @@
         }
     }
 
+    private void upgradeToVersion707(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+                + " ADD " + RawContactsColumns.PHONEBOOK_LABEL_PRIMARY + " TEXT;");
+        db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+                + " ADD " + RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY + " INTEGER;");
+        db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+                + " ADD " + RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + " TEXT;");
+        db.execSQL("ALTER TABLE " + Tables.RAW_CONTACTS
+                + " ADD " + RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + " INTEGER;");
+    }
+
+    private void upgradeToVersion710(SQLiteDatabase db) {
+
+        // Adding timestamp to contacts table.
+        db.execSQL("ALTER TABLE contacts"
+                + " ADD contact_last_updated_timestamp INTEGER;");
+
+        db.execSQL("UPDATE contacts"
+                + " SET contact_last_updated_timestamp"
+                + " = " + System.currentTimeMillis());
+
+        db.execSQL("CREATE INDEX contacts_contact_last_updated_timestamp_index "
+                + "ON contacts(contact_last_updated_timestamp)");
+
+        // New deleted contacts table.
+        db.execSQL("CREATE TABLE deleted_contacts (" +
+                "contact_id INTEGER PRIMARY KEY," +
+                "contact_deleted_timestamp INTEGER NOT NULL default 0"
+                + ");");
+
+        db.execSQL("CREATE INDEX deleted_contacts_contact_deleted_timestamp_index "
+                + "ON deleted_contacts(contact_deleted_timestamp)");
+    }
+
     public String extractHandleFromEmailAddress(String email) {
         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
         if (tokens.length == 0) {
@@ -3916,6 +4082,8 @@
                     "contacts_has_phone_index", "9000 500");
             updateIndexStats(db, Tables.CONTACTS,
                     "contacts_name_raw_contact_id_index", "9000 1");
+            updateIndexStats(db, Tables.CONTACTS, MoreDatabaseUtils.buildIndexName(Tables.CONTACTS,
+                    Contacts.CONTACT_LAST_UPDATED_TIMESTAMP), "9000 10");
 
             updateIndexStats(db, Tables.RAW_CONTACTS,
                     "raw_contacts_contact_id_index", "10000 2");
@@ -4063,6 +4231,7 @@
         db.execSQL("DELETE FROM " + Tables.CALLS + ";");
         db.execSQL("DELETE FROM " + Tables.DIRECTORIES + ";");
         db.execSQL("DELETE FROM " + Tables.SEARCH_INDEX + ";");
+        db.execSQL("DELETE FROM " + Tables.DELETED_CONTACTS + ";");
 
         initializeCache(db);
 
@@ -4709,33 +4878,14 @@
     }
 
     /**
-     * Delete the aggregate contact if it has no constituent raw contacts other
-     * than the supplied one.
-     */
-    public void removeContactIfSingleton(long rawContactId) {
-        SQLiteDatabase db = getWritableDatabase();
-
-        // Obtain contact ID from the supplied raw contact ID
-        String contactIdFromRawContactId = "(SELECT " + RawContacts.CONTACT_ID + " FROM "
-                + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=" + rawContactId + ")";
-
-        // Find other raw contacts in the same aggregate contact
-        String otherRawContacts = "(SELECT contacts1." + RawContacts._ID + " FROM "
-                + Tables.RAW_CONTACTS + " contacts1 JOIN " + Tables.RAW_CONTACTS + " contacts2 ON ("
-                + "contacts1." + RawContacts.CONTACT_ID + "=contacts2." + RawContacts.CONTACT_ID
-                + ") WHERE contacts1." + RawContacts._ID + "!=" + rawContactId + ""
-                + " AND contacts2." + RawContacts._ID + "=" + rawContactId + ")";
-
-        db.execSQL("DELETE FROM " + Tables.CONTACTS
-                + " WHERE " + Contacts._ID + "=" + contactIdFromRawContactId
-                + " AND NOT EXISTS " + otherRawContacts + ";");
-    }
-
-    /**
      * Returns the value from the {@link Tables#PROPERTIES} table.
      */
     public String getProperty(String key, String defaultValue) {
-        Cursor cursor = getReadableDatabase().query(Tables.PROPERTIES,
+        return getProperty(getReadableDatabase(), key, defaultValue);
+    }
+
+    public String getProperty(SQLiteDatabase db, String key, String defaultValue) {
+        Cursor cursor = db.query(Tables.PROPERTIES,
                 new String[]{PropertiesColumns.PROPERTY_VALUE},
                 PropertiesColumns.PROPERTY_KEY + "=?",
                 new String[]{key}, null, null, null);
@@ -4981,6 +5131,9 @@
                 " FROM " + Tables.DATA +
                 " WHERE " + Data.RAW_CONTACT_ID + "=?" +
                         " AND (" + Data.DATA1 + " NOT NULL OR " +
+                                Data.DATA8 + " NOT NULL OR " +
+                                Data.DATA9 + " NOT NULL OR " +
+                                Data.DATA10 + " NOT NULL OR " +  // Phonetic name not empty
                                 Organization.TITLE + " NOT NULL)";
 
         public static final int MIMETYPE = 0;
@@ -5135,11 +5288,20 @@
         }
 
         if (bestPhoneticName != null) {
+            if (displayNamePrimary == null) {
+                displayNamePrimary = bestPhoneticName;
+            }
+            if (displayNameAlternative == null) {
+                displayNameAlternative = bestPhoneticName;
+            }
+            // Phonetic names disregard name order so displayNamePrimary and displayNameAlternative
+            // are the same.
             sortKeyPrimary = sortKeyAlternative = bestPhoneticName;
             if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) {
                 bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName);
             }
         } else {
+            bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
             if (displayNameStyle == FullNameStyle.UNDEFINED) {
                 displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName);
                 if (displayNameStyle == FullNameStyle.UNDEFINED
@@ -5151,9 +5313,7 @@
             }
             if (displayNameStyle == FullNameStyle.CHINESE ||
                     displayNameStyle == FullNameStyle.CJK) {
-                sortKeyPrimary = sortKeyAlternative =
-                        ContactLocaleUtils.getIntance().getSortKey(
-                                sortNamePrimary, displayNameStyle);
+                sortKeyPrimary = sortKeyAlternative = sortNamePrimary;
             }
         }
 
@@ -5162,6 +5322,21 @@
             sortKeyAlternative = sortNameAlternative;
         }
 
+        String phonebookLabelPrimary = "";
+        String phonebookLabelAlternative = "";
+        int phonebookBucketPrimary = 0;
+        int phonebookBucketAlternative = 0;
+        ContactLocaleUtils localeUtils = ContactLocaleUtils.getInstance();
+
+        if (sortKeyPrimary != null) {
+            phonebookBucketPrimary = localeUtils.getBucketIndex(sortKeyPrimary);
+            phonebookLabelPrimary = localeUtils.getBucketLabel(phonebookBucketPrimary);
+        }
+        if (sortKeyAlternative != null) {
+            phonebookBucketAlternative = localeUtils.getBucketIndex(sortKeyAlternative);
+            phonebookLabelAlternative = localeUtils.getBucketLabel(phonebookBucketAlternative);
+        }
+
         if (mRawContactDisplayNameUpdate == null) {
             mRawContactDisplayNameUpdate = db.compileStatement(
                     "UPDATE " + Tables.RAW_CONTACTS +
@@ -5172,7 +5347,11 @@
                             RawContacts.PHONETIC_NAME + "=?," +
                             RawContacts.PHONETIC_NAME_STYLE + "=?," +
                             RawContacts.SORT_KEY_PRIMARY + "=?," +
-                            RawContacts.SORT_KEY_ALTERNATIVE + "=?" +
+                            RawContactsColumns.PHONEBOOK_LABEL_PRIMARY + "=?," +
+                            RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY + "=?," +
+                            RawContacts.SORT_KEY_ALTERNATIVE + "=?," +
+                            RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + "=?," +
+                            RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + "=?" +
                     " WHERE " + RawContacts._ID + "=?");
         }
 
@@ -5182,8 +5361,12 @@
         bindString(mRawContactDisplayNameUpdate, 4, bestPhoneticName);
         mRawContactDisplayNameUpdate.bindLong(5, bestPhoneticNameStyle);
         bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary);
-        bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative);
-        mRawContactDisplayNameUpdate.bindLong(8, rawContactId);
+        bindString(mRawContactDisplayNameUpdate, 7, phonebookLabelPrimary);
+        mRawContactDisplayNameUpdate.bindLong(8, phonebookBucketPrimary);
+        bindString(mRawContactDisplayNameUpdate, 9, sortKeyAlternative);
+        bindString(mRawContactDisplayNameUpdate, 10, phonebookLabelAlternative);
+        mRawContactDisplayNameUpdate.bindLong(11, phonebookBucketAlternative);
+        mRawContactDisplayNameUpdate.bindLong(12, rawContactId);
         mRawContactDisplayNameUpdate.execute();
     }
 
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index c9c7f2f..5754ced 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -19,6 +19,7 @@
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.OnAccountsUpdateListener;
+import android.app.AppOpsManager;
 import android.app.SearchManager;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
@@ -68,6 +69,7 @@
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Authorization;
+import android.provider.ContactsContract.CommonDataKinds.Contactables;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Identity;
@@ -108,6 +110,7 @@
 
 import com.android.common.content.ProjectionMap;
 import com.android.common.content.SyncStateContentProviderHelper;
+import com.android.common.io.MoreCloseables;
 import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
 import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
@@ -140,6 +143,8 @@
 import com.android.providers.contacts.aggregation.ContactAggregator.AggregationSuggestionParameter;
 import com.android.providers.contacts.aggregation.ProfileAggregator;
 import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
+import com.android.providers.contacts.database.ContactsTableUtil;
+import com.android.providers.contacts.database.DeletedContactsTableUtil;
 import com.android.providers.contacts.util.Clock;
 import com.android.providers.contacts.util.DbQueryUtils;
 import com.android.providers.contacts.util.NeededForTesting;
@@ -192,6 +197,7 @@
     private static final int BACKGROUND_TASK_UPDATE_DIRECTORIES = 8;
     private static final int BACKGROUND_TASK_CHANGE_LOCALE = 9;
     private static final int BACKGROUND_TASK_CLEANUP_PHOTOS = 10;
+    private static final int BACKGROUND_TASK_CLEAN_DELETE_LOG = 11;
 
     /** Default for the maximum number of returned aggregation suggestions. */
     private static final int DEFAULT_MAX_SUGGESTIONS = 5;
@@ -208,6 +214,8 @@
      */
     private static final int DEFAULT_PREAUTHORIZED_URI_EXPIRATION = 5 * 60 * 1000;
 
+    private static final int USAGE_TYPE_ALL = -1;
+
     /**
      * Random URI parameter that will be appended to preauthorized URIs for uniqueness.
      */
@@ -303,6 +311,8 @@
     private static final int CALLABLES = 3011;
     private static final int CALLABLES_ID = 3012;
     private static final int CALLABLES_FILTER = 3013;
+    private static final int CONTACTABLES = 3014;
+    private static final int CONTACTABLES_FILTER = 3015;
 
     private static final int PHONE_LOOKUP = 4000;
 
@@ -363,6 +373,9 @@
     private static final int DISPLAY_PHOTO_ID = 22000;
     private static final int PHOTO_DIMENSIONS = 22001;
 
+    private static final int DELETED_CONTACTS = 23000;
+    private static final int DELETED_CONTACTS_ID = 23001;
+
     // Inserts into URIs in this map will direct to the profile database if the parent record's
     // value (looked up from the ContentValues object with the key specified by the value in this
     // map) is in the profile ID-space (see {@link ProfileDatabaseHelper#PROFILE_ID_SPACE}).
@@ -514,11 +527,11 @@
     private static final String EMAIL_FILTER_SORT_ORDER =
         Contacts.STARRED + " DESC, "
         + Data.IS_SUPER_PRIMARY + " DESC, "
-        + Data.IS_PRIMARY + " DESC, "
         + SORT_BY_DATA_USAGE + ", "
         + Contacts.IN_VISIBLE_GROUP + " DESC, "
         + Contacts.DISPLAY_NAME + ", "
-        + Data.CONTACT_ID;
+        + Data.CONTACT_ID + ", "
+        + Data.IS_PRIMARY + " DESC";
 
     /** Currently same as {@link #EMAIL_FILTER_SORT_ORDER} */
     private static final String PHONE_FILTER_SORT_ORDER = EMAIL_FILTER_SORT_ORDER;
@@ -564,9 +577,14 @@
             .add(Contacts.SEND_TO_VOICEMAIL)
             .add(Contacts.SORT_KEY_ALTERNATIVE)
             .add(Contacts.SORT_KEY_PRIMARY)
+            .add(ContactsColumns.PHONEBOOK_LABEL_PRIMARY)
+            .add(ContactsColumns.PHONEBOOK_BUCKET_PRIMARY)
+            .add(ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE)
+            .add(ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE)
             .add(Contacts.STARRED)
             .add(Contacts.TIMES_CONTACTED)
             .add(Contacts.HAS_PHONE_NUMBER)
+            .add(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP)
             .build();
 
     private static final ProjectionMap sContactsPresenceColumns = ProjectionMap.builder()
@@ -663,6 +681,11 @@
             .add(Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON)
             .build();
 
+    private static final ProjectionMap sDataUsageColumns = ProjectionMap.builder()
+            .add(Data.TIMES_USED, Tables.DATA_USAGE_STAT + "." + Data.TIMES_USED)
+            .add(Data.LAST_TIME_USED, Tables.DATA_USAGE_STAT + "." + Data.LAST_TIME_USED)
+            .build();
+
     /** Contains just BaseColumns._COUNT */
     private static final ProjectionMap sCountProjectionMap = ProjectionMap.builder()
             .add(BaseColumns._COUNT, "COUNT(*)")
@@ -751,6 +774,10 @@
             .add(RawContacts.PHONETIC_NAME_STYLE)
             .add(RawContacts.SORT_KEY_PRIMARY)
             .add(RawContacts.SORT_KEY_ALTERNATIVE)
+            .add(RawContactsColumns.PHONEBOOK_LABEL_PRIMARY)
+            .add(RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY)
+            .add(RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE)
+            .add(RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE)
             .add(RawContacts.TIMES_CONTACTED)
             .add(RawContacts.LAST_TIME_CONTACTED)
             .add(RawContacts.CUSTOM_RINGTONE)
@@ -812,6 +839,7 @@
             .addAll(sRawContactColumns)
             .addAll(sContactsColumns)
             .addAll(sContactPresenceColumns)
+            .addAll(sDataUsageColumns)
             .build();
 
     /** Contains columns from the data view used for SIP address lookup. */
@@ -829,6 +857,7 @@
             .addAll(sDataPresenceColumns)
             .addAll(sContactsColumns)
             .addAll(sContactPresenceColumns)
+            .addAll(sDataUsageColumns)
             .build();
 
     /** Contains columns from the data view used for SIP address lookup. */
@@ -885,6 +914,11 @@
             .add(Groups.SYNC4)
             .build();
 
+    private static final ProjectionMap sDeletedContactsProjectionMap = ProjectionMap.builder()
+            .add(ContactsContract.DeletedContacts.CONTACT_ID)
+            .add(ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP)
+            .build();
+
     /**
      * Contains {@link Groups} columns along with summary details.
      *
@@ -1144,6 +1178,11 @@
         matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter", CALLABLES_FILTER);
         matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter/*", CALLABLES_FILTER);
 
+        matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/", CONTACTABLES);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/filter", CONTACTABLES_FILTER);
+        matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/filter/*",
+                CONTACTABLES_FILTER);
+
         matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
         matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
         matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
@@ -1211,6 +1250,9 @@
 
         matcher.addURI(ContactsContract.AUTHORITY, "display_photo/#", DISPLAY_PHOTO_ID);
         matcher.addURI(ContactsContract.AUTHORITY, "photo_dimensions", PHOTO_DIMENSIONS);
+
+        matcher.addURI(ContactsContract.AUTHORITY, "deleted_contacts", DELETED_CONTACTS);
+        matcher.addURI(ContactsContract.AUTHORITY, "deleted_contacts/#", DELETED_CONTACTS_ID);
     }
 
     private static class DirectoryInfo {
@@ -1354,6 +1396,7 @@
             Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate start");
         }
         super.onCreate();
+        setAppOps(AppOpsManager.OP_READ_CONTACTS, AppOpsManager.OP_WRITE_CONTACTS);
         try {
             return initialize();
         } catch (RuntimeException e) {
@@ -1380,7 +1423,7 @@
         StrictMode.setThreadPolicy(
                 new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
 
-        mFastScrollingIndexCache = new FastScrollingIndexCache(getContext());
+        mFastScrollingIndexCache = FastScrollingIndexCache.getInstance(getContext());
 
         mContactsHelper = getDatabaseHelper(getContext());
         mDbHelper.set(mContactsHelper);
@@ -1441,7 +1484,7 @@
         mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
         mPostalSplitter = new PostalSplitter(mCurrentLocale);
         mCommonNicknameCache = new CommonNicknameCache(mContactsHelper.getReadableDatabase());
-        ContactLocaleUtils.getIntance().setLocale(mCurrentLocale);
+        ContactLocaleUtils.setLocale(mCurrentLocale);
         mContactAggregator = new ContactAggregator(this, mContactsHelper,
                 createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
         mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
@@ -1601,6 +1644,11 @@
                     break;
                 }
             }
+
+            case BACKGROUND_TASK_CLEAN_DELETE_LOG: {
+                final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
+                DeletedContactsTableUtil.deleteOldLogs(db);
+            }
         }
     }
 
@@ -1613,6 +1661,26 @@
         scheduleBackgroundTask(BACKGROUND_TASK_CHANGE_LOCALE);
     }
 
+    private static boolean needsToUpdateLocaleData(SharedPreferences prefs,
+            Locale locale,ContactsDatabaseHelper contactsHelper,
+            ProfileDatabaseHelper profileHelper) {
+        final String providerLocale = prefs.getString(PREF_LOCALE, null);
+
+        // If locale matches that of the provider, and neither DB needs
+        // updating, there's nothing to do. A DB might require updating
+        // as a result of a system upgrade.
+        if (!locale.toString().equals(providerLocale)) {
+            Log.i(TAG, "Locale has changed from " + providerLocale
+                    + " to " + locale.toString());
+            return true;
+        }
+        if (contactsHelper.needsToUpdateLocaleData(locale) ||
+                profileHelper.needsToUpdateLocaleData(locale)) {
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Verifies that the contacts database is properly configured for the current locale.
      * If not, changes the database locale to the current locale using an asynchronous task.
@@ -1627,23 +1695,44 @@
             return;
         }
 
-        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
-        final String providerLocale = prefs.getString(PREF_LOCALE, null);
         final Locale currentLocale = mCurrentLocale;
-        if (currentLocale.toString().equals(providerLocale)) {
+        final SharedPreferences prefs =
+            PreferenceManager.getDefaultSharedPreferences(getContext());
+        if (!needsToUpdateLocaleData(prefs, currentLocale,
+                        mContactsHelper, mProfileHelper)) {
             return;
         }
 
         int providerStatus = mProviderStatus;
         setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE);
-        mContactsHelper.setLocale(this, currentLocale);
-        mProfileHelper.setLocale(this, currentLocale);
+        mContactsHelper.setLocale(currentLocale);
+        mProfileHelper.setLocale(currentLocale);
         mSearchIndexManager.updateIndex(true);
-        prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).apply();
-        invalidateFastScrollingIndexCache();
+        prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).commit();
         setProviderStatus(providerStatus);
     }
 
+    // Static update routine for use by ContactsUpgradeReceiver during startup.
+    // This clears the search index and marks it to be rebuilt, but doesn't
+    // actually rebuild it. That is done later by
+    // BACKGROUND_TASK_UPDATE_SEARCH_INDEX.
+    protected static void updateLocaleOffline(Context context,
+            ContactsDatabaseHelper contactsHelper,
+            ProfileDatabaseHelper profileHelper) {
+        final Locale currentLocale = Locale.getDefault();
+        final SharedPreferences prefs =
+            PreferenceManager.getDefaultSharedPreferences(context);
+        if (!needsToUpdateLocaleData(prefs, currentLocale,
+                        contactsHelper, profileHelper)) {
+            return;
+        }
+
+        contactsHelper.setLocale(currentLocale);
+        profileHelper.setLocale(currentLocale);
+        contactsHelper.rebuildSearchIndex();
+        prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).commit();
+    }
+
     /**
      * Reinitializes the provider for a new locale.
      */
@@ -2218,6 +2307,9 @@
             db.execSQL(mSb.toString());
         }
 
+        final Set<Long> changedRawContacts = mTransactionContext.get().getChangedRawContactIds();
+        ContactsTableUtil.updateContactLastUpdateByRawContactId(db, changedRawContacts);
+
         // Update sync states.
         for (Map.Entry<Long, Object> entry : mTransactionContext.get().getUpdatedSyncStates()) {
             long id = entry.getKey();
@@ -2639,9 +2731,7 @@
         DataRowHandler rowHandler = getDataRowHandler(mimeType);
         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
         id = rowHandler.insert(db, mTransactionContext.get(), rawContactId, mValues);
-        if (!callerIsSyncAdapter) {
-            mTransactionContext.get().markRawContactDirty(rawContactId);
-        }
+        mTransactionContext.get().markRawContactDirtyAndChanged(rawContactId, callerIsSyncAdapter);
         mTransactionContext.get().rawContactUpdated(rawContactId);
         return id;
     }
@@ -2864,9 +2954,8 @@
                 String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE);
                 DataRowHandler rowHandler = getDataRowHandler(mimeType);
                 count += rowHandler.delete(db, mTransactionContext.get(), c);
-                if (!callerIsSyncAdapter) {
-                    mTransactionContext.get().markRawContactDirty(rawContactId);
-                }
+                mTransactionContext.get().markRawContactDirtyAndChanged(rawContactId,
+                        callerIsSyncAdapter);
             }
         } finally {
             c.close();
@@ -2958,7 +3047,8 @@
                     if (c.getLong(1) != 0) {
                         final long rawContactId = c.getLong(0);
                         insertDataGroupMembership(rawContactId, result);
-                        mTransactionContext.get().markRawContactDirty(rawContactId);
+                        mTransactionContext.get().markRawContactDirtyAndChanged(rawContactId,
+                                callerIsSyncAdapter);
                     }
                 }
             } finally {
@@ -3549,7 +3639,9 @@
 
         mProviderStatusUpdateNeeded = true;
 
-        return db.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
+        int result = ContactsTableUtil.deleteContact(db, contactId);
+        scheduleBackgroundTask(BACKGROUND_TASK_CLEAN_DELETE_LOG);
+        return result;
     }
 
     public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
@@ -3572,14 +3664,24 @@
         }
 
         if (callerIsSyncAdapter || rawContactIsLocal(rawContactId)) {
+
+            // When a raw contact is deleted, a sqlite trigger deletes the parent contact.
+            // TODO: all contact deletes was consolidated into ContactTableUtil but this one can't
+            // because it's in a trigger.  Consider removing trigger and replacing with java code.
+            // This has to happen before the raw contact is deleted since it relies on the number
+            // of raw contacts.
+            ContactsTableUtil.deleteContactIfSingleton(db, rawContactId);
+
             db.delete(Tables.PRESENCE,
                     PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
             int count = db.delete(Tables.RAW_CONTACTS,
                     RawContacts._ID + "=" + rawContactId, null);
+
             mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId);
+            mTransactionContext.get().markRawContactChangedOrDeletedOrInserted(rawContactId);
             return count;
         } else {
-            mDbHelper.get().removeContactIfSingleton(rawContactId);
+            ContactsTableUtil.deleteContactIfSingleton(db, rawContactId);
             return markRawContactAsDeleted(db, rawContactId, callerIsSyncAdapter);
         }
     }
@@ -4282,6 +4384,7 @@
                 // and change accounts at the same time.)
                 mTransactionContext.get().rawContactInserted(rawContactId, accountId);
             }
+            mTransactionContext.get().markRawContactChangedOrDeletedOrInserted(rawContactId);
         }
         return count;
     }
@@ -4419,6 +4522,8 @@
                 values, Contacts.TIMES_CONTACTED);
         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
                 values, Contacts.STARRED);
+        mValues.put(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
+                Clock.getInstance().currentTimeMillis());
 
         int rslt = db.update(Tables.CONTACTS, mValues, Contacts._ID + "=?",
                 mSelectionArgs1);
@@ -4563,7 +4668,15 @@
 
         // WARNING: This method can be run in either contacts mode or profile mode.  It is
         // absolutely imperative that no calls be made inside the following try block that can
-        // interact with the contacts DB.  Otherwise it is quite possible for a deadlock to occur.
+        // interact with a specific contacts or profile DB.  Otherwise it is quite possible for a
+        // deadlock to occur.  i.e. always use the current database in mDbHelper and do not access
+        // mContactsHelper or mProfileHelper directly.
+        //
+        // The problem may be a bit more subtle if you also access something that stores the current
+        // db instance in it's constructor.  updateSearchIndexInTransaction relies on the
+        // SearchIndexManager which upon construction, stores the current db. In this case,
+        // SearchIndexManager always contains the contact DB. This is why the
+        // updateSearchIndexInTransaction is protected with !isInProfileMode now.
         try {
             // First, remove stale rows from raw_contacts, groups, and related tables.
 
@@ -4588,8 +4701,9 @@
 
                     // getAccountIdOrNull() really shouldn't return null here, but just in case...
                     if (accountIdOrNull != null) {
+                        final String accountId = Long.toString(accountIdOrNull);
                         final String[] accountIdParams =
-                                new String[] {Long.toString(accountIdOrNull)};
+                                new String[] {accountId};
                         db.execSQL(
                                 "DELETE FROM " + Tables.GROUPS +
                                 " WHERE " + GroupsColumns.ACCOUNT_ID + " = ?",
@@ -4618,6 +4732,63 @@
                                         " FROM " + Tables.RAW_CONTACTS +
                                         " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
                                         accountIdParams);
+
+                        // Delta api is only needed for regular contacts.
+                        if (!inProfileMode()) {
+                            // Contacts are deleted by a trigger on the raw_contacts table.
+                            // But we also need to insert the contact into the delete log.
+                            // This logic is being consolidated into the ContactsTableUtil.
+
+                            // deleteContactIfSingleton() does not work in this case because raw
+                            // contacts will be deleted in a single batch below.  Contacts with
+                            // multiple raw contacts in the same account will be missed.
+
+                            // Find all contacts that do not have raw contacts in other accounts.
+                            // These should be deleted.
+                            Cursor cursor = db.rawQuery(
+                                    "SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
+                                            " FROM " + Tables.RAW_CONTACTS +
+                                            " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?1" +
+                                            " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
+                                            " NOT IN (" +
+                                            "    SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
+                                            "    FROM " + Tables.RAW_CONTACTS +
+                                            "    WHERE " + RawContactsColumns.ACCOUNT_ID + " != ?1"
+                                            + ")", accountIdParams);
+                            try {
+                                while (cursor.moveToNext()) {
+                                    final long contactId = cursor.getLong(0);
+                                    ContactsTableUtil.deleteContact(db, contactId);
+                                }
+                            } finally {
+                                MoreCloseables.closeQuietly(cursor);
+                            }
+
+                            // If the contact was not deleted, it's last updated timestamp needs to
+                            // be refreshed since one of it's raw contacts got removed.
+                            // Find all contacts that will not be deleted (i.e. contacts with
+                            // raw contacts in other accounts)
+                            cursor = db.rawQuery(
+                                    "SELECT DISTINCT " + RawContactsColumns.CONCRETE_CONTACT_ID +
+                                            " FROM " + Tables.RAW_CONTACTS +
+                                            " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?1" +
+                                            " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
+                                            " IN (" +
+                                            "    SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
+                                            "    FROM " + Tables.RAW_CONTACTS +
+                                            "    WHERE " + RawContactsColumns.ACCOUNT_ID + " != ?1"
+                                            + ")", accountIdParams);
+                            try {
+                                while (cursor.moveToNext()) {
+                                    final long contactId = cursor.getLong(0);
+                                    ContactsTableUtil.updateContactLastUpdateByContactId(db,
+                                            contactId);
+                                }
+                            } finally {
+                                MoreCloseables.closeQuietly(cursor);
+                            }
+                        }
+
                         db.execSQL(
                                 "DELETE FROM " + Tables.RAW_CONTACTS +
                                 " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?",
@@ -5466,7 +5637,7 @@
                     final String ftsMatchQuery =
                             searchDisplayName
                             ? SearchIndexManager.getFtsMatchQuery(filterParam,
-                                    FtsQueryBuilder.UNSCOPED_NORMALIZING)
+                                    FtsQueryBuilder.SCOPED_NAME_NORMALIZING)
                             : null;
                     if (!TextUtils.isEmpty(ftsMatchQuery)) {
                         sb.append(Data.RAW_CONTACT_ID + " IN " +
@@ -5475,7 +5646,7 @@
                                 " JOIN " + Tables.RAW_CONTACTS +
                                 " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID
                                         + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" +
-                                " WHERE " + SearchIndexColumns.NAME + " MATCH '");
+                                " WHERE " + Tables.SEARCH_INDEX + " MATCH '");
                         sb.append(ftsMatchQuery);
                         sb.append("')");
                         hasCondition = true;
@@ -5672,6 +5843,108 @@
                 break;
             }
 
+            case CONTACTABLES:
+            case CONTACTABLES_FILTER: {
+                setTablesAndProjectionMapForData(qb, uri, projection, false);
+
+                String filterParam = null;
+
+                final int uriPathSize = uri.getPathSegments().size();
+                if (uriPathSize > 3) {
+                    filterParam = uri.getLastPathSegment();
+                    if (TextUtils.isEmpty(filterParam)) {
+                        filterParam = null;
+                    }
+                }
+
+                // CONTACTABLES_FILTER but no query provided, return an empty cursor
+                if (uriPathSize > 2 && filterParam == null) {
+                    qb.appendWhere(" AND 0");
+                    break;
+                }
+
+                if (uri.getBooleanQueryParameter(Contactables.VISIBLE_CONTACTS_ONLY, false)) {
+                    qb.appendWhere(" AND " + Data.CONTACT_ID + " in " +
+                            Tables.DEFAULT_DIRECTORY);
+                    }
+
+                final StringBuilder sb = new StringBuilder();
+
+                // we only want data items that are either email addresses or phone numbers
+                sb.append(" AND (");
+                sb.append(DataColumns.MIMETYPE_ID + " IN (");
+                sb.append(mDbHelper.get().getMimeTypeIdForEmail());
+                sb.append(",");
+                sb.append(mDbHelper.get().getMimeTypeIdForPhone());
+                sb.append("))");
+
+                // Rest of the query is only relevant if we are handling CONTACTABLES_FILTER
+                if (uriPathSize < 3) {
+                    qb.appendWhere(sb);
+                    break;
+                }
+
+                // but we want all the email addresses and phone numbers that belong to
+                // all contacts that have any data items (or name) that match the query
+                sb.append(" AND ");
+                sb.append("(" + Data.CONTACT_ID + " IN (");
+
+                // All contacts where the email address data1 column matches the query
+                sb.append(
+                        "SELECT " + RawContacts.CONTACT_ID +
+                        " FROM " + Tables.DATA + " JOIN " + Tables.RAW_CONTACTS +
+                        " ON " + Tables.DATA + "." + Data.RAW_CONTACT_ID + "=" +
+                        Tables.RAW_CONTACTS + "." + RawContacts._ID +
+                        " WHERE (" + DataColumns.MIMETYPE_ID + "=");
+                sb.append(mDbHelper.get().getMimeTypeIdForEmail());
+
+                sb.append(" AND " + Data.DATA1 + " LIKE ");
+                DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
+                sb.append(")");
+
+                // All contacts where the phone number matches the query (determined by checking
+                // Tables.PHONE_LOOKUP
+                final String number = PhoneNumberUtils.normalizeNumber(filterParam);
+                if (!TextUtils.isEmpty(number)) {
+                    sb.append("UNION SELECT DISTINCT " + RawContacts.CONTACT_ID +
+                            " FROM " + Tables.PHONE_LOOKUP + " JOIN " + Tables.RAW_CONTACTS +
+                            " ON (" + Tables.PHONE_LOOKUP + "." +
+                            PhoneLookupColumns.RAW_CONTACT_ID + "=" +
+                            Tables.RAW_CONTACTS + "." + RawContacts._ID + ")" +
+                            " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
+                    sb.append(number);
+                    sb.append("%'");
+                }
+
+                // All contacts where the name matches the query (determined by checking
+                // Tables.SEARCH_INDEX
+                sb.append(
+                        " UNION SELECT " + Data.CONTACT_ID +
+                        " FROM " + Tables.DATA + " JOIN " + Tables.RAW_CONTACTS +
+                        " ON " + Tables.DATA + "." + Data.RAW_CONTACT_ID + "=" +
+                        Tables.RAW_CONTACTS + "." + RawContacts._ID +
+
+                        " WHERE " + Data.RAW_CONTACT_ID + " IN " +
+
+                        "(SELECT " + RawContactsColumns.CONCRETE_ID +
+                        " FROM " + Tables.SEARCH_INDEX +
+                        " JOIN " + Tables.RAW_CONTACTS +
+                        " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID
+                        + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" +
+
+                        " WHERE " + SearchIndexColumns.NAME + " MATCH '");
+
+                final String ftsMatchQuery = SearchIndexManager.getFtsMatchQuery(
+                        filterParam, FtsQueryBuilder.UNSCOPED_NORMALIZING);
+                sb.append(ftsMatchQuery);
+                sb.append("')");
+
+                sb.append("))");
+                qb.appendWhere(sb);
+
+                break;
+            }
+
             case POSTALS: {
                 setTablesAndProjectionMapForData(qb, uri, projection, false);
                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
@@ -5752,7 +6025,13 @@
 
             case DATA:
             case PROFILE_DATA: {
-                setTablesAndProjectionMapForData(qb, uri, projection, false);
+                final String usageType = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
+                final int typeInt = getDataUsageFeedbackType(usageType, USAGE_TYPE_ALL);
+                setTablesAndProjectionMapForData(qb, uri, projection, false, typeInt);
+                if (uri.getBooleanQueryParameter(Data.VISIBLE_CONTACTS_ONLY, false)) {
+                    qb.appendWhere(" AND " + Data.CONTACT_ID + " in " +
+                            Tables.DEFAULT_DIRECTORY);
+                }
                 break;
             }
 
@@ -5952,7 +6231,7 @@
 
             case SEARCH_SUGGESTIONS: {
                 return mGlobalSearchSupport.handleSearchSuggestionsQuery(
-                        db, uri, projection, limit);
+                        db, uri, projection, limit, cancellationSignal);
             }
 
             case SEARCH_SHORTCUT: {
@@ -5960,7 +6239,7 @@
                 String filter = getQueryParameter(
                         uri, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
                 return mGlobalSearchSupport.handleSearchShortcutRefresh(
-                        db, projection, lookupKey, filter);
+                        db, projection, lookupKey, filter, cancellationSignal);
             }
 
             case RAW_CONTACT_ENTITIES:
@@ -6002,6 +6281,21 @@
                 return completeName(uri, projection);
             }
 
+            case DELETED_CONTACTS: {
+                qb.setTables(Tables.DELETED_CONTACTS);
+                qb.setProjectionMap(sDeletedContactsProjectionMap);
+                break;
+            }
+
+            case DELETED_CONTACTS_ID: {
+                String id = uri.getLastPathSegment();
+                qb.setTables(Tables.DELETED_CONTACTS);
+                qb.setProjectionMap(sDeletedContactsProjectionMap);
+                qb.appendWhere(ContactsContract.DeletedContacts.CONTACT_ID + "=?");
+                selectionArgs = insertSelectionArg(selectionArgs, id);
+                break;
+            }
+
             default:
                 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
                         sortOrder, limit);
@@ -6009,8 +6303,10 @@
 
         qb.setStrict(true);
 
+        // Auto-rewrite SORT_KEY_{PRIMARY, ALTERNATIVE} sort orders.
+        String localizedSortOrder = getLocalizedSortOrder(sortOrder);
         Cursor cursor =
-                query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy,
+                query(db, qb, projection, selection, selectionArgs, localizedSortOrder, groupBy,
                 having, limit, cancellationSignal);
 
         if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) {
@@ -6026,6 +6322,34 @@
     }
 
 
+    // Rewrites query sort orders using SORT_KEY_{PRIMARY, ALTERNATIVE}
+    // to use PHONEBOOK_BUCKET_{PRIMARY, ALTERNATIVE} as primary key; all
+    // other sort orders are returned unchanged. Preserves ordering
+    // (eg 'DESC') if present.
+    protected static String getLocalizedSortOrder(String sortOrder) {
+        String localizedSortOrder = sortOrder;
+        if (sortOrder != null) {
+            String sortKey;
+            String sortOrderSuffix = "";
+            int spaceIndex = sortOrder.indexOf(' ');
+            if (spaceIndex != -1) {
+                sortKey = sortOrder.substring(0, spaceIndex);
+                sortOrderSuffix = sortOrder.substring(spaceIndex);
+            } else {
+                sortKey = sortOrder;
+            }
+            if (TextUtils.equals(sortKey, Contacts.SORT_KEY_PRIMARY)) {
+                localizedSortOrder = ContactsColumns.PHONEBOOK_BUCKET_PRIMARY
+                    + sortOrderSuffix + ", " + sortOrder;
+            } else if (TextUtils.equals(sortKey, Contacts.SORT_KEY_ALTERNATIVE)) {
+                localizedSortOrder = ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE
+                    + sortOrderSuffix + ", " + sortOrder;
+            }
+        }
+        return localizedSortOrder;
+    }
+
+
     private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
             String selection, String[] selectionArgs, String sortOrder, String groupBy,
             String having, String limit, CancellationSignal cancellationSignal) {
@@ -6120,7 +6444,7 @@
                 final long start = System.currentTimeMillis();
 
                 b = getFastScrollingIndexExtras(queryUri, db, qb, selection, selectionArgs,
-                        sortOrder, countExpression, cancellationSignal, getLocale());
+                        sortOrder, countExpression, cancellationSignal);
 
                 final long end = System.currentTimeMillis();
                 final int time = (int) (end - start);
@@ -6136,32 +6460,33 @@
     }
 
     private static final class AddressBookIndexQuery {
-        public static final String LETTER = "letter";
-        public static final String TITLE = "title";
+        public static final String NAME = "name";
+        public static final String BUCKET = "bucket";
+        public static final String LABEL = "label";
         public static final String COUNT = "count";
 
         public static final String[] COLUMNS = new String[] {
-                LETTER, TITLE, COUNT
+            NAME, BUCKET, LABEL, COUNT
         };
 
-        public static final int COLUMN_LETTER = 0;
-        public static final int COLUMN_TITLE = 1;
-        public static final int COLUMN_COUNT = 2;
+        public static final int COLUMN_NAME = 0;
+        public static final int COLUMN_BUCKET = 1;
+        public static final int COLUMN_LABEL = 2;
+        public static final int COLUMN_COUNT = 3;
 
-        // The first letter of the sort key column is what is used for the index headings.
-        public static final String SECTION_HEADING = "SUBSTR(%1$s,1,1)";
-
-        public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME;
+        public static final String GROUP_BY = BUCKET + ", " + LABEL;
+        public static final String ORDER_BY =
+            BUCKET + ", " +  NAME + " COLLATE " + PHONEBOOK_COLLATOR_NAME;
     }
 
     /**
-     * Computes counts by the address book index titles and returns it as {@link Bundle} which
+     * Computes counts by the address book index labels and returns it as {@link Bundle} which
      * will be appended to a {@link Cursor} as extras.
      */
     private static Bundle getFastScrollingIndexExtras(final Uri queryUri, final SQLiteDatabase db,
             final SQLiteQueryBuilder qb, final String selection, final String[] selectionArgs,
             final String sortOrder, String countExpression,
-            final CancellationSignal cancellationSignal, final Locale currentLocale) {
+            final CancellationSignal cancellationSignal) {
         String sortKey;
 
         // The sort order suffix could be something like "DESC".
@@ -6180,72 +6505,54 @@
             sortKey = Contacts.SORT_KEY_PRIMARY;
         }
 
+        String bucketKey;
+        String labelKey;
+        if (TextUtils.equals(sortKey, Contacts.SORT_KEY_PRIMARY)) {
+            bucketKey = ContactsColumns.PHONEBOOK_BUCKET_PRIMARY;
+            labelKey = ContactsColumns.PHONEBOOK_LABEL_PRIMARY;
+        } else if (TextUtils.equals(sortKey, Contacts.SORT_KEY_ALTERNATIVE)) {
+            bucketKey = ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE;
+            labelKey = ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE;
+        } else {
+            return null;
+        }
+
         HashMap<String, String> projectionMap = Maps.newHashMap();
-        String sectionHeading = String.format(Locale.US, AddressBookIndexQuery.SECTION_HEADING,
-                sortKey);
-        projectionMap.put(AddressBookIndexQuery.LETTER,
-                sectionHeading + " AS " + AddressBookIndexQuery.LETTER);
+        projectionMap.put(AddressBookIndexQuery.NAME,
+                sortKey + " AS " + AddressBookIndexQuery.NAME);
+        projectionMap.put(AddressBookIndexQuery.BUCKET,
+                bucketKey + " AS " + AddressBookIndexQuery.BUCKET);
+        projectionMap.put(AddressBookIndexQuery.LABEL,
+                labelKey + " AS " + AddressBookIndexQuery.LABEL);
 
         // If "what to count" is not specified, we just count all records.
         if (TextUtils.isEmpty(countExpression)) {
             countExpression = "*";
         }
 
-        /**
-         * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3,
-         * to map the first letter of the sort key to a character that is traditionally
-         * used in phonebooks to represent that letter.  For example, in Korean it will
-         * be the first consonant in the letter; for Japanese it will be Hiragana rather
-         * than Katakana.
-         */
-        projectionMap.put(AddressBookIndexQuery.TITLE,
-                "GET_PHONEBOOK_INDEX(" + sectionHeading + ",'" + currentLocale.toString() + "')"
-                        + " AS " + AddressBookIndexQuery.TITLE);
         projectionMap.put(AddressBookIndexQuery.COUNT,
                 "COUNT(" + countExpression + ") AS " + AddressBookIndexQuery.COUNT);
         qb.setProjectionMap(projectionMap);
+        String orderBy = AddressBookIndexQuery.BUCKET + sortOrderSuffix
+            + ", " + AddressBookIndexQuery.NAME + " COLLATE "
+            + PHONEBOOK_COLLATOR_NAME + sortOrderSuffix;
 
         Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs,
-                AddressBookIndexQuery.ORDER_BY, null /* having */,
-                AddressBookIndexQuery.ORDER_BY + sortOrderSuffix,
-                null, cancellationSignal);
+                AddressBookIndexQuery.GROUP_BY, null /* having */,
+                orderBy, null, cancellationSignal);
 
         try {
-            int groupCount = indexCursor.getCount();
-            String titles[] = new String[groupCount];
-            int counts[] = new int[groupCount];
-            int indexCount = 0;
-            String currentTitle = null;
+            int numLabels = indexCursor.getCount();
+            String labels[] = new String[numLabels];
+            int counts[] = new int[numLabels];
 
-            // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up
-            // with multiple entries for the same title.  The following code
-            // collapses those duplicates.
-            for (int i = 0; i < groupCount; i++) {
+            for (int i = 0; i < numLabels; i++) {
                 indexCursor.moveToNext();
-                String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE);
-                if (title == null) {
-                    title = "";
-                }
-                int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
-                if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) {
-                    titles[indexCount] = currentTitle = title;
-                    counts[indexCount] = count;
-                    indexCount++;
-                } else {
-                    counts[indexCount - 1] += count;
-                }
+                labels[i] = indexCursor.getString(AddressBookIndexQuery.COLUMN_LABEL);
+                counts[i] = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
             }
 
-            if (indexCount < groupCount) {
-                String[] newTitles = new String[indexCount];
-                System.arraycopy(titles, 0, newTitles, 0, indexCount);
-                titles = newTitles;
-
-                int[] newCounts = new int[indexCount];
-                System.arraycopy(counts, 0, newCounts, 0, indexCount);
-                counts = newCounts;
-            }
-            return FastScrollingIndexCache.buildExtraBundle(titles, counts);
+            return FastScrollingIndexCache.buildExtraBundle(labels, counts);
         } finally {
             indexCursor.close();
         }
@@ -6831,9 +7138,8 @@
         appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
         appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
 
-        if (usageType != null) {
-            appendDataUsageStatJoin(sb, usageType, DataColumns.CONCRETE_ID);
-        }
+        appendDataUsageStatJoin(sb, usageType == null ? USAGE_TYPE_ALL : usageType,
+                DataColumns.CONCRETE_ID);
 
         qb.setTables(sb.toString());
 
@@ -6930,9 +7236,29 @@
     }
 
     private void appendDataUsageStatJoin(StringBuilder sb, int usageType, String dataIdColumn) {
-        sb.append(" LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT +
-                " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=" + dataIdColumn +
-                " AND " + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" + usageType + ")");
+        if (usageType != USAGE_TYPE_ALL) {
+            sb.append(" LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT +
+                    " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=");
+            sb.append(dataIdColumn);
+            sb.append(" AND " + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=");
+            sb.append(usageType);
+            sb.append(")");
+        } else {
+            sb.append(
+                    " LEFT OUTER JOIN " +
+                        "(SELECT " +
+                            DataUsageStatColumns.CONCRETE_DATA_ID + ", " +
+                            "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED +
+                                ") as " + DataUsageStatColumns.TIMES_USED + ", " +
+                            "MAX(" + DataUsageStatColumns.CONCRETE_LAST_TIME_USED +
+                                ") as " + DataUsageStatColumns.LAST_TIME_USED +
+                        " FROM " + Tables.DATA_USAGE_STAT + " GROUP BY " +
+                            DataUsageStatColumns.DATA_ID + ") as " + Tables.DATA_USAGE_STAT
+                    );
+            sb.append(" ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=");
+            sb.append(dataIdColumn);
+            sb.append(")");
+        }
     }
 
     private void appendContactPresenceJoin(StringBuilder sb, String[] projection,
@@ -7158,6 +7484,22 @@
 
     public AssetFileDescriptor openAssetFileLocal(Uri uri, String mode)
             throws FileNotFoundException {
+        // In some cases to implement this, we will need to do further queries
+        // on the content provider.  We have already done the permission check for
+        // access to the uri given here, so we don't need to do further checks on
+        // the queries we will do to populate it.  Also this makes sure that when
+        // we go through any app ops checks for those queries that the calling uid
+        // and package names match at that point.
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            return openAssetFileInner(uri, mode);
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private AssetFileDescriptor openAssetFileInner(Uri uri, String mode)
+            throws FileNotFoundException {
 
         final boolean writing = mode.contains("w");
 
@@ -8283,7 +8625,16 @@
      * @return a boolean indicating if the query is one word or not
      */
     private boolean isSingleWordQuery(String query) {
-        return query.split(QUERY_TOKENIZER_REGEX).length == 1;
+        // Split can remove empty trailing tokens but cannot remove starting empty tokens so we
+        // have to loop.
+        String[] tokens = query.split(QUERY_TOKENIZER_REGEX, 0);
+        int count = 0;
+        for (String token : tokens) {
+            if (!"".equals(token)) {
+                count++;
+            }
+        }
+        return count == 1;
     }
 
     /**
diff --git a/src/com/android/providers/contacts/ContactsUpgradeReceiver.java b/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
index e259ffe..ba8acb8 100644
--- a/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
+++ b/src/com/android/providers/contacts/ContactsUpgradeReceiver.java
@@ -26,6 +26,8 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import libcore.icu.ICU;
+
 /**
  * This will be launched during system boot, after the core system has
  * been brought up but before any non-persistent processes have been
@@ -39,6 +41,7 @@
 public class ContactsUpgradeReceiver extends BroadcastReceiver {
     static final String TAG = "ContactsUpgradeReceiver";
     static final String PREF_DB_VERSION = "db_version";
+    static final String PREF_ICU_VERSION = "icu_version";
 
     @Override
     public void onReceive(Context context, Intent intent) {
@@ -50,33 +53,37 @@
 
             // Lookup the last known database version
             SharedPreferences prefs = context.getSharedPreferences(TAG, Context.MODE_PRIVATE);
-            int prefVersion = prefs.getInt(PREF_DB_VERSION, 0);
+            int prefDbVersion = prefs.getInt(PREF_DB_VERSION, 0);
+            final String curIcuVersion = ICU.getIcuVersion();
+            final String prefIcuVersion = prefs.getString(PREF_ICU_VERSION, "");
 
             // If the version is old go ahead and attempt to create or upgrade the database.
-            if (prefVersion != ContactsDatabaseHelper.DATABASE_VERSION) {
+            if (prefDbVersion != ContactsDatabaseHelper.DATABASE_VERSION ||
+                    !prefIcuVersion.equals(curIcuVersion)) {
                 // Store the current version so this receiver isn't run again until the database
                 // version number changes. This is intentionally done even before the upgrade path
                 // is attempted to be conservative. If the upgrade fails for some reason and we
                 // crash and burn we don't want to get into a loop doing so.
-                prefs.edit().putInt(
-                    PREF_DB_VERSION, ContactsDatabaseHelper.DATABASE_VERSION).commit();
+                SharedPreferences.Editor editor = prefs.edit();
+                editor.putInt(PREF_DB_VERSION, ContactsDatabaseHelper.DATABASE_VERSION);
+                editor.putString(PREF_ICU_VERSION, curIcuVersion);
+                editor.commit();
 
                 // Ask for a reference to the database to force the helper to either
                 // create the database or open it up, performing any necessary upgrades
                 // in the process.
                 ContactsDatabaseHelper helper = ContactsDatabaseHelper.getInstance(context);
-                if (context.getDatabasePath(helper.getDatabaseName()).exists()) {
-                    Log.i(TAG, "Creating or opening contacts database");
-                    try {
-                        ActivityManagerNative.getDefault().showBootMessage(
-                                context.getText(R.string.upgrade_msg), true);
-                    } catch (RemoteException e) {
-                    }
-                    helper.getWritableDatabase();
+                ProfileDatabaseHelper profileHelper = ProfileDatabaseHelper.getInstance(context);
+                Log.i(TAG, "Creating or opening contacts database");
+                try {
+                    ActivityManagerNative.getDefault().showBootMessage(
+                            context.getText(R.string.upgrade_msg), true);
+                } catch (RemoteException e) {
                 }
 
-                ProfileDatabaseHelper profileHelper = ProfileDatabaseHelper.getInstance(context);
+                helper.getWritableDatabase();
                 profileHelper.getWritableDatabase();
+                ContactsProvider2.updateLocaleOffline(context, helper, profileHelper);
 
                 // Log the total time taken for the receiver to perform the operation
                 EventLogTags.writeContactsUpgradeReceiver(System.currentTimeMillis() - startTime);
diff --git a/src/com/android/providers/contacts/DataRowHandler.java b/src/com/android/providers/contacts/DataRowHandler.java
index 0366532..a20da06 100644
--- a/src/com/android/providers/contacts/DataRowHandler.java
+++ b/src/com/android/providers/contacts/DataRowHandler.java
@@ -162,9 +162,7 @@
             txContext.invalidateSearchIndexForRawContact(rawContactId);
         }
 
-        if (!callerIsSyncAdapter) {
-            txContext.markRawContactDirty(rawContactId);
-        }
+        txContext.markRawContactDirtyAndChanged(rawContactId, callerIsSyncAdapter);
 
         return true;
     }
diff --git a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
index 01ee1ba..8781d2f 100644
--- a/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
+++ b/src/com/android/providers/contacts/DataRowHandlerForStructuredName.java
@@ -21,6 +21,7 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhoneticNameStyle;
 import android.text.TextUtils;
 
 import com.android.providers.contacts.SearchIndexManager.IndexBuilder;
@@ -141,6 +142,7 @@
             name.fromValues(augmented);
             // As the name could be changed, let's guess the name style again.
             name.fullNameStyle = FullNameStyle.UNDEFINED;
+            name.phoneticNameStyle = PhoneticNameStyle.UNDEFINED;
             mSplitter.guessNameStyle(name);
             int unadjustedFullNameStyle = name.fullNameStyle;
             name.fullNameStyle = mSplitter.getAdjustedFullNameStyle(name.fullNameStyle);
@@ -155,8 +157,11 @@
                         mSplitter.guessFullNameStyle(unstruct));
             }
             if (!update.containsKey(StructuredName.PHONETIC_NAME_STYLE)) {
-                update.put(StructuredName.PHONETIC_NAME_STYLE,
-                        mSplitter.guessPhoneticNameStyle(unstruct));
+                NameSplitter.Name name = new NameSplitter.Name();
+                name.fromValues(update);
+                name.phoneticNameStyle = PhoneticNameStyle.UNDEFINED;
+                mSplitter.guessNameStyle(name);
+                update.put(StructuredName.PHONETIC_NAME_STYLE, name.phoneticNameStyle);
             }
         }
     }
@@ -219,7 +224,14 @@
                 builder.appendName(phoneticGiven);
                 mSb.append(phoneticGiven);
             }
-            builder.appendName(mSb.toString().trim());
+            final String phoneticName = mSb.toString().trim();
+            int phoneticNameStyle = builder.getInt(StructuredName.PHONETIC_NAME_STYLE);
+            if (phoneticNameStyle == PhoneticNameStyle.UNDEFINED) {
+                phoneticNameStyle = mSplitter.guessPhoneticNameStyle(phoneticName);
+            }
+            builder.appendName(phoneticName);
+            mNameLookupBuilder.appendNameShorthandLookup(builder, phoneticName,
+                    phoneticNameStyle);
         }
     }
 }
diff --git a/src/com/android/providers/contacts/FastScrollingIndexCache.java b/src/com/android/providers/contacts/FastScrollingIndexCache.java
index f07a855..e535225 100644
--- a/src/com/android/providers/contacts/FastScrollingIndexCache.java
+++ b/src/com/android/providers/contacts/FastScrollingIndexCache.java
@@ -85,16 +85,24 @@
      */
     private final Map<String, String> mCache = Maps.newHashMap();
 
-    public FastScrollingIndexCache(Context context) {
-        this(PreferenceManager.getDefaultSharedPreferences(context));
+    private static FastScrollingIndexCache sSingleton;
 
-        // At this point, the SharedPreferences might just have been generated and may still be
-        // loading from the file, in which case loading from the preferences would be blocked.
-        // To avoid that, we load lazily.
+    public static FastScrollingIndexCache getInstance(Context context) {
+        if (sSingleton == null) {
+            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+            sSingleton = new FastScrollingIndexCache(prefs);
+        }
+        return sSingleton;
     }
 
     @VisibleForTesting
-    FastScrollingIndexCache(SharedPreferences prefs) {
+    static synchronized FastScrollingIndexCache getInstanceForTest(
+            SharedPreferences prefs) {
+        sSingleton = new FastScrollingIndexCache(prefs);
+        return sSingleton;
+    }
+
+    private FastScrollingIndexCache(SharedPreferences prefs) {
         mPrefs = prefs;
     }
 
@@ -238,7 +246,7 @@
 
     public void invalidate() {
         synchronized (mCache) {
-            mPrefs.edit().remove(PREFERENCE_KEY).apply();
+            mPrefs.edit().remove(PREFERENCE_KEY).commit();
             mCache.clear();
             mPreferenceLoaded = true;
 
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index d4cddee..0febf56 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -22,6 +22,7 @@
 import android.database.MatrixCursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
+import android.os.CancellationSignal;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -178,8 +179,8 @@
         }
     }
 
-    public Cursor handleSearchSuggestionsQuery(
-            SQLiteDatabase db, Uri uri, String[] projection, String limit) {
+    public Cursor handleSearchSuggestionsQuery(SQLiteDatabase db, Uri uri, String[] projection,
+            String limit, CancellationSignal cancellationSignal) {
         final MatrixCursor cursor = new MatrixCursor(
                 projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection);
 
@@ -189,7 +190,7 @@
             String selection = null;
             String searchClause = uri.getLastPathSegment();
             addSearchSuggestionsBasedOnFilter(
-                    cursor, db, projection, selection, searchClause, limit);
+                    cursor, db, projection, selection, searchClause, limit, cancellationSignal);
         }
 
         return cursor;
@@ -206,7 +207,7 @@
      * instead of the lookup key.
      */
     public Cursor handleSearchShortcutRefresh(SQLiteDatabase db, String[] projection,
-            String lookupKey, String filter) {
+            String lookupKey, String filter, CancellationSignal cancellationSignal) {
         long contactId;
         try {
             contactId = mContactsProvider.lookupContactIdByLookupKey(db, lookupKey);
@@ -216,11 +217,13 @@
         MatrixCursor cursor = new MatrixCursor(
                 projection == null ? SEARCH_SUGGESTIONS_COLUMNS : projection);
         return addSearchSuggestionsBasedOnFilter(cursor,
-                db, projection, ContactsColumns.CONCRETE_ID + "=" + contactId, filter, null);
+                db, projection, ContactsColumns.CONCRETE_ID + "=" + contactId, filter, null,
+                cancellationSignal);
     }
 
     private Cursor addSearchSuggestionsBasedOnFilter(MatrixCursor cursor, SQLiteDatabase db,
-            String[] projection, String selection, String filter, String limit) {
+            String[] projection, String selection, String filter, String limit,
+            CancellationSignal cancellationSignal) {
         StringBuilder sb = new StringBuilder();
         final boolean haveFilter = !TextUtils.isEmpty(filter);
         sb.append("SELECT "
@@ -247,7 +250,7 @@
         if (limit != null) {
             sb.append(" LIMIT " + limit);
         }
-        Cursor c = db.rawQuery(sb.toString(), null);
+        Cursor c = db.rawQuery(sb.toString(), null, cancellationSignal);
         SearchSuggestion suggestion = new SearchSuggestion();
         suggestion.filter = filter;
         try {
diff --git a/src/com/android/providers/contacts/HanziToPinyin.java b/src/com/android/providers/contacts/HanziToPinyin.java
index 2b659f4..0c35e21 100644
--- a/src/com/android/providers/contacts/HanziToPinyin.java
+++ b/src/com/android/providers/contacts/HanziToPinyin.java
@@ -19,332 +19,23 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Locale;
 
+import libcore.icu.Transliterator;
+
 /**
- * An object to convert Chinese character to its corresponding pinyin string. For characters with
- * multiple possible pinyin string, only one is selected according to collator. Polyphone is not
- * supported in this implementation. This class is implemented to achieve the best runtime
- * performance and minimum runtime resources with tolerable sacrifice of accuracy. This
- * implementation highly depends on zh_CN ICU collation data and must be always synchronized with
- * ICU.
- *
- * Currently this file is aligned to zh.txt in ICU 4.6
+ * An object to convert Chinese character to its corresponding pinyin string.
+ * For characters with multiple possible pinyin string, only one is selected
+ * according to ICU Transliterator class. Polyphone is not supported in this
+ * implementation.
  */
 public class HanziToPinyin {
     private static final String TAG = "HanziToPinyin";
 
-    // Turn on this flag when we want to check internal data structure.
-    private static final boolean DEBUG = false;
-
-    /**
-     * Unihans array.
-     *
-     * Each unihans is the first one within same pinyin when collator is zh_CN.
-     */
-    public static final char[] UNIHANS = {
-            '\u963f', '\u54ce', '\u5b89', '\u80ae', '\u51f9', '\u516b',
-            '\u6300', '\u6273', '\u90a6', '\u52f9', '\u9642', '\u5954',
-            '\u4f3b', '\u5c44', '\u8fb9', '\u706c', '\u618b', '\u6c43',
-            '\u51ab', '\u7676', '\u5cec', '\u5693', '\u5072', '\u53c2',
-            '\u4ed3', '\u64a1', '\u518a', '\u5d7e', '\u66fd', '\u66fe',
-            '\u5c64', '\u53c9', '\u8286', '\u8fbf', '\u4f25', '\u6284',
-            '\u8f66', '\u62bb', '\u6c88', '\u6c89', '\u9637', '\u5403',
-            '\u5145', '\u62bd', '\u51fa', '\u6b3b', '\u63e3', '\u5ddb',
-            '\u5205', '\u5439', '\u65fe', '\u9034', '\u5472', '\u5306',
-            '\u51d1', '\u7c97', '\u6c46', '\u5d14', '\u90a8', '\u6413',
-            '\u5491', '\u5446', '\u4e39', '\u5f53', '\u5200', '\u561a',
-            '\u6265', '\u706f', '\u6c10', '\u55f2', '\u7538', '\u5201',
-            '\u7239', '\u4e01', '\u4e1f', '\u4e1c', '\u543a', '\u53be',
-            '\u8011', '\u8968', '\u5428', '\u591a', '\u59b8', '\u8bf6',
-            '\u5940', '\u97a5', '\u513f', '\u53d1', '\u5e06', '\u531a',
-            '\u98de', '\u5206', '\u4e30', '\u8985', '\u4ecf', '\u7d11',
-            '\u4f15', '\u65ee', '\u4f85', '\u7518', '\u5188', '\u768b',
-            '\u6208', '\u7ed9', '\u6839', '\u522f', '\u5de5', '\u52fe',
-            '\u4f30', '\u74dc', '\u4e56', '\u5173', '\u5149', '\u5f52',
-            '\u4e28', '\u5459', '\u54c8', '\u548d', '\u4f44', '\u592f',
-            '\u8320', '\u8bc3', '\u9ed2', '\u62eb', '\u4ea8', '\u5677',
-            '\u53ff', '\u9f41', '\u4e6f', '\u82b1', '\u6000', '\u72bf',
-            '\u5ddf', '\u7070', '\u660f', '\u5419', '\u4e0c', '\u52a0',
-            '\u620b', '\u6c5f', '\u827d', '\u9636', '\u5dfe', '\u5755',
-            '\u5182', '\u4e29', '\u51e5', '\u59e2', '\u5658', '\u519b',
-            '\u5494', '\u5f00', '\u520a', '\u5ffc', '\u5c3b', '\u533c',
-            '\u808e', '\u52a5', '\u7a7a', '\u62a0', '\u625d', '\u5938',
-            '\u84af', '\u5bbd', '\u5321', '\u4e8f', '\u5764', '\u6269',
-            '\u5783', '\u6765', '\u5170', '\u5577', '\u635e', '\u808b',
-            '\u52d2', '\u5d1a', '\u5215', '\u4fe9', '\u5941', '\u826f',
-            '\u64a9', '\u5217', '\u62ce', '\u5222', '\u6e9c', '\u56d6',
-            '\u9f99', '\u779c', '\u565c', '\u5a08', '\u7567', '\u62a1',
-            '\u7f57', '\u5463', '\u5988', '\u57cb', '\u5ada', '\u7264',
-            '\u732b', '\u4e48', '\u5445', '\u95e8', '\u753f', '\u54aa',
-            '\u5b80', '\u55b5', '\u4e5c', '\u6c11', '\u540d', '\u8c2c',
-            '\u6478', '\u54de', '\u6bea', '\u55ef', '\u62cf', '\u8149',
-            '\u56e1', '\u56d4', '\u5b6c', '\u7592', '\u5a1e', '\u6041',
-            '\u80fd', '\u59ae', '\u62c8', '\u5b22', '\u9e1f', '\u634f',
-            '\u56dc', '\u5b81', '\u599e', '\u519c', '\u7fba', '\u5974',
-            '\u597b', '\u759f', '\u9ec1', '\u90cd', '\u5594', '\u8bb4',
-            '\u5991', '\u62cd', '\u7705', '\u4e53', '\u629b', '\u5478',
-            '\u55b7', '\u5309', '\u4e15', '\u56e8', '\u527d', '\u6c15',
-            '\u59d8', '\u4e52', '\u948b', '\u5256', '\u4ec6', '\u4e03',
-            '\u6390', '\u5343', '\u545b', '\u6084', '\u767f', '\u4eb2',
-            '\u72c5', '\u828e', '\u4e18', '\u533a', '\u5cd1', '\u7f3a',
-            '\u590b', '\u5465', '\u7a63', '\u5a06', '\u60f9', '\u4eba',
-            '\u6254', '\u65e5', '\u8338', '\u53b9', '\u909a', '\u633c',
-            '\u5827', '\u5a51', '\u77a4', '\u637c', '\u4ee8', '\u6be2',
-            '\u4e09', '\u6852', '\u63bb', '\u95aa', '\u68ee', '\u50e7',
-            '\u6740', '\u7b5b', '\u5c71', '\u4f24', '\u5f30', '\u5962',
-            '\u7533', '\u8398', '\u6552', '\u5347', '\u5c38', '\u53ce',
-            '\u4e66', '\u5237', '\u8870', '\u95e9', '\u53cc', '\u8c01',
-            '\u542e', '\u8bf4', '\u53b6', '\u5fea', '\u635c', '\u82cf',
-            '\u72fb', '\u590a', '\u5b59', '\u5506', '\u4ed6', '\u56fc',
-            '\u574d', '\u6c64', '\u5932', '\u5fd1', '\u71a5', '\u5254',
-            '\u5929', '\u65eb', '\u5e16', '\u5385', '\u56f2', '\u5077',
-            '\u51f8', '\u6e4d', '\u63a8', '\u541e', '\u4e47', '\u7a75',
-            '\u6b6a', '\u5f2f', '\u5c23', '\u5371', '\u6637', '\u7fc1',
-            '\u631d', '\u4e4c', '\u5915', '\u8672', '\u4eda', '\u4e61',
-            '\u7071', '\u4e9b', '\u5fc3', '\u661f', '\u51f6', '\u4f11',
-            '\u5401', '\u5405', '\u524a', '\u5743', '\u4e2b', '\u6079',
-            '\u592e', '\u5e7a', '\u503b', '\u4e00', '\u56d9', '\u5e94',
-            '\u54df', '\u4f63', '\u4f18', '\u625c', '\u56e6', '\u66f0',
-            '\u6655', '\u7b60', '\u7b7c', '\u5e00', '\u707d', '\u5142',
-            '\u5328', '\u50ae', '\u5219', '\u8d3c', '\u600e', '\u5897',
-            '\u624e', '\u635a', '\u6cbe', '\u5f20', '\u957f', '\u9577',
-            '\u4f4b', '\u8707', '\u8d1e', '\u4e89', '\u4e4b', '\u5cd9',
-            '\u5ea2', '\u4e2d', '\u5dde', '\u6731', '\u6293', '\u62fd',
-            '\u4e13', '\u5986', '\u96b9', '\u5b92', '\u5353', '\u4e72',
-            '\u5b97', '\u90b9', '\u79df', '\u94bb', '\u539c', '\u5c0a',
-            '\u6628', '\u5159', '\u9fc3', '\u9fc4', };
-
-    /**
-     * Pinyin array.
-     *
-     * Each pinyin is corresponding to unihans of same
-     * offset in the unihans array.
-     */
-    public static final byte[][] PINYINS = {
-            { 65,   0,   0,   0,   0,   0}, { 65,  73,   0,   0,   0,   0},
-            { 65,  78,   0,   0,   0,   0}, { 65,  78,  71,   0,   0,   0},
-            { 65,  79,   0,   0,   0,   0}, { 66,  65,   0,   0,   0,   0},
-            { 66,  65,  73,   0,   0,   0}, { 66,  65,  78,   0,   0,   0},
-            { 66,  65,  78,  71,   0,   0}, { 66,  65,  79,   0,   0,   0},
-            { 66,  69,  73,   0,   0,   0}, { 66,  69,  78,   0,   0,   0},
-            { 66,  69,  78,  71,   0,   0}, { 66,  73,   0,   0,   0,   0},
-            { 66,  73,  65,  78,   0,   0}, { 66,  73,  65,  79,   0,   0},
-            { 66,  73,  69,   0,   0,   0}, { 66,  73,  78,   0,   0,   0},
-            { 66,  73,  78,  71,   0,   0}, { 66,  79,   0,   0,   0,   0},
-            { 66,  85,   0,   0,   0,   0}, { 67,  65,   0,   0,   0,   0},
-            { 67,  65,  73,   0,   0,   0}, { 67,  65,  78,   0,   0,   0},
-            { 67,  65,  78,  71,   0,   0}, { 67,  65,  79,   0,   0,   0},
-            { 67,  69,   0,   0,   0,   0}, { 67,  69,  78,   0,   0,   0},
-            { 67,  69,  78,  71,   0,   0}, { 90,  69,  78,  71,   0,   0},
-            { 67,  69,  78,  71,   0,   0}, { 67,  72,  65,   0,   0,   0},
-            { 67,  72,  65,  73,   0,   0}, { 67,  72,  65,  78,   0,   0},
-            { 67,  72,  65,  78,  71,   0}, { 67,  72,  65,  79,   0,   0},
-            { 67,  72,  69,   0,   0,   0}, { 67,  72,  69,  78,   0,   0},
-            { 83,  72,  69,  78,   0,   0}, { 67,  72,  69,  78,   0,   0},
-            { 67,  72,  69,  78,  71,   0}, { 67,  72,  73,   0,   0,   0},
-            { 67,  72,  79,  78,  71,   0}, { 67,  72,  79,  85,   0,   0},
-            { 67,  72,  85,   0,   0,   0}, { 67,  72,  85,  65,   0,   0},
-            { 67,  72,  85,  65,  73,   0}, { 67,  72,  85,  65,  78,   0},
-            { 67,  72,  85,  65,  78,  71}, { 67,  72,  85,  73,   0,   0},
-            { 67,  72,  85,  78,   0,   0}, { 67,  72,  85,  79,   0,   0},
-            { 67,  73,   0,   0,   0,   0}, { 67,  79,  78,  71,   0,   0},
-            { 67,  79,  85,   0,   0,   0}, { 67,  85,   0,   0,   0,   0},
-            { 67,  85,  65,  78,   0,   0}, { 67,  85,  73,   0,   0,   0},
-            { 67,  85,  78,   0,   0,   0}, { 67,  85,  79,   0,   0,   0},
-            { 68,  65,   0,   0,   0,   0}, { 68,  65,  73,   0,   0,   0},
-            { 68,  65,  78,   0,   0,   0}, { 68,  65,  78,  71,   0,   0},
-            { 68,  65,  79,   0,   0,   0}, { 68,  69,   0,   0,   0,   0},
-            { 68,  69,  78,   0,   0,   0}, { 68,  69,  78,  71,   0,   0},
-            { 68,  73,   0,   0,   0,   0}, { 68,  73,  65,   0,   0,   0},
-            { 68,  73,  65,  78,   0,   0}, { 68,  73,  65,  79,   0,   0},
-            { 68,  73,  69,   0,   0,   0}, { 68,  73,  78,  71,   0,   0},
-            { 68,  73,  85,   0,   0,   0}, { 68,  79,  78,  71,   0,   0},
-            { 68,  79,  85,   0,   0,   0}, { 68,  85,   0,   0,   0,   0},
-            { 68,  85,  65,  78,   0,   0}, { 68,  85,  73,   0,   0,   0},
-            { 68,  85,  78,   0,   0,   0}, { 68,  85,  79,   0,   0,   0},
-            { 69,   0,   0,   0,   0,   0}, { 69,  73,   0,   0,   0,   0},
-            { 69,  78,   0,   0,   0,   0}, { 69,  78,  71,   0,   0,   0},
-            { 69,  82,   0,   0,   0,   0}, { 70,  65,   0,   0,   0,   0},
-            { 70,  65,  78,   0,   0,   0}, { 70,  65,  78,  71,   0,   0},
-            { 70,  69,  73,   0,   0,   0}, { 70,  69,  78,   0,   0,   0},
-            { 70,  69,  78,  71,   0,   0}, { 70,  73,  65,  79,   0,   0},
-            { 70,  79,   0,   0,   0,   0}, { 70,  79,  85,   0,   0,   0},
-            { 70,  85,   0,   0,   0,   0}, { 71,  65,   0,   0,   0,   0},
-            { 71,  65,  73,   0,   0,   0}, { 71,  65,  78,   0,   0,   0},
-            { 71,  65,  78,  71,   0,   0}, { 71,  65,  79,   0,   0,   0},
-            { 71,  69,   0,   0,   0,   0}, { 71,  69,  73,   0,   0,   0},
-            { 71,  69,  78,   0,   0,   0}, { 71,  69,  78,  71,   0,   0},
-            { 71,  79,  78,  71,   0,   0}, { 71,  79,  85,   0,   0,   0},
-            { 71,  85,   0,   0,   0,   0}, { 71,  85,  65,   0,   0,   0},
-            { 71,  85,  65,  73,   0,   0}, { 71,  85,  65,  78,   0,   0},
-            { 71,  85,  65,  78,  71,   0}, { 71,  85,  73,   0,   0,   0},
-            { 71,  85,  78,   0,   0,   0}, { 71,  85,  79,   0,   0,   0},
-            { 72,  65,   0,   0,   0,   0}, { 72,  65,  73,   0,   0,   0},
-            { 72,  65,  78,   0,   0,   0}, { 72,  65,  78,  71,   0,   0},
-            { 72,  65,  79,   0,   0,   0}, { 72,  69,   0,   0,   0,   0},
-            { 72,  69,  73,   0,   0,   0}, { 72,  69,  78,   0,   0,   0},
-            { 72,  69,  78,  71,   0,   0}, { 72,  77,   0,   0,   0,   0},
-            { 72,  79,  78,  71,   0,   0}, { 72,  79,  85,   0,   0,   0},
-            { 72,  85,   0,   0,   0,   0}, { 72,  85,  65,   0,   0,   0},
-            { 72,  85,  65,  73,   0,   0}, { 72,  85,  65,  78,   0,   0},
-            { 72,  85,  65,  78,  71,   0}, { 72,  85,  73,   0,   0,   0},
-            { 72,  85,  78,   0,   0,   0}, { 72,  85,  79,   0,   0,   0},
-            { 74,  73,   0,   0,   0,   0}, { 74,  73,  65,   0,   0,   0},
-            { 74,  73,  65,  78,   0,   0}, { 74,  73,  65,  78,  71,   0},
-            { 74,  73,  65,  79,   0,   0}, { 74,  73,  69,   0,   0,   0},
-            { 74,  73,  78,   0,   0,   0}, { 74,  73,  78,  71,   0,   0},
-            { 74,  73,  79,  78,  71,   0}, { 74,  73,  85,   0,   0,   0},
-            { 74,  85,   0,   0,   0,   0}, { 74,  85,  65,  78,   0,   0},
-            { 74,  85,  69,   0,   0,   0}, { 74,  85,  78,   0,   0,   0},
-            { 75,  65,   0,   0,   0,   0}, { 75,  65,  73,   0,   0,   0},
-            { 75,  65,  78,   0,   0,   0}, { 75,  65,  78,  71,   0,   0},
-            { 75,  65,  79,   0,   0,   0}, { 75,  69,   0,   0,   0,   0},
-            { 75,  69,  78,   0,   0,   0}, { 75,  69,  78,  71,   0,   0},
-            { 75,  79,  78,  71,   0,   0}, { 75,  79,  85,   0,   0,   0},
-            { 75,  85,   0,   0,   0,   0}, { 75,  85,  65,   0,   0,   0},
-            { 75,  85,  65,  73,   0,   0}, { 75,  85,  65,  78,   0,   0},
-            { 75,  85,  65,  78,  71,   0}, { 75,  85,  73,   0,   0,   0},
-            { 75,  85,  78,   0,   0,   0}, { 75,  85,  79,   0,   0,   0},
-            { 76,  65,   0,   0,   0,   0}, { 76,  65,  73,   0,   0,   0},
-            { 76,  65,  78,   0,   0,   0}, { 76,  65,  78,  71,   0,   0},
-            { 76,  65,  79,   0,   0,   0}, { 76,  69,   0,   0,   0,   0},
-            { 76,  69,  73,   0,   0,   0}, { 76,  69,  78,  71,   0,   0},
-            { 76,  73,   0,   0,   0,   0}, { 76,  73,  65,   0,   0,   0},
-            { 76,  73,  65,  78,   0,   0}, { 76,  73,  65,  78,  71,   0},
-            { 76,  73,  65,  79,   0,   0}, { 76,  73,  69,   0,   0,   0},
-            { 76,  73,  78,   0,   0,   0}, { 76,  73,  78,  71,   0,   0},
-            { 76,  73,  85,   0,   0,   0}, { 76,  79,   0,   0,   0,   0},
-            { 76,  79,  78,  71,   0,   0}, { 76,  79,  85,   0,   0,   0},
-            { 76,  85,   0,   0,   0,   0}, { 76,  85,  65,  78,   0,   0},
-            { 76,  85,  69,   0,   0,   0}, { 76,  85,  78,   0,   0,   0},
-            { 76,  85,  79,   0,   0,   0}, { 77,   0,   0,   0,   0,   0},
-            { 77,  65,   0,   0,   0,   0}, { 77,  65,  73,   0,   0,   0},
-            { 77,  65,  78,   0,   0,   0}, { 77,  65,  78,  71,   0,   0},
-            { 77,  65,  79,   0,   0,   0}, { 77,  69,   0,   0,   0,   0},
-            { 77,  69,  73,   0,   0,   0}, { 77,  69,  78,   0,   0,   0},
-            { 77,  69,  78,  71,   0,   0}, { 77,  73,   0,   0,   0,   0},
-            { 77,  73,  65,  78,   0,   0}, { 77,  73,  65,  79,   0,   0},
-            { 77,  73,  69,   0,   0,   0}, { 77,  73,  78,   0,   0,   0},
-            { 77,  73,  78,  71,   0,   0}, { 77,  73,  85,   0,   0,   0},
-            { 77,  79,   0,   0,   0,   0}, { 77,  79,  85,   0,   0,   0},
-            { 77,  85,   0,   0,   0,   0}, { 78,   0,   0,   0,   0,   0},
-            { 78,  65,   0,   0,   0,   0}, { 78,  65,  73,   0,   0,   0},
-            { 78,  65,  78,   0,   0,   0}, { 78,  65,  78,  71,   0,   0},
-            { 78,  65,  79,   0,   0,   0}, { 78,  69,   0,   0,   0,   0},
-            { 78,  69,  73,   0,   0,   0}, { 78,  69,  78,   0,   0,   0},
-            { 78,  69,  78,  71,   0,   0}, { 78,  73,   0,   0,   0,   0},
-            { 78,  73,  65,  78,   0,   0}, { 78,  73,  65,  78,  71,   0},
-            { 78,  73,  65,  79,   0,   0}, { 78,  73,  69,   0,   0,   0},
-            { 78,  73,  78,   0,   0,   0}, { 78,  73,  78,  71,   0,   0},
-            { 78,  73,  85,   0,   0,   0}, { 78,  79,  78,  71,   0,   0},
-            { 78,  79,  85,   0,   0,   0}, { 78,  85,   0,   0,   0,   0},
-            { 78,  85,  65,  78,   0,   0}, { 78,  85,  69,   0,   0,   0},
-            { 78,  85,  78,   0,   0,   0}, { 78,  85,  79,   0,   0,   0},
-            { 79,   0,   0,   0,   0,   0}, { 79,  85,   0,   0,   0,   0},
-            { 80,  65,   0,   0,   0,   0}, { 80,  65,  73,   0,   0,   0},
-            { 80,  65,  78,   0,   0,   0}, { 80,  65,  78,  71,   0,   0},
-            { 80,  65,  79,   0,   0,   0}, { 80,  69,  73,   0,   0,   0},
-            { 80,  69,  78,   0,   0,   0}, { 80,  69,  78,  71,   0,   0},
-            { 80,  73,   0,   0,   0,   0}, { 80,  73,  65,  78,   0,   0},
-            { 80,  73,  65,  79,   0,   0}, { 80,  73,  69,   0,   0,   0},
-            { 80,  73,  78,   0,   0,   0}, { 80,  73,  78,  71,   0,   0},
-            { 80,  79,   0,   0,   0,   0}, { 80,  79,  85,   0,   0,   0},
-            { 80,  85,   0,   0,   0,   0}, { 81,  73,   0,   0,   0,   0},
-            { 81,  73,  65,   0,   0,   0}, { 81,  73,  65,  78,   0,   0},
-            { 81,  73,  65,  78,  71,   0}, { 81,  73,  65,  79,   0,   0},
-            { 81,  73,  69,   0,   0,   0}, { 81,  73,  78,   0,   0,   0},
-            { 81,  73,  78,  71,   0,   0}, { 81,  73,  79,  78,  71,   0},
-            { 81,  73,  85,   0,   0,   0}, { 81,  85,   0,   0,   0,   0},
-            { 81,  85,  65,  78,   0,   0}, { 81,  85,  69,   0,   0,   0},
-            { 81,  85,  78,   0,   0,   0}, { 82,  65,  78,   0,   0,   0},
-            { 82,  65,  78,  71,   0,   0}, { 82,  65,  79,   0,   0,   0},
-            { 82,  69,   0,   0,   0,   0}, { 82,  69,  78,   0,   0,   0},
-            { 82,  69,  78,  71,   0,   0}, { 82,  73,   0,   0,   0,   0},
-            { 82,  79,  78,  71,   0,   0}, { 82,  79,  85,   0,   0,   0},
-            { 82,  85,   0,   0,   0,   0}, { 82,  85,  65,   0,   0,   0},
-            { 82,  85,  65,  78,   0,   0}, { 82,  85,  73,   0,   0,   0},
-            { 82,  85,  78,   0,   0,   0}, { 82,  85,  79,   0,   0,   0},
-            { 83,  65,   0,   0,   0,   0}, { 83,  65,  73,   0,   0,   0},
-            { 83,  65,  78,   0,   0,   0}, { 83,  65,  78,  71,   0,   0},
-            { 83,  65,  79,   0,   0,   0}, { 83,  69,   0,   0,   0,   0},
-            { 83,  69,  78,   0,   0,   0}, { 83,  69,  78,  71,   0,   0},
-            { 83,  72,  65,   0,   0,   0}, { 83,  72,  65,  73,   0,   0},
-            { 83,  72,  65,  78,   0,   0}, { 83,  72,  65,  78,  71,   0},
-            { 83,  72,  65,  79,   0,   0}, { 83,  72,  69,   0,   0,   0},
-            { 83,  72,  69,  78,   0,   0}, { 88,  73,  78,   0,   0,   0},
-            { 83,  72,  69,  78,   0,   0}, { 83,  72,  69,  78,  71,   0},
-            { 83,  72,  73,   0,   0,   0}, { 83,  72,  79,  85,   0,   0},
-            { 83,  72,  85,   0,   0,   0}, { 83,  72,  85,  65,   0,   0},
-            { 83,  72,  85,  65,  73,   0}, { 83,  72,  85,  65,  78,   0},
-            { 83,  72,  85,  65,  78,  71}, { 83,  72,  85,  73,   0,   0},
-            { 83,  72,  85,  78,   0,   0}, { 83,  72,  85,  79,   0,   0},
-            { 83,  73,   0,   0,   0,   0}, { 83,  79,  78,  71,   0,   0},
-            { 83,  79,  85,   0,   0,   0}, { 83,  85,   0,   0,   0,   0},
-            { 83,  85,  65,  78,   0,   0}, { 83,  85,  73,   0,   0,   0},
-            { 83,  85,  78,   0,   0,   0}, { 83,  85,  79,   0,   0,   0},
-            { 84,  65,   0,   0,   0,   0}, { 84,  65,  73,   0,   0,   0},
-            { 84,  65,  78,   0,   0,   0}, { 84,  65,  78,  71,   0,   0},
-            { 84,  65,  79,   0,   0,   0}, { 84,  69,   0,   0,   0,   0},
-            { 84,  69,  78,  71,   0,   0}, { 84,  73,   0,   0,   0,   0},
-            { 84,  73,  65,  78,   0,   0}, { 84,  73,  65,  79,   0,   0},
-            { 84,  73,  69,   0,   0,   0}, { 84,  73,  78,  71,   0,   0},
-            { 84,  79,  78,  71,   0,   0}, { 84,  79,  85,   0,   0,   0},
-            { 84,  85,   0,   0,   0,   0}, { 84,  85,  65,  78,   0,   0},
-            { 84,  85,  73,   0,   0,   0}, { 84,  85,  78,   0,   0,   0},
-            { 84,  85,  79,   0,   0,   0}, { 87,  65,   0,   0,   0,   0},
-            { 87,  65,  73,   0,   0,   0}, { 87,  65,  78,   0,   0,   0},
-            { 87,  65,  78,  71,   0,   0}, { 87,  69,  73,   0,   0,   0},
-            { 87,  69,  78,   0,   0,   0}, { 87,  69,  78,  71,   0,   0},
-            { 87,  79,   0,   0,   0,   0}, { 87,  85,   0,   0,   0,   0},
-            { 88,  73,   0,   0,   0,   0}, { 88,  73,  65,   0,   0,   0},
-            { 88,  73,  65,  78,   0,   0}, { 88,  73,  65,  78,  71,   0},
-            { 88,  73,  65,  79,   0,   0}, { 88,  73,  69,   0,   0,   0},
-            { 88,  73,  78,   0,   0,   0}, { 88,  73,  78,  71,   0,   0},
-            { 88,  73,  79,  78,  71,   0}, { 88,  73,  85,   0,   0,   0},
-            { 88,  85,   0,   0,   0,   0}, { 88,  85,  65,  78,   0,   0},
-            { 88,  85,  69,   0,   0,   0}, { 88,  85,  78,   0,   0,   0},
-            { 89,  65,   0,   0,   0,   0}, { 89,  65,  78,   0,   0,   0},
-            { 89,  65,  78,  71,   0,   0}, { 89,  65,  79,   0,   0,   0},
-            { 89,  69,   0,   0,   0,   0}, { 89,  73,   0,   0,   0,   0},
-            { 89,  73,  78,   0,   0,   0}, { 89,  73,  78,  71,   0,   0},
-            { 89,  79,   0,   0,   0,   0}, { 89,  79,  78,  71,   0,   0},
-            { 89,  79,  85,   0,   0,   0}, { 89,  85,   0,   0,   0,   0},
-            { 89,  85,  65,  78,   0,   0}, { 89,  85,  69,   0,   0,   0},
-            { 89,  85,  78,   0,   0,   0}, { 74,  85,  78,   0,   0,   0},
-            { 89,  85,  78,   0,   0,   0}, { 90,  65,   0,   0,   0,   0},
-            { 90,  65,  73,   0,   0,   0}, { 90,  65,  78,   0,   0,   0},
-            { 90,  65,  78,  71,   0,   0}, { 90,  65,  79,   0,   0,   0},
-            { 90,  69,   0,   0,   0,   0}, { 90,  69,  73,   0,   0,   0},
-            { 90,  69,  78,   0,   0,   0}, { 90,  69,  78,  71,   0,   0},
-            { 90,  72,  65,   0,   0,   0}, { 90,  72,  65,  73,   0,   0},
-            { 90,  72,  65,  78,   0,   0}, { 90,  72,  65,  78,  71,   0},
-            { 67,  72,  65,  78,  71,   0}, { 90,  72,  65,  78,  71,   0},
-            { 90,  72,  65,  79,   0,   0}, { 90,  72,  69,   0,   0,   0},
-            { 90,  72,  69,  78,   0,   0}, { 90,  72,  69,  78,  71,   0},
-            { 90,  72,  73,   0,   0,   0}, { 83,  72,  73,   0,   0,   0},
-            { 90,  72,  73,   0,   0,   0}, { 90,  72,  79,  78,  71,   0},
-            { 90,  72,  79,  85,   0,   0}, { 90,  72,  85,   0,   0,   0},
-            { 90,  72,  85,  65,   0,   0}, { 90,  72,  85,  65,  73,   0},
-            { 90,  72,  85,  65,  78,   0}, { 90,  72,  85,  65,  78,  71},
-            { 90,  72,  85,  73,   0,   0}, { 90,  72,  85,  78,   0,   0},
-            { 90,  72,  85,  79,   0,   0}, { 90,  73,   0,   0,   0,   0},
-            { 90,  79,  78,  71,   0,   0}, { 90,  79,  85,   0,   0,   0},
-            { 90,  85,   0,   0,   0,   0}, { 90,  85,  65,  78,   0,   0},
-            { 90,  85,  73,   0,   0,   0}, { 90,  85,  78,   0,   0,   0},
-            { 90,  85,  79,   0,   0,   0}, {  0,   0,   0,   0,   0,   0},
-            { 83,  72,  65,  78,   0,   0}, {  0,   0,   0,   0,   0,   0}, };
-
-    /** First and last Chinese character with known Pinyin according to zh collation */
-    private static final String FIRST_PINYIN_UNIHAN = "\u963F";
-    private static final String LAST_PINYIN_UNIHAN = "\u9FFF";
-
-    private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA);
-
     private static HanziToPinyin sInstance;
-    private final boolean mHasChinaCollator;
+    private Transliterator mPinyinTransliterator;
+    private Transliterator mAsciiTransliterator;
 
     public static class Token {
         /**
@@ -380,166 +71,98 @@
         public String target;
     }
 
-    protected HanziToPinyin(boolean hasChinaCollator) {
-        mHasChinaCollator = hasChinaCollator;
+    private HanziToPinyin() {
+        try {
+            mPinyinTransliterator = new Transliterator("Han-Latin/Names; Latin-Ascii; Any-Upper");
+            mAsciiTransliterator = new Transliterator("Latin-Ascii");
+        } catch (RuntimeException e) {
+            Log.w(TAG, "Han-Latin/Names transliterator data is missing,"
+                  + " HanziToPinyin is disabled");
+        }
+    }
+
+    public boolean hasChineseTransliterator() {
+        return mPinyinTransliterator != null;
     }
 
     public static HanziToPinyin getInstance() {
         synchronized (HanziToPinyin.class) {
-            if (sInstance != null) {
-                return sInstance;
+            if (sInstance == null) {
+                sInstance = new HanziToPinyin();
             }
-            // Check if zh_CN collation data is available
-            final Locale locale[] = Collator.getAvailableLocales();
-            for (int i = 0; i < locale.length; i++) {
-                if (locale[i].equals(Locale.CHINA)) {
-                    // Do self validation just once.
-                    if (DEBUG) {
-                        Log.d(TAG, "Self validation. Result: " + doSelfValidation());
-                    }
-                    sInstance = new HanziToPinyin(true);
-                    return sInstance;
-                }
-            }
-            Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled");
-            sInstance = new HanziToPinyin(false);
             return sInstance;
         }
     }
 
-    /**
-     * Validate if our internal table has some wrong value.
-     *
-     * @return true when the table looks correct.
-     */
-    private static boolean doSelfValidation() {
-        char lastChar = UNIHANS[0];
-        String lastString = Character.toString(lastChar);
-        for (char c : UNIHANS) {
-            if (lastChar == c) {
-                continue;
-            }
-            final String curString = Character.toString(c);
-            int cmp = COLLATOR.compare(lastString, curString);
-            if (cmp >= 0) {
-                Log.e(TAG, "Internal error in Unihan table. " + "The last string \"" + lastString
-                        + "\" is greater than current string \"" + curString + "\".");
-                return false;
-            }
-            lastString = curString;
-        }
-        return true;
-    }
+    private void tokenize(char character, Token token) {
+        token.source = Character.toString(character);
 
-    private Token getToken(char character) {
-        Token token = new Token();
-        final String letter = Character.toString(character);
-        token.source = letter;
-        int offset = -1;
-        int cmp;
-        if (character < 256) {
+        // ASCII
+        if (character < 128) {
             token.type = Token.LATIN;
-            token.target = letter;
-            return token;
-        } else {
-            cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN);
-            if (cmp < 0) {
-                token.type = Token.UNKNOWN;
-                token.target = letter;
-                return token;
-            } else if (cmp == 0) {
-                token.type = Token.PINYIN;
-                offset = 0;
-            } else {
-                cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN);
-                if (cmp > 0) {
-                    token.type = Token.UNKNOWN;
-                    token.target = letter;
-                    return token;
-                } else if (cmp == 0) {
-                    token.type = Token.PINYIN;
-                    offset = UNIHANS.length - 1;
-                }
-            }
+            token.target = token.source;
+            return;
+        }
+
+        // Extended Latin. Transcode these to ASCII equivalents
+        if (character < 0x250 || (0x1e00 <= character && character < 0x1eff)) {
+            token.type = Token.LATIN;
+            token.target = mAsciiTransliterator == null ? token.source :
+                mAsciiTransliterator.transliterate(token.source);
+            return;
         }
 
         token.type = Token.PINYIN;
-        if (offset < 0) {
-            int begin = 0;
-            int end = UNIHANS.length - 1;
-            while (begin <= end) {
-                offset = (begin + end) / 2;
-                final String unihan = Character.toString(UNIHANS[offset]);
-                cmp = COLLATOR.compare(letter, unihan);
-                if (cmp == 0) {
-                    break;
-                } else if (cmp > 0) {
-                    begin = offset + 1;
-                } else {
-                    end = offset - 1;
-                }
-            }
-        }
-        if (cmp < 0) {
-            offset--;
-        }
-        StringBuilder pinyin = new StringBuilder();
-        for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) {
-            pinyin.append((char) PINYINS[offset][j]);
-        }
-        token.target = pinyin.toString();
-        if (TextUtils.isEmpty(token.target)) {
+        token.target = mPinyinTransliterator.transliterate(token.source);
+        if (TextUtils.isEmpty(token.target) ||
+            TextUtils.equals(token.source, token.target)) {
             token.type = Token.UNKNOWN;
             token.target = token.source;
         }
-        return token;
     }
 
     /**
      * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without
      * space will be put into a Token, One Hanzi character which has pinyin will be treated as a
-     * Token. If these is no China collator, the empty token array is returned.
+     * Token. If there is no Chinese transliterator, the empty token array is returned.
      */
     public ArrayList<Token> get(final String input) {
         ArrayList<Token> tokens = new ArrayList<Token>();
-        if (!mHasChinaCollator || TextUtils.isEmpty(input)) {
+        if (!hasChineseTransliterator() || TextUtils.isEmpty(input)) {
             // return empty tokens.
             return tokens;
         }
+
         final int inputLength = input.length();
         final StringBuilder sb = new StringBuilder();
         int tokenType = Token.LATIN;
+        Token token = new Token();
+
         // Go through the input, create a new token when
         // a. Token type changed
         // b. Get the Pinyin of current charater.
         // c. current character is space.
         for (int i = 0; i < inputLength; i++) {
             final char character = input.charAt(i);
-            if (character == ' ') {
+            if (Character.isSpaceChar(character)) {
                 if (sb.length() > 0) {
                     addToken(sb, tokens, tokenType);
                 }
-            } else if (character < 256) {
-                if (tokenType != Token.LATIN && sb.length() > 0) {
-                    addToken(sb, tokens, tokenType);
-                }
-                tokenType = Token.LATIN;
-                sb.append(character);
             } else {
-                Token t = getToken(character);
-                if (t.type == Token.PINYIN) {
+                tokenize(character, token);
+                if (token.type == Token.PINYIN) {
                     if (sb.length() > 0) {
                         addToken(sb, tokens, tokenType);
                     }
-                    tokens.add(t);
-                    tokenType = Token.PINYIN;
+                    tokens.add(token);
+                    token = new Token();
                 } else {
-                    if (tokenType != t.type && sb.length() > 0) {
+                    if (tokenType != token.type && sb.length() > 0) {
                         addToken(sb, tokens, tokenType);
                     }
-                    tokenType = t.type;
-                    sb.append(character);
+                    sb.append(token.target);
                 }
+                tokenType = token.type;
             }
         }
         if (sb.length() > 0) {
diff --git a/src/com/android/providers/contacts/LegacyApiSupport.java b/src/com/android/providers/contacts/LegacyApiSupport.java
index 9859d11..1ed7488 100644
--- a/src/com/android/providers/contacts/LegacyApiSupport.java
+++ b/src/com/android/providers/contacts/LegacyApiSupport.java
@@ -1877,13 +1877,13 @@
 
             case SEARCH_SUGGESTIONS:
                 return mGlobalSearchSupport.handleSearchSuggestionsQuery(
-                        db, uri, projection, limit);
+                        db, uri, projection, limit, null);
 
             case SEARCH_SHORTCUT: {
                 String lookupKey = uri.getLastPathSegment();
                 String filter = ContactsProvider2.getQueryParameter(uri, "filter");
                 return mGlobalSearchSupport.handleSearchShortcutRefresh(
-                        db, projection, lookupKey, filter);
+                        db, projection, lookupKey, filter, null);
             }
 
             case LIVE_FOLDERS_PEOPLE:
diff --git a/src/com/android/providers/contacts/NameLookupBuilder.java b/src/com/android/providers/contacts/NameLookupBuilder.java
index 8375b88..fb266da 100644
--- a/src/com/android/providers/contacts/NameLookupBuilder.java
+++ b/src/com/android/providers/contacts/NameLookupBuilder.java
@@ -319,9 +319,14 @@
         }
     }
 
-    private void appendNameShorthandLookup(IndexBuilder builder, String name, int fullNameStyle) {
+    /**
+     * Insert more name indexes according to locale specifies for those locales
+     * for which we have alternative shorthand name methods (eg, Pinyin for
+     * Chinese, Romaji for Japanese).
+     */
+    public void appendNameShorthandLookup(IndexBuilder builder, String name, int fullNameStyle) {
         Iterator<String> it =
-                ContactLocaleUtils.getIntance().getNameLookupKeys(name, fullNameStyle);
+                ContactLocaleUtils.getInstance().getNameLookupKeys(name, fullNameStyle);
         if (it != null) {
             while (it.hasNext()) {
                 builder.appendName(it.next());
diff --git a/src/com/android/providers/contacts/NameSplitter.java b/src/com/android/providers/contacts/NameSplitter.java
index 43743ee..ebf6136 100644
--- a/src/com/android/providers/contacts/NameSplitter.java
+++ b/src/com/android/providers/contacts/NameSplitter.java
@@ -330,9 +330,6 @@
         }
 
         String firstToken = tokenizer.mTokens[tokenizer.mStartPointer];
-        if (mPrefixesSet.contains(firstToken.toUpperCase())) {
-           tokenizer.mStartPointer++;
-        }
         int count = 0;
         for (int i = tokenizer.mStartPointer; i < tokenizer.mEndPointer; i++) {
             tokens[count++] = tokenizer.mTokens[i];
diff --git a/src/com/android/providers/contacts/TransactionContext.java b/src/com/android/providers/contacts/TransactionContext.java
index 2bbacf0..d8c93ef 100644
--- a/src/com/android/providers/contacts/TransactionContext.java
+++ b/src/com/android/providers/contacts/TransactionContext.java
@@ -35,6 +35,11 @@
     private HashMap<Long, Long> mInsertedRawContactsAccounts;
     private HashSet<Long> mUpdatedRawContacts;
     private HashSet<Long> mDirtyRawContacts;
+    // Set used to track what has been changed and deleted. This is needed so we can update the
+    // contact last touch timestamp.  Dirty set above is only set when sync adapter is false.
+    // {@see android.provider.ContactsContract#CALLER_IS_SYNCADAPTER}. While the set below will
+    // contain all changed contacts.
+    private HashSet<Long> mChangedRawContacts;
     private HashSet<Long> mStaleSearchIndexRawContacts;
     private HashSet<Long> mStaleSearchIndexContacts;
     private HashMap<Long, Object> mUpdatedSyncStates;
@@ -50,6 +55,8 @@
     public void rawContactInserted(long rawContactId, long accountId) {
         if (mInsertedRawContactsAccounts == null) mInsertedRawContactsAccounts = Maps.newHashMap();
         mInsertedRawContactsAccounts.put(rawContactId, accountId);
+
+        markRawContactChangedOrDeletedOrInserted(rawContactId);
     }
 
     public void rawContactUpdated(long rawContactId) {
@@ -57,9 +64,22 @@
         mUpdatedRawContacts.add(rawContactId);
     }
 
-    public void markRawContactDirty(long rawContactId) {
-        if (mDirtyRawContacts == null) mDirtyRawContacts = Sets.newHashSet();
-        mDirtyRawContacts.add(rawContactId);
+    public void markRawContactDirtyAndChanged(long rawContactId, boolean isSyncAdapter) {
+        if (!isSyncAdapter) {
+            if (mDirtyRawContacts == null) {
+                mDirtyRawContacts = Sets.newHashSet();
+            }
+            mDirtyRawContacts.add(rawContactId);
+        }
+
+        markRawContactChangedOrDeletedOrInserted(rawContactId);
+    }
+
+    public void markRawContactChangedOrDeletedOrInserted(long rawContactId) {
+        if (mChangedRawContacts == null) {
+            mChangedRawContacts = Sets.newHashSet();
+        }
+        mChangedRawContacts.add(rawContactId);
     }
 
     public void syncStateUpdated(long rowId, Object data) {
@@ -92,6 +112,11 @@
         return mDirtyRawContacts;
     }
 
+    public Set<Long> getChangedRawContactIds() {
+        if (mChangedRawContacts == null) mChangedRawContacts = Sets.newHashSet();
+        return mChangedRawContacts;
+    }
+
     public Set<Long> getStaleSearchIndexRawContactIds() {
         if (mStaleSearchIndexRawContacts == null) mStaleSearchIndexRawContacts = Sets.newHashSet();
         return mStaleSearchIndexRawContacts;
@@ -122,6 +147,7 @@
         mUpdatedRawContacts = null;
         mUpdatedSyncStates = null;
         mDirtyRawContacts = null;
+        mChangedRawContacts = null;
     }
 
     public void clearSearchIndexUpdates() {
diff --git a/src/com/android/providers/contacts/aggregation/ContactAggregator.java b/src/com/android/providers/contacts/aggregation/ContactAggregator.java
index baae2e5..6c53fa0 100644
--- a/src/com/android/providers/contacts/aggregation/ContactAggregator.java
+++ b/src/com/android/providers/contacts/aggregation/ContactAggregator.java
@@ -63,6 +63,9 @@
 import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
 import com.android.providers.contacts.aggregation.util.ContactMatcher;
 import com.android.providers.contacts.aggregation.util.ContactMatcher.MatchScore;
+import com.android.providers.contacts.database.ContactsTableUtil;
+import com.android.providers.contacts.util.Clock;
+
 import com.google.android.collect.Maps;
 
 import java.util.ArrayList;
@@ -143,7 +146,6 @@
     private SQLiteStatement mAggregatedPresenceReplace;
     private SQLiteStatement mPresenceContactIdUpdate;
     private SQLiteStatement mRawContactCountQuery;
-    private SQLiteStatement mContactDelete;
     private SQLiteStatement mAggregatedPresenceDelete;
     private SQLiteStatement mMarkForAggregation;
     private SQLiteStatement mPhotoIdUpdate;
@@ -300,10 +302,6 @@
                 " WHERE " + RawContacts.CONTACT_ID + "=?"
                         + " AND " + RawContacts._ID + "<>?");
 
-        mContactDelete = db.compileStatement(
-                "DELETE FROM " + Tables.CONTACTS +
-                " WHERE " + Contacts._ID + "=?");
-
         mAggregatedPresenceDelete = db.compileStatement(
                 "DELETE FROM " + Tables.AGGREGATED_PRESENCE +
                 " WHERE " + AggregatedPresenceColumns.CONTACT_ID + "=?");
@@ -778,8 +776,7 @@
             // Joining with an existing aggregate
             if (currentContactContentsCount == 0) {
                 // Delete a previous aggregate if it only contained this raw contact
-                mContactDelete.bindLong(1, currentContactId);
-                mContactDelete.execute();
+                ContactsTableUtil.deleteContact(db, currentContactId);
 
                 mAggregatedPresenceDelete.bindLong(1, currentContactId);
                 mAggregatedPresenceDelete.execute();
@@ -1763,7 +1760,8 @@
                         + Contacts.TIMES_CONTACTED + "=?, "
                         + Contacts.STARRED + "=?, "
                         + Contacts.HAS_PHONE_NUMBER + "=?, "
-                        + Contacts.LOOKUP_KEY + "=? " +
+                        + Contacts.LOOKUP_KEY + "=?, "
+                        + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + "=? " +
                 " WHERE " + Contacts._ID + "=?";
 
         String INSERT_SQL =
@@ -1777,8 +1775,10 @@
                         + Contacts.TIMES_CONTACTED + ", "
                         + Contacts.STARRED + ", "
                         + Contacts.HAS_PHONE_NUMBER + ", "
-                        + Contacts.LOOKUP_KEY + ") " +
-                " VALUES (?,?,?,?,?,?,?,?,?,?)";
+                        + Contacts.LOOKUP_KEY + ", "
+                        + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
+                        + ") " +
+                " VALUES (?,?,?,?,?,?,?,?,?,?,?)";
 
         int NAME_RAW_CONTACT_ID = 1;
         int PHOTO_ID = 2;
@@ -1790,7 +1790,8 @@
         int STARRED = 8;
         int HAS_PHONE_NUMBER = 9;
         int LOOKUP_KEY = 10;
-        int CONTACT_ID = 11;
+        int CONTACT_LAST_UPDATED_TIMESTAMP = 11;
+        int CONTACT_ID = 12;
     }
 
     /**
@@ -1954,6 +1955,8 @@
                 hasPhoneNumber);
         statement.bindString(ContactReplaceSqlStatement.LOOKUP_KEY,
                 Uri.encode(lookupKey.toString()));
+        statement.bindLong(ContactReplaceSqlStatement.CONTACT_LAST_UPDATED_TIMESTAMP,
+                Clock.getInstance().currentTimeMillis());
     }
 
     /**
diff --git a/src/com/android/providers/contacts/database/ContactsTableUtil.java b/src/com/android/providers/contacts/database/ContactsTableUtil.java
new file mode 100644
index 0000000..dbc3d3e
--- /dev/null
+++ b/src/com/android/providers/contacts/database/ContactsTableUtil.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.database;
+
+import static android.provider.ContactsContract.Contacts;
+import static com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+
+import com.android.common.io.MoreCloseables;
+import com.android.providers.contacts.util.Clock;
+
+import java.util.Set;
+
+/**
+ * Methods for operating on the contacts table.
+ */
+public class ContactsTableUtil {
+
+    /**
+     * Drop indexes if present.  Create indexes.
+     *
+     * @param db The sqlite database instance.
+     */
+    public static void createIndexes(SQLiteDatabase db) {
+        final String table = Tables.CONTACTS;
+
+        db.execSQL("CREATE INDEX contacts_has_phone_index ON " + table + " (" +
+                Contacts.HAS_PHONE_NUMBER +
+                ");");
+
+        db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + table + " (" +
+                Contacts.NAME_RAW_CONTACT_ID +
+                ");");
+
+        db.execSQL(MoreDatabaseUtils.buildCreateIndexSql(table,
+                Contacts.CONTACT_LAST_UPDATED_TIMESTAMP));
+    }
+
+    public static void updateContactLastUpdateByContactId(SQLiteDatabase db, long contactId) {
+        final ContentValues values = new ContentValues();
+        values.put(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
+                Clock.getInstance().currentTimeMillis());
+        db.update(Tables.CONTACTS, values, Contacts._ID + " = ?",
+                new String[] {String.valueOf(contactId)});
+    }
+
+    /**
+     * Refreshes the last updated timestamp of the contact with the current time.
+     *
+     * @param db The sqlite database instance.
+     * @param rawContactIds A set of raw contacts ids to refresh the contact for.
+     */
+    public static void updateContactLastUpdateByRawContactId(SQLiteDatabase db,
+            Set<Long> rawContactIds) {
+        if (rawContactIds.isEmpty()) {
+            return;
+        }
+
+        db.execSQL(buildUpdateLastUpdateSql(rawContactIds));
+    }
+
+    /**
+     * Build a sql to update the last updated timestamp for contacts.
+     *
+     * @param rawContactIds The raw contact ids that contacts should be updated for.
+     * @return The update sql statement.
+     */
+    private static String buildUpdateLastUpdateSql(Set<Long> rawContactIds) {
+        // Not using bind args here due to sqlite bind arg size limit.  Large number of bind args
+        // will cause a sqlite error:
+        //     android.database.sqlite.SQLiteException: too many SQL variables (code 1)
+        // Sql injection is not possible because input is a set of Long.  If any part of the sql
+        // is built with user input strings, then this must be converted to using bind args.
+        final String sql = "UPDATE " + Tables.CONTACTS
+                + " SET " + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " = "
+                + Clock.getInstance().currentTimeMillis()
+                + " WHERE " + Contacts._ID + " IN ( "
+                + "  SELECT " + ContactsContract.RawContacts.CONTACT_ID
+                + "  FROM " + Tables.RAW_CONTACTS
+                + "  WHERE " + ContactsContract.RawContacts._ID
+                + " IN (" + TextUtils.join(",", rawContactIds) + ") "
+                + ")";
+        return sql;
+    }
+
+    /**
+     * Delete a contact identified by the contact id.
+     *
+     * @param db The sqlite database instance.
+     * @param contactId The contact id to delete.
+     * @return The number of records deleted.
+     */
+    public static int deleteContact(SQLiteDatabase db, long contactId) {
+        DeletedContactsTableUtil.insertDeletedContact(db, contactId);
+        return db.delete(Tables.CONTACTS, Contacts._ID + " = ?", new String[]{contactId + ""});
+    }
+
+    /**
+     * Delete the aggregate contact if it has no constituent raw contacts other than the supplied
+     * one.
+     */
+    public static void deleteContactIfSingleton(SQLiteDatabase db, long rawContactId) {
+        // This query will find a contact id if the contact has a raw contacts other than the one
+        // passed in.
+        final String sql = "select " + ContactsContract.RawContacts.CONTACT_ID + ", count(1)"
+                + " from " + Tables.RAW_CONTACTS
+                + " where " + ContactsContract.RawContacts.CONTACT_ID + " ="
+                + "  (select " + ContactsContract.RawContacts.CONTACT_ID
+                + "   from " + Tables.RAW_CONTACTS
+                + "   where " + ContactsContract.RawContacts._ID + " = ?)"
+                + " group by " + ContactsContract.RawContacts.CONTACT_ID;
+        final Cursor cursor = db.rawQuery(sql, new String[]{rawContactId + ""});
+        try {
+            if (cursor.moveToNext()) {
+                long contactId = cursor.getLong(0);
+                long numRawContacts = cursor.getLong(1);
+
+                if (numRawContacts == 1) {
+                    // Only one raw contact, we can delete the parent.
+                    deleteContact(db, contactId);
+                }
+            }
+        } finally {
+            MoreCloseables.closeQuietly(cursor);
+        }
+    }
+}
diff --git a/src/com/android/providers/contacts/database/DeletedContactsTableUtil.java b/src/com/android/providers/contacts/database/DeletedContactsTableUtil.java
new file mode 100644
index 0000000..31cf885
--- /dev/null
+++ b/src/com/android/providers/contacts/database/DeletedContactsTableUtil.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.database;
+
+import android.content.ContentValues;
+import android.database.sqlite.SQLiteDatabase;
+import android.provider.ContactsContract;
+
+import com.android.providers.contacts.ContactsDatabaseHelper;
+import com.android.providers.contacts.util.Clock;
+
+/**
+ * Methods for operating on the deleted_contacts table.
+ */
+public class DeletedContactsTableUtil {
+
+    /**
+     * Create deleted_contacts tables and indexes.
+     *
+     * @param db The sqlite database instance.
+     */
+    public static void create(SQLiteDatabase db) {
+        // Deleted contacts log
+        db.execSQL("CREATE TABLE " + ContactsDatabaseHelper.Tables.DELETED_CONTACTS + " (" +
+                ContactsContract.DeletedContacts.CONTACT_ID + " INTEGER PRIMARY KEY," +
+                ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP +
+                " INTEGER NOT NULL default 0"
+                + ");");
+
+        db.execSQL(MoreDatabaseUtils.buildCreateIndexSql(
+                ContactsDatabaseHelper.Tables.DELETED_CONTACTS,
+                ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP));
+    }
+
+    /**
+     * Inserts a deleted contact log record.
+     *
+     * @param db The SQLiteDatabase instance.
+     * @param contactId The contact id to insert.
+     * @return The row id
+     */
+    public static long insertDeletedContact(SQLiteDatabase db, long contactId) {
+        ContentValues values = new ContentValues();
+        values.put(ContactsContract.DeletedContacts.CONTACT_ID, contactId);
+        values.put(ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP,
+                Clock.getInstance().currentTimeMillis());
+        // a.k.a upsert
+        return db.insertWithOnConflict(ContactsDatabaseHelper.Tables.DELETED_CONTACTS, null, values,
+                SQLiteDatabase.CONFLICT_REPLACE);
+    }
+
+    /**
+     * Deletes old log records.
+     *
+     * @param db The database instance to use.
+     */
+    public static int deleteOldLogs(SQLiteDatabase db) {
+
+        long time = Clock.getInstance().currentTimeMillis() -
+                ContactsContract.DeletedContacts.DAYS_KEPT_MILLISECONDS;
+
+        String[] args = new String[]{time + ""};
+
+        return db.delete(ContactsDatabaseHelper.Tables.DELETED_CONTACTS,
+                ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + " < ?", args);
+    }
+}
diff --git a/src/com/android/providers/contacts/database/MoreDatabaseUtils.java b/src/com/android/providers/contacts/database/MoreDatabaseUtils.java
new file mode 100644
index 0000000..fa186bb
--- /dev/null
+++ b/src/com/android/providers/contacts/database/MoreDatabaseUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.database;
+
+import com.android.providers.contacts.util.NeededForTesting;
+
+/**
+ * Static methods for database operations.
+ */
+public class MoreDatabaseUtils {
+
+    /**
+     * Builds a CREATE INDEX ddl statement for a given table and field.
+     *
+     * @param table The table name.
+     * @param field The field to index.
+     * @return The create index sql statement.
+     */
+    public static String buildCreateIndexSql(String table, String field) {
+        return "CREATE INDEX " + buildIndexName(table, field) + " ON " + table
+                + "(" + field + ")";
+    }
+
+    /**
+     * Builds a DROP INDEX ddl statement for a given table and field.
+     *
+     * @param table The table name that was originally used to create the index.
+     * @param field The field that was originally used to create the index.
+     * @return The drop index sql statement.
+     */
+    @NeededForTesting
+    public static String buildDropIndexSql(String table, String field) {
+        return "DROP INDEX IF EXISTS " + buildIndexName(table, field);
+    }
+
+    /**
+     * The index is created with a name using the following convention:
+     * <p>
+     * [table name]_[field name]_index
+     */
+    public static String buildIndexName(String table, String field) {
+        return table + "_" + field + "_index";
+    }
+
+    /**
+     * Build a bind arg where clause.
+     * <p>
+     * e.g. Calling this method with value of 4 results in:
+     * <p>
+     * "?,?,?,?"
+     *
+     * @param numArgs The number of arguments.
+     * @return A string that can be used for bind args in a sql where clause.
+     */
+    @NeededForTesting
+    public static String buildBindArgString(int numArgs) {
+        final StringBuilder sb = new StringBuilder();
+        String delimiter = "";
+        for (int i = 0; i < numArgs; i++) {
+            sb.append(delimiter).append("?");
+            delimiter = ",";
+        }
+        return sb.toString();
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index 3759196..a67785c 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -17,6 +17,7 @@
 package com.android.providers.contacts;
 
 import static com.android.providers.contacts.ContactsActor.PACKAGE_GREY;
+import static com.android.providers.contacts.TestUtils.cv;
 
 import android.accounts.Account;
 import android.content.ContentProvider;
@@ -58,6 +59,10 @@
 import android.util.Log;
 
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.testutil.CommonDatabaseUtils;
+import com.android.providers.contacts.testutil.DataUtil;
+import com.android.providers.contacts.testutil.RawContactUtil;
+import com.android.providers.contacts.testutil.TestUtil;
 import com.android.providers.contacts.util.Hex;
 import com.android.providers.contacts.util.MockClock;
 import com.google.android.collect.Sets;
@@ -150,38 +155,6 @@
         return mActor.provider;
     }
 
-    protected Uri maybeAddAccountQueryParameters(Uri uri, Account account) {
-        if (account == null) {
-            return uri;
-        }
-        return uri.buildUpon()
-                .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
-                .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account.type)
-                .build();
-    }
-
-    protected long createRawContact() {
-        return createRawContact(null);
-    }
-
-    protected long createRawContactWithName() {
-        return createRawContactWithName(null);
-    }
-
-    protected long createRawContactWithName(Account account) {
-        return createRawContactWithName("John", "Doe", account);
-    }
-
-    protected long createRawContactWithName(String firstName, String lastName) {
-        return createRawContactWithName(firstName, lastName, null);
-    }
-
-    protected long createRawContactWithName(String firstName, String lastName, Account account) {
-        long rawContactId = createRawContact(account);
-        insertStructuredName(rawContactId, firstName, lastName);
-        return rawContactId;
-    }
-
     protected Uri setCallerIsSyncAdapter(Uri uri, Account account) {
         if (account == null) {
             return uri;
@@ -193,14 +166,6 @@
         return builder.build();
     }
 
-    protected long createRawContact(Account account, String... extras) {
-        ContentValues values = new ContentValues();
-        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);
@@ -208,17 +173,10 @@
 
     protected int updateItem(Uri uri, String... extras) {
         ContentValues values = new ContentValues();
-        extrasVarArgsToValues(values, extras);
+        CommonDatabaseUtils.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, false, false);
     }
@@ -240,7 +198,7 @@
         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);
+        final Uri uri = TestUtil.maybeAddAccountQueryParameters(Groups.CONTENT_URI, account);
         return ContentUris.parseId(mResolver.insert(uri, values));
     }
 
@@ -262,32 +220,6 @@
         mResolver.insert(Settings.CONTENT_URI, values);
     }
 
-    protected Uri insertStructuredName(long rawContactId, String givenName, String familyName) {
-        ContentValues values = new ContentValues();
-        StringBuilder sb = new StringBuilder();
-        if (givenName != null) {
-            sb.append(givenName);
-        }
-        if (givenName != null && familyName != null) {
-            sb.append(" ");
-        }
-        if (familyName != null) {
-            sb.append(familyName);
-        }
-        values.put(StructuredName.DISPLAY_NAME, sb.toString());
-        values.put(StructuredName.GIVEN_NAME, givenName);
-        values.put(StructuredName.FAMILY_NAME, familyName);
-
-        return insertStructuredName(rawContactId, values);
-    }
-
-    protected Uri insertStructuredName(long rawContactId, ContentValues values) {
-        values.put(Data.RAW_CONTACT_ID, rawContactId);
-        values.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
-        Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
-        return resultUri;
-    }
-
     protected Uri insertOrganization(long rawContactId, ContentValues values) {
         return insertOrganization(rawContactId, values, false);
     }
@@ -335,8 +267,18 @@
         return insertEmail(rawContactId, email, primary, Email.TYPE_HOME, null);
     }
 
+    protected Uri insertEmail(long rawContactId, String email, boolean primary,
+            boolean superPrimary) {
+        return insertEmail(rawContactId, email, primary, superPrimary, Email.TYPE_HOME, null);
+    }
+
     protected Uri insertEmail(long rawContactId, String email, boolean primary, int type,
             String label) {
+        return insertEmail(rawContactId, email, primary, false, type, label);
+    }
+
+    protected Uri insertEmail(long rawContactId, String email, boolean primary,
+            boolean superPrimary, int type,  String label) {
         ContentValues values = new ContentValues();
         values.put(Data.RAW_CONTACT_ID, rawContactId);
         values.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
@@ -346,6 +288,9 @@
         if (primary) {
             values.put(Data.IS_PRIMARY, 1);
         }
+        if (superPrimary) {
+            values.put(Data.IS_SUPER_PRIMARY, 1);
+        }
 
         Uri resultUri = mResolver.insert(Data.CONTENT_URI, values);
         return resultUri;
@@ -493,21 +438,17 @@
 
     protected Uri insertStreamItem(long rawContactId, ContentValues values, Account account) {
         return mResolver.insert(
-                maybeAddAccountQueryParameters(
-                        Uri.withAppendedPath(
-                                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
-                                RawContacts.StreamItems.CONTENT_DIRECTORY),
-                        account),
+                TestUtil.maybeAddAccountQueryParameters(Uri.withAppendedPath(
+                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                        RawContacts.StreamItems.CONTENT_DIRECTORY), account),
                 values);
     }
 
     protected Uri insertStreamItemPhoto(long streamItemId, ContentValues values, Account account) {
         return mResolver.insert(
-                maybeAddAccountQueryParameters(
-                        Uri.withAppendedPath(
-                                ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
-                                StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
-                        account),
+                TestUtil.maybeAddAccountQueryParameters(Uri.withAppendedPath(
+                        ContentUris.withAppendedId(StreamItems.CONTENT_URI, streamItemId),
+                        StreamItems.StreamItemPhotos.CONTENT_DIRECTORY), account),
                 values);
     }
 
@@ -1099,6 +1040,22 @@
         assertTrue(message.toString(), result);
     }
 
+    protected void assertCursorContains(Cursor cursor, ContentValues expectedValues) {
+        final StringBuilder message = new StringBuilder();
+        boolean found = false;
+        cursor.moveToPosition(-1);
+        while (cursor.moveToNext()) {
+            message.setLength(0);
+            final int pos = cursor.getPosition();
+            found = equalsWithExpectedValues(cursor, expectedValues, message);
+            if (found) {
+                break;
+            }
+        }
+        assertTrue("Expected values can not be found " + expectedValues + "," + message.toString(),
+                found);
+    }
+
     protected void assertCursorValues(Cursor cursor, ContentValues... expectedValues) {
         StringBuilder message = new StringBuilder();
 
@@ -1166,6 +1123,25 @@
         return true;
     }
 
+    private static final String[] DATA_USAGE_PROJECTION =
+            new String[] {Data.DATA1, Data.TIMES_USED, Data.LAST_TIME_USED};
+
+    protected void assertDataUsageCursorContains(Uri uri, String data1, int timesUsed,
+            int lastTimeUsed) {
+        final Cursor cursor = mResolver.query(uri, DATA_USAGE_PROJECTION, null, null,
+                null);
+        try {
+            assertCursorContains(cursor,
+                    cv(
+                            Data.DATA1, data1,
+                            Data.TIMES_USED, timesUsed,
+                            Data.LAST_TIME_USED, lastTimeUsed)
+            );
+        } finally {
+            cursor.close();
+        }
+    }
+
     private String[] buildProjection(ContentValues values) {
         String[] projection = new String[values.size()];
         Iterator<Entry<String, Object>> iter = values.valueSet().iterator();
@@ -1494,11 +1470,11 @@
 
             final long groupId = createGroup(mAccount, "gsid1", "title1");
 
-            long rawContactId = createRawContact();
+            long rawContactId = RawContactUtil.createRawContact(mResolver);
             insertGroupMembership(rawContactId, groupId);
 
             if (givenName != null || familyName != null) {
-                insertStructuredName(rawContactId, givenName, familyName);
+                DataUtil.insertStructuredName(mResolver, rawContactId, givenName, familyName);
             }
             if (nickname != null) {
                 insertNickname(rawContactId, nickname);
diff --git a/tests/src/com/android/providers/contacts/CallerInfoIntegrationTest.java b/tests/src/com/android/providers/contacts/CallerInfoIntegrationTest.java
index 4c0d2df..78b9ec5 100644
--- a/tests/src/com/android/providers/contacts/CallerInfoIntegrationTest.java
+++ b/tests/src/com/android/providers/contacts/CallerInfoIntegrationTest.java
@@ -23,6 +23,7 @@
 import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.internal.telephony.CallerInfo;
+import com.android.providers.contacts.testutil.DataUtil;
 
 /**
  * Integration test for {@link CallerInfo} and {@link ContactsProvider2}.
@@ -44,7 +45,7 @@
         Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         long rawContactId = ContentUris.parseId(rawContactUri);
 
-        insertStructuredName(rawContactId, "Hot", "Tamale");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Hot", "Tamale");
         insertPhoneNumber(rawContactId, "800-466-4411");
 
         CallerInfo callerInfo = CallerInfo.getCallerInfo(getProvider().getContext(), "18004664411");
diff --git a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
index 96cbb9b..be14f45 100644
--- a/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
+++ b/tests/src/com/android/providers/contacts/ContactDirectoryManagerTest.java
@@ -63,7 +63,7 @@
         private MatrixCursor mResponse;
 
         @Override
-        public void attachInfo(Context context, ProviderInfo info) {
+        public void attachInfoForTesting(Context context, ProviderInfo info) {
             mAuthority = info.authority;
         }
 
diff --git a/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
index cf18155..ea4ef0f 100644
--- a/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
+++ b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
@@ -21,14 +21,21 @@
 import android.test.suitebuilder.annotation.SmallTest;
 
 import java.text.Collator;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Locale;
 
 @SmallTest
 public class ContactLocaleUtilsTest extends AndroidTestCase {
+    private static final String PHONE_NUMBER_1 = "+1 (650) 555-1212";
+    private static final String PHONE_NUMBER_2 = "650-555-1212";
     private static final String LATIN_NAME = "John Smith";
+    private static final String LATIN_NAME_2 = "John Paul Jones";
+    private static final String KANJI_NAME = "\u65e5";
+    private static final String ARABIC_NAME = "\u0646\u0648\u0631"; /* Noor */
     private static final String CHINESE_NAME = "\u675C\u9D51";
     private static final String CHINESE_LATIN_MIX_NAME_1 = "D\u675C\u9D51";
     private static final String CHINESE_LATIN_MIX_NAME_2 = "MARY \u675C\u9D51";
@@ -39,72 +46,223 @@
     private static final String[] CHINESE_LATIN_MIX_NAME_2_KEY = {"\u9D51", "\u675C\u9D51",
         "MARY \u675C\u9D51", "JUAN", "DUJUAN", "MARY DUJUAN", "J", "DJ", "MDJ"};
     private static final String[] LATIN_NAME_KEY = {"John Smith", "Smith", "JS", "S"};
+    private static final String[] LATIN_NAME_KEY_2 = {
+        "John Paul Jones", "Paul Jones", "Jones", "JPJ", "PJ", "J"};
+    private static final String[] LABELS_EN_US = {
+        "", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+        "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+        "#", ""};
+    private static final String[] LABELS_DE = {
+        "", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+        "N", "O", "P", "Q", "R", "S", "Sch", "St", "T", "U", "V", "W", "X",
+        "Y", "Z", "#", ""};
+    private static final String[] LABELS_JA_JP = {
+        "", "\u3042", "\u304B", "\u3055", "\u305F", "\u306A", "\u306F",
+        "\u307E", "\u3084", "\u3089", "\u308F", "\u4ED6",
+        "", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+        "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+        "#", ""};
+    private static final String[] LABELS_ZH_TW = {
+        "", "1\u5283", "2\u5283", "3\u5283", "4\u5283", "5\u5283", "6\u5283",
+        "7\u5283", "8\u5283", "9\u5283", "10\u5283", "11\u5283", "12\u5283",
+        "13\u5283", "14\u5283", "15\u5283", "16\u5283", "17\u5283", "18\u5283",
+        "19\u5283", "20\u5283", "21\u5283", "22\u5283", "23\u5283", "24\u5283",
+        "25\u5283", "26\u5283", "27\u5283", "28\u5283", "29\u5283", "30\u5283",
+        "31\u5283", "32\u5283", "33\u5283",
+        "35\u5283", "36\u5283", "39\u5283", "48\u5283",
+        "", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+        "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+        "#", ""};
+    private static final String[] LABELS_KO = {
+        "", "\u3131", "\u3134", "\u3137", "\u3139", "\u3141", "\u3142",
+        "\u3145", "\u3147", "\u3148", "\u314A", "\u314B", "\u314C", "\u314D",
+        "\u314E",
+        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+        "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+        "#", ""};
+    private static final String[] LABELS_AR = {
+        "", "\u0627", "\u0628", "\u062a", "\u062b", "\u062c", "\u062d",
+        "\u062e", "\u062f", "\u0630", "\u0631", "\u0632", "\u0633", "\u0634",
+        "\u0635", "\u0636", "\u0637", "\u0638", "\u0639", "\u063a", "\u0641",
+        "\u0642", "\u0643", "\u0644", "\u0645", "\u0646", "\u0647", "\u0648",
+        "\u064a",
+        "", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+        "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+        "#", ""};
 
+    private static final String JAPANESE_MISC = "\u4ed6";
 
-    private ContactLocaleUtils mContactLocaleUtils = ContactLocaleUtils.getIntance();
+    private static final Locale LOCALE_ARABIC = new Locale("ar");
+    private boolean hasChineseCollator;
+    private boolean hasJapaneseCollator;
+    private boolean hasKoreanCollator;
+    private boolean hasArabicCollator;
+    private boolean hasGermanCollator;
 
-    public void testContactLocaleUtilsBase() throws Exception {
-        assertEquals(mContactLocaleUtils.getSortKey(LATIN_NAME, FullNameStyle.UNDEFINED),
-                LATIN_NAME);
-        assertNull(mContactLocaleUtils.getNameLookupKeys(LATIN_NAME,
-                FullNameStyle.UNDEFINED));
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        final Locale locale[] = Collator.getAvailableLocales();
+        for (int i = 0; i < locale.length; i++) {
+            if (locale[i].equals(Locale.CHINA)) {
+                hasChineseCollator = true;
+            } else if (locale[i].equals(Locale.JAPAN)) {
+                hasJapaneseCollator = true;
+            } else if (locale[i].equals(Locale.KOREA)) {
+                hasKoreanCollator = true;
+            } else if (locale[i].equals(LOCALE_ARABIC)) {
+                hasArabicCollator = true;
+            } else if (locale[i].equals(Locale.GERMANY)) {
+                hasGermanCollator = true;
+            }
+        }
+    }
+
+    private String getLabel(String name) {
+        ContactLocaleUtils utils = ContactLocaleUtils.getInstance();
+        int bucketIndex = utils.getBucketIndex(name);
+        return utils.getBucketLabel(bucketIndex);
+    }
+
+    private Iterator<String> getNameLookupKeys(String name, int nameStyle) {
+        ContactLocaleUtils utils = ContactLocaleUtils.getInstance();
+        return utils.getNameLookupKeys(name, nameStyle);
+    }
+
+    private ArrayList<String> getLabels() {
+        ContactLocaleUtils utils = ContactLocaleUtils.getInstance();
+        return utils.getLabels();
+    }
+
+    public void testEnglishContactLocaleUtils() throws Exception {
+        ContactLocaleUtils.setLocale(Locale.ENGLISH);
+        assertEquals("#", getLabel(PHONE_NUMBER_1));
+        assertEquals("#", getLabel(PHONE_NUMBER_2));
+        assertEquals("J", getLabel(LATIN_NAME));
+        assertEquals("", getLabel(CHINESE_NAME));
+        assertEquals("D", getLabel(CHINESE_LATIN_MIX_NAME_1));
+        assertEquals("B", getLabel("Bob Smith"));
+
+        assertNull(getNameLookupKeys(LATIN_NAME, FullNameStyle.UNDEFINED));
+        verifyLabels(getLabels(), LABELS_EN_US);
+    }
+
+    public void testJapaneseContactLocaleUtils() throws Exception {
+        if (!hasJapaneseCollator) {
+            return;
+        }
+
+        ContactLocaleUtils.setLocale(Locale.JAPAN);
+        assertEquals("#", getLabel(PHONE_NUMBER_1));
+        assertEquals("#", getLabel(PHONE_NUMBER_2));
+        assertEquals(JAPANESE_MISC, getLabel(KANJI_NAME));
+        assertEquals("J", getLabel(LATIN_NAME));
+        assertEquals(JAPANESE_MISC, getLabel(CHINESE_NAME));
+        assertEquals("D", getLabel(CHINESE_LATIN_MIX_NAME_1));
+
+        assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CJK));
+        assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CHINESE));
+
+        // Following two tests are broken with ICU 50
+        verifyLabels(getLabels(), LABELS_JA_JP);
+        assertEquals("B", getLabel("Bob Smith"));
     }
 
     public void testChineseContactLocaleUtils() throws Exception {
-        if (!hasChineseCollator()) {
+        if (!hasChineseCollator) {
             return;
         }
 
-        assertTrue(mContactLocaleUtils.getSortKey(CHINESE_NAME,
-                FullNameStyle.CHINESE).equalsIgnoreCase("DU \u675C JUAN \u9D51"));
+        ContactLocaleUtils.setLocale(Locale.SIMPLIFIED_CHINESE);
+        assertEquals("#", getLabel(PHONE_NUMBER_1));
+        assertEquals("#", getLabel(PHONE_NUMBER_2));
+        assertEquals("J", getLabel(LATIN_NAME));
+        assertEquals("D", getLabel(CHINESE_NAME));
+        assertEquals("D", getLabel(CHINESE_LATIN_MIX_NAME_1));
+        assertEquals("B", getLabel("Bob Smith"));
+        verifyLabels(getLabels(), LABELS_EN_US);
 
-        assertTrue(mContactLocaleUtils.getSortKey(CHINESE_LATIN_MIX_NAME_1,
-                FullNameStyle.CHINESE).equalsIgnoreCase("d DU \u675C JUAN \u9D51"));
+        ContactLocaleUtils.setLocale(Locale.TRADITIONAL_CHINESE);
+        assertEquals("#", getLabel(PHONE_NUMBER_1));
+        assertEquals("#", getLabel(PHONE_NUMBER_2));
+        assertEquals("J", getLabel(LATIN_NAME));
+        assertEquals("7\u5283", getLabel(CHINESE_NAME));
+        assertEquals("D", getLabel(CHINESE_LATIN_MIX_NAME_1));
 
-        assertTrue(mContactLocaleUtils.getSortKey(CHINESE_LATIN_MIX_NAME_2,
-                FullNameStyle.CHINESE).equalsIgnoreCase("mary DU \u675C JUAN \u9D51"));
-
-        Iterator<String> keys = mContactLocaleUtils.getNameLookupKeys(CHINESE_NAME,
+        ContactLocaleUtils.setLocale(Locale.SIMPLIFIED_CHINESE);
+        Iterator<String> keys = getNameLookupKeys(CHINESE_NAME,
                 FullNameStyle.CHINESE);
         verifyKeys(keys, CHINESE_NAME_KEY);
 
-        keys = mContactLocaleUtils.getNameLookupKeys(CHINESE_LATIN_MIX_NAME_1,
-                FullNameStyle.CHINESE);
+        keys = getNameLookupKeys(CHINESE_LATIN_MIX_NAME_1, FullNameStyle.CHINESE);
         verifyKeys(keys, CHINESE_LATIN_MIX_NAME_1_KEY);
 
-        keys = mContactLocaleUtils.getNameLookupKeys(CHINESE_LATIN_MIX_NAME_2,
-                FullNameStyle.CHINESE);
+        keys = getNameLookupKeys(CHINESE_LATIN_MIX_NAME_2, FullNameStyle.CHINESE);
         verifyKeys(keys, CHINESE_LATIN_MIX_NAME_2_KEY);
+
+        // Following test broken with ICU 50
+        ContactLocaleUtils.setLocale(Locale.TRADITIONAL_CHINESE);
+        verifyLabels(getLabels(), LABELS_ZH_TW);
+        assertEquals("B", getLabel("Bob Smith"));
     }
 
     public void testChineseStyleNameWithDifferentLocale() throws Exception {
-        if (!hasChineseCollator()) {
+        if (!hasChineseCollator) {
             return;
         }
-        mContactLocaleUtils.setLocale(Locale.ENGLISH);
-        assertEquals("DU \u675C JUAN \u9D51",
-                mContactLocaleUtils.getSortKey(CHINESE_NAME, FullNameStyle.CHINESE).toUpperCase());
-        assertEquals("DU \u675C JUAN \u9D51",
-                mContactLocaleUtils.getSortKey(CHINESE_NAME, FullNameStyle.CJK).toUpperCase());
-        mContactLocaleUtils.setLocale(Locale.CHINESE);
-        assertEquals("DU \u675C JUAN \u9D51",
-                mContactLocaleUtils.getSortKey(CHINESE_NAME, FullNameStyle.CHINESE).toUpperCase());
-        assertEquals("DU \u675C JUAN \u9D51",
-                mContactLocaleUtils.getSortKey(CHINESE_NAME, FullNameStyle.CJK).toUpperCase());
-        assertEquals(LATIN_NAME, mContactLocaleUtils.getSortKey(LATIN_NAME, FullNameStyle.WESTERN));
 
-        mContactLocaleUtils.setLocale(Locale.ENGLISH);
-        Iterator<String> keys = mContactLocaleUtils.getNameLookupKeys(CHINESE_NAME,
-                FullNameStyle.CHINESE);
+        ContactLocaleUtils.setLocale(Locale.ENGLISH);
+        assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CHINESE));
+        assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CJK));
+
+        ContactLocaleUtils.setLocale(Locale.CHINA);
+        Iterator<String> keys = getNameLookupKeys(CHINESE_NAME,
+                FullNameStyle.CJK);
         verifyKeys(keys, CHINESE_NAME_KEY);
-        keys = mContactLocaleUtils.getNameLookupKeys(CHINESE_NAME, FullNameStyle.CJK);
-        verifyKeys(keys, CHINESE_NAME_KEY);
-        mContactLocaleUtils.setLocale(Locale.CHINESE);
-        keys = mContactLocaleUtils.getNameLookupKeys(CHINESE_NAME, FullNameStyle.CJK);
-        verifyKeys(keys, CHINESE_NAME_KEY);
-        keys = mContactLocaleUtils.getNameLookupKeys(LATIN_NAME, FullNameStyle.WESTERN);
+        keys = getNameLookupKeys(LATIN_NAME, FullNameStyle.WESTERN);
         verifyKeys(keys, LATIN_NAME_KEY);
+        keys = getNameLookupKeys(LATIN_NAME_2, FullNameStyle.WESTERN);
+        verifyKeys(keys, LATIN_NAME_KEY_2);
 
+        ContactLocaleUtils.setLocale(Locale.TRADITIONAL_CHINESE);
+        assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CJK));
+    }
+
+    public void testKoreanContactLocaleUtils() throws Exception {
+        if (!hasKoreanCollator) {
+            return;
+        }
+
+        ContactLocaleUtils.setLocale(Locale.KOREA);
+        assertEquals("\u3131", getLabel("\u1100"));
+        assertEquals("\u3131", getLabel("\u3131"));
+        assertEquals("\u3131", getLabel("\u1101"));
+        assertEquals("\u314e", getLabel("\u1161"));
+        assertEquals("B", getLabel("Bob Smith"));
+        verifyLabels(getLabels(), LABELS_KO);
+    }
+
+    public void testArabicContactLocaleUtils() throws Exception {
+        if (!hasArabicCollator) {
+            return;
+        }
+
+        ContactLocaleUtils.setLocale(LOCALE_ARABIC);
+        assertEquals("\u0646", getLabel(ARABIC_NAME));
+        assertEquals("B", getLabel("Bob Smith"));
+        verifyLabels(getLabels(), LABELS_AR);
+    }
+
+    public void testGermanContactLocaleUtils() throws Exception {
+        if (!hasGermanCollator) {
+            return;
+        }
+
+        ContactLocaleUtils.setLocale(Locale.GERMANY);
+        assertEquals("S", getLabel("Sacher"));
+        assertEquals("Sch", getLabel("Schiller"));
+        assertEquals("St", getLabel("Steiff"));
+        verifyLabels(getLabels(), LABELS_DE);
     }
 
     private void verifyKeys(final Iterator<String> resultKeys, final String[] expectedKeys)
@@ -113,16 +271,17 @@
         while (resultKeys.hasNext()) {
             allKeys.add(resultKeys.next());
         }
-        assertEquals(allKeys, new HashSet<String>(Arrays.asList(expectedKeys)));
+        assertEquals(new HashSet<String>(Arrays.asList(expectedKeys)), allKeys);
     }
 
-    private boolean hasChineseCollator() {
-        final Locale locale[] = Collator.getAvailableLocales();
-        for (int i = 0; i < locale.length; i++) {
-            if (locale[i].equals(Locale.CHINA)) {
-                return true;
-            }
-        }
-        return false;
+    // Verify that the initial set of resultLabels matches the expectedLabels.
+    // Ignore the (large) number of secondary locale labels that make up the
+    // tail labels in the result set right before the final "#" and "" buckets.
+    private void verifyLabels(final ArrayList<String> resultLabels,
+            final String[] expectedLabels) throws Exception {
+        final List<String> expectedLabelList = Arrays.asList(expectedLabels);
+        final int numLabels = expectedLabelList.size() - 2;
+        assertEquals(expectedLabelList.subList(0, numLabels),
+                resultLabels.subList(0, numLabels));
     }
 }
diff --git a/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java b/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
index 08f3a07..047e8ea 100644
--- a/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
+++ b/tests/src/com/android/providers/contacts/ContactLookupKeyTest.java
@@ -24,6 +24,7 @@
 import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
+import com.android.providers.contacts.testutil.RawContactUtil;
 
 import java.util.ArrayList;
 
@@ -40,8 +41,8 @@
 public class ContactLookupKeyTest extends BaseContactsProvider2Test {
 
     public void testLookupKeyUsingDisplayNameAndNoAccount() {
-        long rawContactId1 = createRawContactWithName("John", "Doe");
-        long rawContactId2 = createRawContactWithName("johndoe", null);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "johndoe", null);
         setAggregationException(
                 AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
 
@@ -68,13 +69,13 @@
     }
 
     public void testLookupKeyUsingSourceIdAndNoAccount() {
-        long rawContactId1 = createRawContactWithName("John", "Doe");
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
         storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.SOURCE_ID, "123");
 
-        long rawContactId2 = createRawContactWithName("johndoe", null);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "johndoe", null);
         storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.SOURCE_ID, "4.5.6");
 
-        long rawContactId3 = createRawContactWithName("john", "dough");
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "john", "dough");
         storeValue(RawContacts.CONTENT_URI, rawContactId3, RawContacts.SOURCE_ID, "http://foo?bar");
 
         setAggregationException(
@@ -94,12 +95,12 @@
     }
 
     public void testLookupKeySameSourceIdDifferentAccounts() {
-        long rawContactId1 = createRawContactWithName("Dear", "Doe");
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "Dear", "Doe");
         storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.ACCOUNT_TYPE, "foo");
         storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.ACCOUNT_NAME, "FOO");
         storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.SOURCE_ID, "1");
 
-        long rawContactId2 = createRawContactWithName("Deer", "Dough");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "Deer", "Dough");
         storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.ACCOUNT_TYPE, "bar");
         storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.ACCOUNT_NAME, "BAR");
         storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.SOURCE_ID, "1");
@@ -125,13 +126,13 @@
     }
 
     public void testLookupKeyChoosingLargestContact() {
-        long rawContactId1 = createRawContactWithName("John", "Doe");
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
         storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.SOURCE_ID, "1");
 
-        long rawContactId2 = createRawContactWithName("John", "Doe");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
         storeValue(RawContacts.CONTENT_URI, rawContactId2, RawContacts.SOURCE_ID, "2");
 
-        long rawContactId3 = createRawContactWithName("John", "Doe");
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
         storeValue(RawContacts.CONTENT_URI, rawContactId3, RawContacts.SOURCE_ID, "3");
         setAggregationException(
                 AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
@@ -165,7 +166,7 @@
     }
 
     public void testGetLookupUri() {
-        long rawContactId1 = createRawContactWithName("John", "Doe");
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
         storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.SOURCE_ID, "1");
 
         long contactId = queryContactId(rawContactId1);
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 7a30244..8da2800 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -199,7 +199,7 @@
         ContentProvider provider = providerClass.newInstance();
         ProviderInfo info = new ProviderInfo();
         info.authority = authority;
-        provider.attachInfo(mProviderContext, info);
+        provider.attachInfoForTesting(mProviderContext, info);
         resolver.addProvider(authority, provider);
         return provider;
     }
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index 1011ae2..58086e2 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -21,6 +21,7 @@
 import android.accounts.Account;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Entity;
@@ -32,6 +33,7 @@
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.CommonDataKinds.Callable;
+import android.provider.ContactsContract.CommonDataKinds.Contactables;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Im;
@@ -67,12 +69,20 @@
 import android.text.TextUtils;
 
 import com.android.internal.util.ArrayUtils;
-import com.android.providers.contacts.ContactsDatabaseHelper;
 import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.testutil.CommonDatabaseUtils;
+import com.android.providers.contacts.testutil.ContactUtil;
+import com.android.providers.contacts.testutil.DataUtil;
+import com.android.providers.contacts.testutil.DatabaseAsserts;
+import com.android.providers.contacts.testutil.DeletedContactUtil;
+import com.android.providers.contacts.testutil.RawContactUtil;
+import com.android.providers.contacts.testutil.TestUtil;
 import com.android.providers.contacts.tests.R;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Sets;
@@ -83,6 +93,7 @@
 import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
@@ -99,8 +110,7 @@
 @LargeTest
 public class ContactsProvider2Test extends BaseContactsProvider2Test {
 
-    private static final Account ACCOUNT_1 = new Account("account_name_1", "account_type_1");
-    private static final Account ACCOUNT_2 = new Account("account_name_2", "account_type_2");
+    private static final String TAG = ContactsProvider2Test.class.getSimpleName();
 
     public void testContactsProjection() {
         assertProjection(Contacts.CONTENT_URI, new String[]{
@@ -112,6 +122,10 @@
                 Contacts.PHONETIC_NAME_STYLE,
                 Contacts.SORT_KEY_PRIMARY,
                 Contacts.SORT_KEY_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_LABEL_PRIMARY,
+                ContactsColumns.PHONEBOOK_BUCKET_PRIMARY,
+                ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE,
                 Contacts.LAST_TIME_CONTACTED,
                 Contacts.TIMES_CONTACTED,
                 Contacts.STARRED,
@@ -133,6 +147,7 @@
                 Contacts.CONTACT_STATUS_RES_PACKAGE,
                 Contacts.CONTACT_STATUS_LABEL,
                 Contacts.CONTACT_STATUS_ICON,
+                Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
         });
     }
 
@@ -146,6 +161,10 @@
                 Contacts.PHONETIC_NAME_STYLE,
                 Contacts.SORT_KEY_PRIMARY,
                 Contacts.SORT_KEY_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_LABEL_PRIMARY,
+                ContactsColumns.PHONEBOOK_BUCKET_PRIMARY,
+                ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE,
                 Contacts.LAST_TIME_CONTACTED,
                 Contacts.TIMES_CONTACTED,
                 Contacts.STARRED,
@@ -167,6 +186,7 @@
                 Contacts.CONTACT_STATUS_RES_PACKAGE,
                 Contacts.CONTACT_STATUS_LABEL,
                 Contacts.CONTACT_STATUS_ICON,
+                Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
                 DataUsageStatColumns.TIMES_USED,
                 DataUsageStatColumns.LAST_TIME_USED,
         });
@@ -184,6 +204,10 @@
                 Contacts.PHONETIC_NAME_STYLE,
                 Contacts.SORT_KEY_PRIMARY,
                 Contacts.SORT_KEY_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_LABEL_PRIMARY,
+                ContactsColumns.PHONEBOOK_BUCKET_PRIMARY,
+                ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE,
                 Contacts.LAST_TIME_CONTACTED,
                 Contacts.TIMES_CONTACTED,
                 Contacts.STARRED,
@@ -205,6 +229,7 @@
                 Contacts.CONTACT_STATUS_RES_PACKAGE,
                 Contacts.CONTACT_STATUS_LABEL,
                 Contacts.CONTACT_STATUS_ICON,
+                Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
                 DataUsageStatColumns.TIMES_USED,
                 DataUsageStatColumns.LAST_TIME_USED,
                 Phone.NUMBER,
@@ -224,6 +249,10 @@
                 Contacts.PHONETIC_NAME_STYLE,
                 Contacts.SORT_KEY_PRIMARY,
                 Contacts.SORT_KEY_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_LABEL_PRIMARY,
+                ContactsColumns.PHONEBOOK_BUCKET_PRIMARY,
+                ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE,
                 Contacts.LAST_TIME_CONTACTED,
                 Contacts.TIMES_CONTACTED,
                 Contacts.STARRED,
@@ -245,7 +274,7 @@
                 Contacts.CONTACT_STATUS_RES_PACKAGE,
                 Contacts.CONTACT_STATUS_LABEL,
                 Contacts.CONTACT_STATUS_ICON,
-
+                Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
                 SearchSnippetColumns.SNIPPET,
         });
     }
@@ -271,6 +300,10 @@
                 RawContacts.NAME_VERIFIED,
                 RawContacts.SORT_KEY_PRIMARY,
                 RawContacts.SORT_KEY_ALTERNATIVE,
+                RawContactsColumns.PHONEBOOK_LABEL_PRIMARY,
+                RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY,
+                RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE,
+                RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE,
                 RawContacts.TIMES_CONTACTED,
                 RawContacts.LAST_TIME_CONTACTED,
                 RawContacts.CUSTOM_RINGTONE,
@@ -320,6 +353,8 @@
                 Data.STATUS_RES_PACKAGE,
                 Data.STATUS_LABEL,
                 Data.STATUS_ICON,
+                Data.TIMES_USED,
+                Data.LAST_TIME_USED,
                 RawContacts.ACCOUNT_NAME,
                 RawContacts.ACCOUNT_TYPE,
                 RawContacts.DATA_SET,
@@ -337,6 +372,10 @@
                 Contacts.PHONETIC_NAME_STYLE,
                 Contacts.SORT_KEY_PRIMARY,
                 Contacts.SORT_KEY_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_LABEL_PRIMARY,
+                ContactsColumns.PHONEBOOK_BUCKET_PRIMARY,
+                ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE,
                 Contacts.LAST_TIME_CONTACTED,
                 Contacts.TIMES_CONTACTED,
                 Contacts.STARRED,
@@ -357,6 +396,7 @@
                 Contacts.CONTACT_STATUS_RES_PACKAGE,
                 Contacts.CONTACT_STATUS_LABEL,
                 Contacts.CONTACT_STATUS_ICON,
+                Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
                 GroupMembership.GROUP_SOURCE_ID,
         });
     }
@@ -397,6 +437,8 @@
                 Data.STATUS_RES_PACKAGE,
                 Data.STATUS_LABEL,
                 Data.STATUS_ICON,
+                Data.TIMES_USED,
+                Data.LAST_TIME_USED,
                 RawContacts.RAW_CONTACT_IS_USER_PROFILE,
                 Contacts._ID,
                 Contacts.DISPLAY_NAME_PRIMARY,
@@ -406,6 +448,10 @@
                 Contacts.PHONETIC_NAME_STYLE,
                 Contacts.SORT_KEY_PRIMARY,
                 Contacts.SORT_KEY_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_LABEL_PRIMARY,
+                ContactsColumns.PHONEBOOK_BUCKET_PRIMARY,
+                ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE,
                 Contacts.LAST_TIME_CONTACTED,
                 Contacts.TIMES_CONTACTED,
                 Contacts.STARRED,
@@ -425,6 +471,7 @@
                 Contacts.CONTACT_STATUS_RES_PACKAGE,
                 Contacts.CONTACT_STATUS_LABEL,
                 Contacts.CONTACT_STATUS_ICON,
+                Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
                 GroupMembership.GROUP_SOURCE_ID,
         });
     }
@@ -490,6 +537,10 @@
                 Contacts.PHONETIC_NAME_STYLE,
                 Contacts.SORT_KEY_PRIMARY,
                 Contacts.SORT_KEY_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_LABEL_PRIMARY,
+                ContactsColumns.PHONEBOOK_BUCKET_PRIMARY,
+                ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE,
+                ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE,
                 Contacts.LAST_TIME_CONTACTED,
                 Contacts.TIMES_CONTACTED,
                 Contacts.STARRED,
@@ -511,6 +562,7 @@
                 Contacts.CONTACT_STATUS_RES_PACKAGE,
                 Contacts.CONTACT_STATUS_LABEL,
                 Contacts.CONTACT_STATUS_ICON,
+                Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
                 GroupMembership.GROUP_SOURCE_ID,
         });
     }
@@ -731,7 +783,7 @@
     public void testDataDirectoryWithLookupUri() {
         ContentValues values = new ContentValues();
 
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         insertPhoneNumber(rawContactId, "555-GOOG-411");
         insertEmail(rawContactId, "google@android.com");
 
@@ -778,12 +830,12 @@
         Account account1 = new Account("act1", "actype1");
         Account account2 = new Account("act2", "actype2");
 
-        long rawContactId1 = createRawContactWithName(account1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, account1);
         insertImHandle(rawContactId1, Im.PROTOCOL_GOOGLE_TALK, null, "gtalk");
         insertStatusUpdate(Im.PROTOCOL_GOOGLE_TALK, null, "gtalk", StatusUpdates.IDLE, "Busy", 90,
                 StatusUpdates.CAPABILITY_HAS_CAMERA, false);
 
-        long rawContactId2 = createRawContact(account2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, account2);
         setAggregationException(
                 AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
 
@@ -800,12 +852,12 @@
         Account account1 = new Account("act1", "actype1");
         Account account2 = new Account("act2", "actype2");
 
-        long rawContactId1 = createRawContactWithName(account1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, account1);
         insertImHandle(rawContactId1, Im.PROTOCOL_GOOGLE_TALK, null, "gtalk");
         insertStatusUpdate(Im.PROTOCOL_GOOGLE_TALK, null, "gtalk", StatusUpdates.IDLE, "Busy", 90,
                 StatusUpdates.CAPABILITY_HAS_CAMERA, false);
 
-        long rawContactId2 = createRawContact(account2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, account2);
         setAggregationException(
                 AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
 
@@ -892,7 +944,7 @@
     }
 
     public void testDataInsert() {
-        long rawContactId = createRawContactWithName("John", "Doe");
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
 
         ContentValues values = new ContentValues();
         putDataValues(values, rawContactId);
@@ -921,13 +973,13 @@
     public void testRawContactDataQuery() {
         Account account1 = new Account("a", "b");
         Account account2 = new Account("c", "d");
-        long rawContactId1 = createRawContact(account1);
-        Uri dataUri1 = insertStructuredName(rawContactId1, "John", "Doe");
-        long rawContactId2 = createRawContact(account2);
-        Uri dataUri2 = insertStructuredName(rawContactId2, "Jane", "Doe");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, account1);
+        Uri dataUri1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "John", "Doe");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, account2);
+        Uri dataUri2 = DataUtil.insertStructuredName(mResolver, rawContactId2, "Jane", "Doe");
 
-        Uri uri1 = maybeAddAccountQueryParameters(dataUri1, account1);
-        Uri uri2 = maybeAddAccountQueryParameters(dataUri2, account2);
+        Uri uri1 = TestUtil.maybeAddAccountQueryParameters(dataUri1, account1);
+        Uri uri2 = TestUtil.maybeAddAccountQueryParameters(dataUri2, account2);
         assertStoredValue(uri1, Data._ID, ContentUris.parseId(dataUri1)) ;
         assertStoredValue(uri2, Data._ID, ContentUris.parseId(dataUri2)) ;
     }
@@ -944,7 +996,7 @@
         Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         long rawContactId = ContentUris.parseId(rawContactUri);
 
-        insertStructuredName(rawContactId, "Meghan", "Knox");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Meghan", "Knox");
         Uri uri = insertPhoneNumber(rawContactId, "18004664411");
         long phoneId = ContentUris.parseId(uri);
 
@@ -970,10 +1022,10 @@
     }
 
     public void testPhonesWithMergedContacts() {
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         insertPhoneNumber(rawContactId1, "123456789", true);
 
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         insertPhoneNumber(rawContactId2, "123456789", true);
 
         setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
@@ -1009,7 +1061,7 @@
     }
 
     public void testPhonesNormalizedNumber() {
-        final long rawContactId = createRawContact();
+        final long rawContactId = RawContactUtil.createRawContact(mResolver);
 
         // Write both a number and a normalized number. Those should be written as-is
         final ContentValues values = new ContentValues();
@@ -1129,10 +1181,12 @@
                 Phone.CONTENT_FILTER_URI.equals(baseFilterUri)
                         || Callable.CONTENT_FILTER_URI.equals(baseFilterUri));
 
-        final long rawContactId1 = createRawContactWithName("Hot", "Tamale", ACCOUNT_1);
+        final long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "Hot",
+                "Tamale", TestUtil.ACCOUNT_1);
         insertPhoneNumber(rawContactId1, "1-800-466-4411");
 
-        final long rawContactId2 = createRawContactWithName("Chilled", "Guacamole", ACCOUNT_2);
+        final long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "Chilled",
+                "Guacamole", TestUtil.ACCOUNT_2);
         insertPhoneNumber(rawContactId2, "1-800-466-5432");
         insertPhoneNumber(rawContactId2, "0@example.com", false, Phone.TYPE_PAGER);
         insertPhoneNumber(rawContactId2, "1@example.com", false, Phone.TYPE_PAGER);
@@ -1212,17 +1266,16 @@
         // Sanity test. Run tests for "Chilled Guacamole" again and see nothing changes
         // after the Sip address being inserted.
         assertStoredValues(filterUri2, values);
-        assertStoredValues(filterUri3, values);
         assertEquals(0, getCount(filterUri4, null, null));
         assertEquals(0, getCount(filterUri5, null, null));
         assertStoredValues(filterUri6, new ContentValues[] {values1, values2, values3} );
     }
 
     public void testPhonesFilterSearchParams() {
-        final long rid1 = createRawContactWithName("Dad", null);
+        final long rid1 = RawContactUtil.createRawContactWithName(mResolver, "Dad", null);
         insertPhoneNumber(rid1, "123-456-7890");
 
-        final long rid2 = createRawContactWithName("Mam", null);
+        final long rid2 = RawContactUtil.createRawContactWithName(mResolver, "Mam", null);
         insertPhoneNumber(rid2, "323-123-4567");
 
         // By default, "dad" will match both the display name and the phone number.
@@ -1261,7 +1314,7 @@
         Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         long rawContactId = ContentUris.parseId(rawContactUri);
 
-        insertStructuredName(rawContactId, "Hot", "Tamale");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Hot", "Tamale");
         insertPhoneNumber(rawContactId, "18004664411");
 
         // We'll create two lookup records, 18004664411 and +18004664411, and the below lookup
@@ -1302,7 +1355,7 @@
         rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         rawContactId = ContentUris.parseId(rawContactUri);
 
-        insertStructuredName(rawContactId, "Hot", "Tamale");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Hot", "Tamale");
         insertPhoneNumber(rawContactId, "+1-650-861-0000");
 
         values.clear();
@@ -1334,7 +1387,7 @@
         rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         rawContactId = ContentUris.parseId(rawContactUri);
 
-        insertStructuredName(rawContactId, "Hot1", "Tamale");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Hot1", "Tamale");
         insertPhoneNumber(rawContactId, "650-861-0001");
 
         values.clear();
@@ -1354,7 +1407,7 @@
         rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         rawContactId = ContentUris.parseId(rawContactUri);
 
-        insertStructuredName(rawContactId, "Hot2", "Tamale");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Hot2", "Tamale");
         insertPhoneNumber(rawContactId, "861-0002");
 
         values.clear();
@@ -1377,7 +1430,7 @@
         values.put(RawContacts.CUSTOM_RINGTONE, "d");
         values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
         long rawContactId = ContentUris.parseId(mResolver.insert(RawContacts.CONTENT_URI, values));
-        insertStructuredName(rawContactId, "Senor", "Chang");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Senor", "Chang");
         insertPhoneNumber(rawContactId, fullNumber);
 
         // Full number should definitely match.
@@ -1413,7 +1466,7 @@
         values.put(RawContacts.CUSTOM_RINGTONE, "d");
         values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
         long rawContactId = ContentUris.parseId(mResolver.insert(RawContacts.CONTENT_URI, values));
-        insertStructuredName(rawContactId, "Senor", "Chang");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Senor", "Chang");
         insertPhoneNumber(rawContactId, storedNumber);
 
         assertEquals(1, getCount(Uri.withAppendedPath(
@@ -1449,7 +1502,7 @@
             values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
             long rawContactId = ContentUris.parseId(
                     mResolver.insert(RawContacts.CONTENT_URI, values));
-            insertStructuredName(rawContactId, "Senor", "Chang");
+            DataUtil.insertStructuredName(mResolver, rawContactId, "Senor", "Chang");
             insertPhoneNumber(rawContactId, fullNumber);
             insertPhoneNumber(rawContactId, "5103337596");
             insertPhoneNumber(rawContactId, "+19012345678");
@@ -1494,7 +1547,7 @@
         Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         long rawContactId = ContentUris.parseId(rawContactUri);
 
-        insertStructuredName(rawContactId, "Hot", "Tamale");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Hot", "Tamale");
         Uri phoneUri = insertPhoneNumber(rawContactId, "18004664411");
 
         Uri lookupUri1 = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, "8004664411");
@@ -1528,11 +1581,11 @@
 
     /** Tests if {@link Callable#CONTENT_URI} returns both phones and sip addresses. */
     public void testCallablesQuery() {
-        long rawContactId1 = createRawContactWithName("Meghan", "Knox");
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "Meghan", "Knox");
         long phoneId1 = ContentUris.parseId(insertPhoneNumber(rawContactId1, "18004664411"));
         long contactId1 = queryContactId(rawContactId1);
 
-        long rawContactId2 = createRawContactWithName("John", "Doe");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
         long sipAddressId2 = ContentUris.parseId(
                 insertSipAddress(rawContactId2, "sip@example.com"));
         long contactId2 = queryContactId(rawContactId2);
@@ -1574,7 +1627,7 @@
         Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         final long rawContactId = ContentUris.parseId(rawContactUri);
 
-        insertStructuredName(rawContactId, "Meghan", "Knox");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Meghan", "Knox");
         final Uri emailUri = insertEmail(rawContactId, "meghan@acme.com");
         final long emailId = ContentUris.parseId(emailUri);
 
@@ -1623,7 +1676,7 @@
     }
 
     public void testEmailsLookupQuery() {
-        long rawContactId = createRawContactWithName("Hot", "Tamale");
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
         insertEmail(rawContactId, "tamale@acme.com");
 
         Uri filterUri1 = Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, "tamale@acme.com");
@@ -1643,11 +1696,13 @@
     }
 
     public void testEmailsFilterQuery() {
-        long rawContactId1 = createRawContactWithName("Hot", "Tamale", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale",
+                TestUtil.ACCOUNT_1);
         insertEmail(rawContactId1, "tamale@acme.com");
         insertEmail(rawContactId1, "tamale@acme.com");
 
-        long rawContactId2 = createRawContactWithName("Hot", "Tamale", ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale",
+                TestUtil.ACCOUNT_2);
         insertEmail(rawContactId2, "tamale@acme.com");
 
         Uri filterUri1 = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "tam");
@@ -1676,7 +1731,7 @@
      * Tests if ContactsProvider2 returns addresses according to registration order.
      */
     public void testEmailFilterDefaultSortOrder() {
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         insertEmail(rawContactId1, "address1@email.com");
         insertEmail(rawContactId1, "address2@email.com");
         insertEmail(rawContactId1, "address3@email.com");
@@ -1695,7 +1750,7 @@
      * Tests if ContactsProvider2 returns primary addresses before the other addresses.
      */
     public void testEmailFilterPrimaryAddress() {
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         insertEmail(rawContactId1, "address1@email.com");
         insertEmail(rawContactId1, "address2@email.com", true);
         ContentValues v1 = new ContentValues();
@@ -1712,9 +1767,9 @@
      * other address.
      */
     public void testEmailFilterPrimaryAccount() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, TestUtil.ACCOUNT_1);
         insertEmail(rawContactId1, "account1@email.com");
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, TestUtil.ACCOUNT_2);
         insertEmail(rawContactId2, "account2@email.com");
         ContentValues v1 = new ContentValues();
         v1.put(Email.ADDRESS, "account1@email.com");
@@ -1722,25 +1777,25 @@
         v2.put(Email.ADDRESS, "account2@email.com");
 
         Uri filterUri1 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("acc")
-                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, ACCOUNT_1.name)
-                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE, ACCOUNT_1.type)
+                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, TestUtil.ACCOUNT_1.name)
+                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE, TestUtil.ACCOUNT_1.type)
                 .build();
         assertStoredValuesOrderly(filterUri1, new ContentValues[] { v1, v2 });
 
         Uri filterUri2 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("acc")
-                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, ACCOUNT_2.name)
-                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE, ACCOUNT_2.type)
+                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, TestUtil.ACCOUNT_2.name)
+                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE, TestUtil.ACCOUNT_2.type)
                 .build();
         assertStoredValuesOrderly(filterUri2, new ContentValues[] { v2, v1 });
 
         // Just with PRIMARY_ACCOUNT_NAME
         Uri filterUri3 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("acc")
-                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, ACCOUNT_1.name)
+                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, TestUtil.ACCOUNT_1.name)
                 .build();
         assertStoredValuesOrderly(filterUri3, new ContentValues[]{v1, v2});
 
         Uri filterUri4 = Email.CONTENT_FILTER_URI.buildUpon().appendPath("acc")
-                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, ACCOUNT_2.name)
+                .appendQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME, TestUtil.ACCOUNT_2.name)
                 .build();
         assertStoredValuesOrderly(filterUri4, new ContentValues[] { v2, v1 });
     }
@@ -1750,7 +1805,7 @@
      */
     public void testEmailFilterSameDomainAccountOrder() {
         final Account account = new Account("tester@email.com", "not_used");
-        final long rawContactId = createRawContact(account);
+        final long rawContactId = RawContactUtil.createRawContact(mResolver, account);
         insertEmail(rawContactId, "account1@testemail.com");
         insertEmail(rawContactId, "account1@email.com");
 
@@ -1767,11 +1822,11 @@
     /**
      * Test "default" emails are sorted above emails used last.
      */
-    public void testEmailFilterDefaultOverUsageSort() {
-        final long rawContactId = createRawContact(ACCOUNT_1);
+    public void testEmailFilterSuperPrimaryOverUsageSort() {
+        final long rawContactId = RawContactUtil.createRawContact(mResolver, TestUtil.ACCOUNT_1);
         final Uri emailUri1 = insertEmail(rawContactId, "account1@testemail.com");
         final Uri emailUri2 = insertEmail(rawContactId, "account2@testemail.com");
-        insertEmail(rawContactId, "account3@testemail.com", true);
+        insertEmail(rawContactId, "account3@testemail.com", true, true);
 
         // Update account1 and account 2 to have higher usage.
         updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, emailUri1);
@@ -1787,13 +1842,39 @@
         assertStoredValuesOrderly(filterUri, v3, v1, v2);
     }
 
+    /**
+     * Test primary emails are sorted below emails used last.
+     *
+     * primary may be set without super primary.  Only super primary indicates "default" in the
+     * contact ui.
+     */
+    public void testEmailFilterUsageOverPrimarySort() {
+        final long rawContactId = RawContactUtil.createRawContact(mResolver, TestUtil.ACCOUNT_1);
+        final Uri emailUri1 = insertEmail(rawContactId, "account1@testemail.com");
+        final Uri emailUri2 = insertEmail(rawContactId, "account2@testemail.com");
+        insertEmail(rawContactId, "account3@testemail.com", true);
+
+        // Update account1 and account 2 to have higher usage.
+        updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, emailUri1);
+        updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, emailUri1);
+        updateDataUsageFeedback(DataUsageFeedback.USAGE_TYPE_LONG_TEXT, emailUri2);
+
+        final ContentValues v1 = cv(Email.ADDRESS, "account1@testemail.com");
+        final ContentValues v2 = cv(Email.ADDRESS, "account2@testemail.com");
+        final ContentValues v3 = cv(Email.ADDRESS, "account3@testemail.com");
+
+        // Test that account 3 is first even though account 1 and 2 have higher usage.
+        Uri filterUri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, "acc");
+        assertStoredValuesOrderly(filterUri, v1, v2, v3);
+    }
+
     /** Tests {@link DataUsageFeedback} correctly promotes a data row instead of a raw contact. */
     public void testEmailFilterSortOrderWithFeedback() {
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         String address1 = "address1@email.com";
         insertEmail(rawContactId1, address1);
 
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         String address2 = "address2@email.com";
         insertEmail(rawContactId2, address2);
         String address3 = "address3@email.com";
@@ -1845,7 +1926,7 @@
      * {@link DataUsageStatColumns#LAST_TIME_USED}
      */
     public void testEmailFilterSortOrderWithOldHistory() {
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         long dataId1 = ContentUris.parseId(insertEmail(rawContactId1, "address1@email.com"));
         long dataId2 = ContentUris.parseId(insertEmail(rawContactId1, "address2@email.com"));
         long dataId3 = ContentUris.parseId(insertEmail(rawContactId1, "address3@email.com"));
@@ -1906,7 +1987,7 @@
     }
 
     public void testPostalsQuery() {
-        long rawContactId = createRawContactWithName("Alice", "Nextore");
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Alice", "Nextore");
         Uri dataUri = insertPostalAddress(rawContactId, "1600 Amphiteatre Ave, Mountain View");
         final long dataId = ContentUris.parseId(dataUri);
 
@@ -1950,6 +2031,162 @@
         assertStoredValues(dedupeUri, values);
     }
 
+    public void testDataContentUriInvisibleQuery() {
+        final ContentValues values = new ContentValues();
+        final long contactId = createContact(values, "John", "Doe",
+                "18004664411", "goog411@acme.com", StatusUpdates.INVISIBLE, 4, 1, 0,
+                        StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO);
+
+        final Uri uri = Data.CONTENT_URI.buildUpon().
+                appendQueryParameter(Data.VISIBLE_CONTACTS_ONLY, "true").build();
+        assertEquals(4, getCount(uri, null, null));
+
+        markInvisible(contactId);
+
+        assertEquals(0, getCount(uri, null, null));
+    }
+
+    public void testContactablesQuery() {
+        final long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot",
+                "Tamale");
+
+        insertPhoneNumber(rawContactId, "510-123-5769");
+        insertEmail(rawContactId, "tamale@acme.com");
+
+        final ContentValues cv1 = new ContentValues();
+        cv1.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+        cv1.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        cv1.put(Email.DATA, "tamale@acme.com");
+        cv1.put(Email.TYPE, Email.TYPE_HOME);
+        cv1.putNull(Email.LABEL);
+
+        final ContentValues cv2 = new ContentValues();
+        cv2.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+        cv2.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        cv2.put(Phone.DATA, "510-123-5769");
+        cv2.put(Phone.TYPE, Phone.TYPE_HOME);
+        cv2.putNull(Phone.LABEL);
+
+        final Uri filterUri0 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "");
+        assertEquals(0, getCount(filterUri0, null, null));
+
+        final Uri filterUri1 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "tamale");
+        assertStoredValues(filterUri1, cv1, cv2);
+
+        final Uri filterUri2 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "hot");
+        assertStoredValues(filterUri2, cv1, cv2);
+
+        final Uri filterUri3 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "tamale@ac");
+        assertStoredValues(filterUri3, cv1, cv2);
+
+        final Uri filterUri4 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "510");
+        assertStoredValues(filterUri4, cv1, cv2);
+
+        final Uri filterUri5 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "cold");
+        assertEquals(0, getCount(filterUri5, null, null));
+
+        final Uri filterUri6 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI,
+                "tamale@google");
+        assertEquals(0, getCount(filterUri6, null, null));
+
+        final Uri filterUri7 = Contactables.CONTENT_URI;
+        assertStoredValues(filterUri7, cv1, cv2);
+    }
+
+    public void testContactablesMultipleQuery() {
+
+        final long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot",
+                "Tamale");
+        insertPhoneNumber(rawContactId, "510-123-5769");
+        insertEmail(rawContactId, "tamale@acme.com");
+        insertEmail(rawContactId, "hot@google.com");
+
+        final long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "Cold",
+                "Tamago");
+        insertEmail(rawContactId2, "eggs@farmers.org");
+
+        final long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
+        insertPhoneNumber(rawContactId3, "518-354-1111");
+        insertEmail(rawContactId3, "doeadeer@afemaledeer.com");
+
+        final ContentValues cv1 = new ContentValues();
+        cv1.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+        cv1.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        cv1.put(Email.DATA, "tamale@acme.com");
+        cv1.put(Email.TYPE, Email.TYPE_HOME);
+        cv1.putNull(Email.LABEL);
+
+        final ContentValues cv2 = new ContentValues();
+        cv2.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+        cv2.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        cv2.put(Phone.DATA, "510-123-5769");
+        cv2.put(Phone.TYPE, Phone.TYPE_HOME);
+        cv2.putNull(Phone.LABEL);
+
+        final ContentValues cv3 = new ContentValues();
+        cv3.put(Contacts.DISPLAY_NAME, "Hot Tamale");
+        cv3.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        cv3.put(Email.DATA, "hot@google.com");
+        cv3.put(Email.TYPE, Email.TYPE_HOME);
+        cv3.putNull(Email.LABEL);
+
+        final ContentValues cv4 = new ContentValues();
+        cv4.put(Contacts.DISPLAY_NAME, "Cold Tamago");
+        cv4.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        cv4.put(Email.DATA, "eggs@farmers.org");
+        cv4.put(Email.TYPE, Email.TYPE_HOME);
+        cv4.putNull(Email.LABEL);
+
+        final ContentValues cv5 = new ContentValues();
+        cv5.put(Contacts.DISPLAY_NAME, "John Doe");
+        cv5.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+        cv5.put(Email.DATA, "doeadeer@afemaledeer.com");
+        cv5.put(Email.TYPE, Email.TYPE_HOME);
+        cv5.putNull(Email.LABEL);
+
+        final ContentValues cv6 = new ContentValues();
+        cv6.put(Contacts.DISPLAY_NAME, "John Doe");
+        cv6.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+        cv6.put(Phone.DATA, "518-354-1111");
+        cv6.put(Phone.TYPE, Phone.TYPE_HOME);
+        cv6.putNull(Phone.LABEL);
+
+        final Uri filterUri1 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "tamale");
+
+        assertStoredValues(filterUri1, cv1, cv2, cv3);
+
+        final Uri filterUri2 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "hot");
+        assertStoredValues(filterUri2, cv1, cv2, cv3);
+
+        final Uri filterUri3 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "tam");
+        assertStoredValues(filterUri3, cv1, cv2, cv3, cv4);
+
+        final Uri filterUri4 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "518");
+        assertStoredValues(filterUri4, cv5, cv6);
+
+        final Uri filterUri5 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "doe");
+        assertStoredValues(filterUri5, cv5, cv6);
+
+        final Uri filterUri6 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI, "51");
+        assertStoredValues(filterUri6, cv1, cv2, cv3, cv5, cv6);
+
+        final Uri filterUri7 = Uri.withAppendedPath(Contactables.CONTENT_FILTER_URI,
+                "tamale@google");
+        assertEquals(0, getCount(filterUri7, null, null));
+
+        final Uri filterUri8 = Contactables.CONTENT_URI;
+        assertStoredValues(filterUri8, cv1, cv2, cv3, cv4, cv5, cv6);
+
+        // test VISIBLE_CONTACTS_ONLY boolean parameter
+        final Uri filterUri9 = filterUri6.buildUpon().appendQueryParameter(
+                Contactables.VISIBLE_CONTACTS_ONLY, "true").build();
+        assertStoredValues(filterUri9, cv1, cv2, cv3, cv5, cv6);
+        // mark Hot Tamale as invisible - cv1, cv2, and cv3 should no longer be in the cursor
+        markInvisible(queryContactId(rawContactId));
+        assertStoredValues(filterUri9, cv5, cv6);
+    }
+
+
     public void testQueryContactData() {
         ContentValues values = new ContentValues();
         long contactId = createContact(values, "John", "Doe",
@@ -1985,7 +2222,7 @@
         nameValues.put(StructuredName.FAMILY_NAME, "Goulash");
         nameValues.put(StructuredName.PHONETIC_FAMILY_NAME, "goo");
         nameValues.put(StructuredName.PHONETIC_GIVEN_NAME, "LASH");
-        Uri nameUri = insertStructuredName(rawContactId, nameValues);
+        Uri nameUri = DataUtil.insertStructuredName(mResolver, rawContactId, nameValues);
 
         long contactId = queryContactId(rawContactId);
         values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
@@ -2019,7 +2256,7 @@
                 StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO |
                 StatusUpdates.CAPABILITY_HAS_VOICE);
 
-        insertStructuredName(rawContactId, "James", "Bond");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "James", "Bond");
 
         long contactId = queryContactId(rawContactId);
         values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
@@ -2045,7 +2282,7 @@
                 StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO |
                 StatusUpdates.CAPABILITY_HAS_VOICE);
 
-        insertStructuredName(rawContactId, "James", "Bond");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "James", "Bond");
 
         long contactId = queryContactId(rawContactId);
         values.put(Contacts.CONTACT_PRESENCE, StatusUpdates.INVISIBLE);
@@ -2135,23 +2372,23 @@
 
     public void testQueryContactStrequentFrequentOrder() {
         // Prepare test data
-        final long rid1 = createRawContact();
+        final long rid1 = RawContactUtil.createRawContact(mResolver);
         final long did1 = ContentUris.parseId(insertPhoneNumber(rid1, "1"));
         final long did1e = ContentUris.parseId(insertEmail(rid1, "1@email.com"));
 
-        final long rid2 = createRawContact();
+        final long rid2 = RawContactUtil.createRawContact(mResolver);
         final long did2 = ContentUris.parseId(insertPhoneNumber(rid2, "2"));
 
-        final long rid3 = createRawContact();
+        final long rid3 = RawContactUtil.createRawContact(mResolver);
         final long did3 = ContentUris.parseId(insertPhoneNumber(rid3, "3"));
 
-        final long rid4 = createRawContact();
+        final long rid4 = RawContactUtil.createRawContact(mResolver);
         final long did4 = ContentUris.parseId(insertPhoneNumber(rid4, "4"));
 
-        final long rid5 = createRawContact();
+        final long rid5 = RawContactUtil.createRawContact(mResolver);
         final long did5 = ContentUris.parseId(insertPhoneNumber(rid5, "5"));
 
-        final long rid6 = createRawContact();
+        final long rid6 = RawContactUtil.createRawContact(mResolver);
         final long did6 = ContentUris.parseId(insertPhoneNumber(rid6, "6"));
 
         final long cid1 = queryContactId(rid1);
@@ -2301,6 +2538,48 @@
 
         // Now we have only 1 frequent.
         assertStoredValues(Contacts.CONTENT_FREQUENT_URI, new ContentValues[] {values1});
+
+    }
+
+    public void testQueryDataUsageStat() {
+        ContentValues values1 = new ContentValues();
+        final String email1 = "a@acme.com";
+        final long cid1 = createContact(values1, "Noah", "Tever", "18004664411",
+                email1, StatusUpdates.OFFLINE, 0, 0, 0, 0);
+
+        sMockClock.install();
+        sMockClock.setCurrentTimeMillis(100);
+
+        sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values1);
+
+        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 1, 100);
+
+        sMockClock.setCurrentTimeMillis(111);
+        sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values1);
+
+        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 2, 111);
+
+        sMockClock.setCurrentTimeMillis(123);
+        sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, values1);
+
+        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 3, 123);
+
+        final Uri dataUriWithUsageTypeLongText = Data.CONTENT_URI.buildUpon().appendQueryParameter(
+                DataUsageFeedback.USAGE_TYPE, DataUsageFeedback.USAGE_TYPE_LONG_TEXT).build();
+
+        assertDataUsageCursorContains(dataUriWithUsageTypeLongText, "a@acme.com", 2, 111);
+
+        sMockClock.setCurrentTimeMillis(200);
+        sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
+        sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
+        sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_CALL, values1);
+
+        assertDataUsageCursorContains(Data.CONTENT_URI, "a@acme.com", 6, 200);
+
+        final Uri dataUriWithUsageTypeCall = Data.CONTENT_URI.buildUpon().appendQueryParameter(
+                DataUsageFeedback.USAGE_TYPE, DataUsageFeedback.USAGE_TYPE_CALL).build();
+
+        assertDataUsageCursorContains(dataUriWithUsageTypeCall, "a@acme.com", 3, 200);
     }
 
     public void testQueryContactGroup() {
@@ -2481,7 +2760,7 @@
         mActor.removePermissions("android.permission.WRITE_PROFILE");
 
         // Create a non-profile contact.
-        long rawContactId = createRawContactWithName("Domo", "Arigato");
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Domo", "Arigato");
         long dataId = getStoredLongValue(Data.CONTENT_URI,
                 Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
                 new String[]{String.valueOf(rawContactId), StructuredName.CONTENT_ITEM_TYPE},
@@ -2630,7 +2909,8 @@
         // Insert a profile record with a new data set.
         Account account = new Account("a", "b");
         String dataSet = "c";
-        Uri profileUri = maybeAddAccountQueryParameters(Profile.CONTENT_RAW_CONTACTS_URI, account)
+        Uri profileUri = TestUtil.maybeAddAccountQueryParameters(Profile.CONTENT_RAW_CONTACTS_URI,
+                account)
                 .buildUpon().appendQueryParameter(RawContacts.DATA_SET, dataSet).build();
         ContentValues values = new ContentValues();
         long rawContactId = ContentUris.parseId(mResolver.insert(profileUri, values));
@@ -2661,7 +2941,7 @@
         ContentValues values = new ContentValues();
         Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         long rawContactId = ContentUris.parseId(rawContactUri);
-        insertStructuredName(rawContactId, "John", "Doe");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "John", "Doe");
         Uri photoUri = insertPhoto(rawContactId);
         long photoId = ContentUris.parseId(photoUri);
         insertPhoneNumber(rawContactId, "18004664411");
@@ -2713,8 +2993,8 @@
         Account account2 = new Account("c", "d");
         long groupId1 = createGroup(account1, "e", "f");
         long groupId2 = createGroup(account2, "g", "h");
-        Uri uri1 = maybeAddAccountQueryParameters(Groups.CONTENT_URI, account1);
-        Uri uri2 = maybeAddAccountQueryParameters(Groups.CONTENT_URI, account2);
+        Uri uri1 = TestUtil.maybeAddAccountQueryParameters(Groups.CONTENT_URI, account1);
+        Uri uri2 = TestUtil.maybeAddAccountQueryParameters(Groups.CONTENT_URI, account2);
         assertEquals(1, getCount(uri1, null, null));
         assertEquals(1, getCount(uri2, null, null));
         assertStoredValue(uri1, Groups._ID + "=" + groupId1, null, Groups._ID, groupId1) ;
@@ -2748,7 +3028,7 @@
     }
 
     public void testGroupCreationAfterMembershipInsert() {
-        long rawContactId1 = createRawContact(mAccount);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount);
         Uri groupMembershipUri = insertGroupMembership(rawContactId1, "gsid1");
 
         long groupId = assertSingleGroup(NO_LONG, mAccount, "gsid1", null);
@@ -2757,7 +3037,7 @@
     }
 
     public void testGroupReuseAfterMembershipInsert() {
-        long rawContactId1 = createRawContact(mAccount);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount);
         long groupId1 = createGroup(mAccount, "gsid1", "title1");
         Uri groupMembershipUri = insertGroupMembership(rawContactId1, "gsid1");
 
@@ -2767,7 +3047,7 @@
     }
 
     public void testGroupInsertFailureOnGroupIdConflict() {
-        long rawContactId1 = createRawContact(mAccount);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount);
         long groupId1 = createGroup(mAccount, "gsid1", "title1");
 
         ContentValues values = new ContentValues();
@@ -2851,13 +3131,16 @@
 
         // Prepare raw contact id not used at all, to test group summary uri won't be confused
         // with it.
-        final long rawContactId0 = createRawContactWithName("firstName0", "lastName0");
+        final long rawContactId0 = RawContactUtil.createRawContactWithName(mResolver, "firstName0",
+                "lastName0");
 
-        final long rawContactId1 = createRawContactWithName("firstName1", "lastName1");
+        final long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "firstName1",
+                "lastName1");
         insertEmail(rawContactId1, "address1@email.com");
         insertGroupMembership(rawContactId1, groupId1);
 
-        final long rawContactId2 = createRawContactWithName("firstName2", "lastName2");
+        final long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "firstName2",
+                "lastName2");
         insertEmail(rawContactId2, "address2@email.com");
         insertPhoneNumber(rawContactId2, "222-222-2222");
         insertGroupMembership(rawContactId2, groupId1);
@@ -2899,7 +3182,8 @@
         assertStoredValues(Groups.CONTENT_SUMMARY_URI, new ContentValues[] { v1, v2, v3 });
 
         // Introduce new raw contact, pretending the user added another info.
-        final long rawContactId3 = createRawContactWithName("firstName3", "lastName3");
+        final long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "firstName3",
+                "lastName3");
         insertEmail(rawContactId3, "address3@email.com");
         insertPhoneNumber(rawContactId3, "333-333-3333");
         insertGroupMembership(rawContactId3, groupId2);
@@ -2967,8 +3251,8 @@
         createSettings(account1, "0", "0");
         createSettings(account2, "1", "1");
         createSettings(account3, "1", "0");
-        Uri uri1 = maybeAddAccountQueryParameters(Settings.CONTENT_URI, account1);
-        Uri uri2 = maybeAddAccountQueryParameters(Settings.CONTENT_URI, account2);
+        Uri uri1 = TestUtil.maybeAddAccountQueryParameters(Settings.CONTENT_URI, account1);
+        Uri uri2 = TestUtil.maybeAddAccountQueryParameters(Settings.CONTENT_URI, account2);
         Uri uri3 = Settings.CONTENT_URI.buildUpon()
                 .appendQueryParameter(RawContacts.ACCOUNT_NAME, account3.getAccountName())
                 .appendQueryParameter(RawContacts.ACCOUNT_TYPE, account3.getAccountType())
@@ -3005,36 +3289,37 @@
     }
 
     public void testDisplayNameParsingWhenPartsUnspecified() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, "Mr.John Kevin von Smith, Jr.");
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         assertStructuredName(rawContactId, "Mr.", "John", "Kevin", "von Smith", "Jr.");
     }
 
     public void testDisplayNameParsingWhenPartsAreNull() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, "Mr.John Kevin von Smith, Jr.");
         values.putNull(StructuredName.GIVEN_NAME);
         values.putNull(StructuredName.FAMILY_NAME);
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
         assertStructuredName(rawContactId, "Mr.", "John", "Kevin", "von Smith", "Jr.");
     }
 
     public void testDisplayNameParsingWhenPartsSpecified() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, "Mr.John Kevin von Smith, Jr.");
         values.put(StructuredName.FAMILY_NAME, "Johnson");
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         assertStructuredName(rawContactId, null, null, null, "Johnson", null);
     }
 
     public void testContactWithoutPhoneticName() {
-        final long rawContactId = createRawContact(null);
+        ContactLocaleUtils.setLocale(Locale.ENGLISH);
+        final long rawContactId = RawContactUtil.createRawContact(mResolver, null);
 
         ContentValues values = new ContentValues();
         values.put(StructuredName.PREFIX, "Mr");
@@ -3042,7 +3327,7 @@
         values.put(StructuredName.MIDDLE_NAME, "K.");
         values.put(StructuredName.FAMILY_NAME, "Doe");
         values.put(StructuredName.SUFFIX, "Jr.");
-        Uri dataUri = insertStructuredName(rawContactId, values);
+        Uri dataUri = DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         values.clear();
         values.put(RawContacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
@@ -3051,7 +3336,9 @@
         values.putNull(RawContacts.PHONETIC_NAME);
         values.put(RawContacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
         values.put(RawContacts.SORT_KEY_PRIMARY, "John K. Doe, Jr.");
+        values.put(RawContactsColumns.PHONEBOOK_LABEL_PRIMARY, "J");
         values.put(RawContacts.SORT_KEY_ALTERNATIVE, "Doe, John K., Jr.");
+        values.put(RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE, "D");
 
         Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
         assertStoredValues(rawContactUri, values);
@@ -3063,7 +3350,9 @@
         values.putNull(Contacts.PHONETIC_NAME);
         values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
         values.put(Contacts.SORT_KEY_PRIMARY, "John K. Doe, Jr.");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_PRIMARY, "J");
         values.put(Contacts.SORT_KEY_ALTERNATIVE, "Doe, John K., Jr.");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE, "D");
 
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
                 queryContactId(rawContactId));
@@ -3074,17 +3363,17 @@
     }
 
     public void testContactWithChineseName() {
-
-        // Only run this test when Chinese collation is supported
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseCollator()) {
             return;
         }
+        ContactLocaleUtils.setLocale(Locale.SIMPLIFIED_CHINESE);
 
-        long rawContactId = createRawContact(null);
+        long rawContactId = RawContactUtil.createRawContact(mResolver, null);
 
         ContentValues values = new ContentValues();
+        // "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B"
         values.put(StructuredName.DISPLAY_NAME, "\u6BB5\u5C0F\u6D9B");
-        Uri dataUri = insertStructuredName(rawContactId, values);
+        Uri dataUri = DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         values.clear();
         values.put(RawContacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
@@ -3092,8 +3381,10 @@
         values.put(RawContacts.DISPLAY_NAME_ALTERNATIVE, "\u6BB5\u5C0F\u6D9B");
         values.putNull(RawContacts.PHONETIC_NAME);
         values.put(RawContacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
-        values.put(RawContacts.SORT_KEY_PRIMARY, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
-        values.put(RawContacts.SORT_KEY_ALTERNATIVE, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
+        values.put(RawContacts.SORT_KEY_PRIMARY, "\u6BB5\u5C0F\u6D9B");
+        values.put(RawContactsColumns.PHONEBOOK_LABEL_PRIMARY, "D");
+        values.put(RawContacts.SORT_KEY_ALTERNATIVE, "\u6BB5\u5C0F\u6D9B");
+        values.put(RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE, "D");
 
         Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
         assertStoredValues(rawContactUri, values);
@@ -3104,8 +3395,10 @@
         values.put(Contacts.DISPLAY_NAME_ALTERNATIVE, "\u6BB5\u5C0F\u6D9B");
         values.putNull(Contacts.PHONETIC_NAME);
         values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
-        values.put(Contacts.SORT_KEY_PRIMARY, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
-        values.put(Contacts.SORT_KEY_ALTERNATIVE, "DUAN \u6BB5 XIAO \u5C0F TAO \u6D9B");
+        values.put(Contacts.SORT_KEY_PRIMARY, "\u6BB5\u5C0F\u6D9B");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_PRIMARY, "D");
+        values.put(Contacts.SORT_KEY_ALTERNATIVE, "\u6BB5\u5C0F\u6D9B");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE, "D");
 
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
                 queryContactId(rawContactId));
@@ -3115,13 +3408,38 @@
         assertStoredValues(dataUri, values);
     }
 
-    public void testContactWithJapaneseName() {
-        long rawContactId = createRawContact(null);
+    public void testJapaneseNameContactInEnglishLocale() {
+        // Need Japanese locale data for transliteration
+        if (!hasJapaneseCollator()) {
+            return;
+        }
+        ContactLocaleUtils.setLocale(Locale.US);
+        long rawContactId = RawContactUtil.createRawContact(mResolver, null);
 
         ContentValues values = new ContentValues();
         values.put(StructuredName.GIVEN_NAME, "\u7A7A\u6D77");
         values.put(StructuredName.PHONETIC_GIVEN_NAME, "\u304B\u3044\u304F\u3046");
-        Uri dataUri = insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
+
+        long contactId = queryContactId(rawContactId);
+        // en_US should behave same as ja_JP (match on Hiragana and Romaji
+        // but not Pinyin)
+        assertContactFilter(contactId, "\u304B\u3044\u304F\u3046");
+        assertContactFilter(contactId, "kaiku");
+        assertContactFilterNoResult("kong");
+    }
+
+    public void testContactWithJapaneseName() {
+        if (!hasJapaneseCollator()) {
+            return;
+        }
+        ContactLocaleUtils.setLocale(Locale.JAPAN);
+        long rawContactId = RawContactUtil.createRawContact(mResolver, null);
+
+        ContentValues values = new ContentValues();
+        values.put(StructuredName.GIVEN_NAME, "\u7A7A\u6D77");
+        values.put(StructuredName.PHONETIC_GIVEN_NAME, "\u304B\u3044\u304F\u3046");
+        Uri dataUri = DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         values.clear();
         values.put(RawContacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME);
@@ -3131,6 +3449,8 @@
         values.put(RawContacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.JAPANESE);
         values.put(RawContacts.SORT_KEY_PRIMARY, "\u304B\u3044\u304F\u3046");
         values.put(RawContacts.SORT_KEY_ALTERNATIVE, "\u304B\u3044\u304F\u3046");
+        values.put(RawContactsColumns.PHONEBOOK_LABEL_PRIMARY, "\u304B");
+        values.put(RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE, "\u304B");
 
         Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
         assertStoredValues(rawContactUri, values);
@@ -3143,6 +3463,8 @@
         values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.JAPANESE);
         values.put(Contacts.SORT_KEY_PRIMARY, "\u304B\u3044\u304F\u3046");
         values.put(Contacts.SORT_KEY_ALTERNATIVE, "\u304B\u3044\u304F\u3046");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_PRIMARY, "\u304B");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE, "\u304B");
 
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
                 queryContactId(rawContactId));
@@ -3150,13 +3472,19 @@
 
         // The same values should be available through a join with Data
         assertStoredValues(dataUri, values);
+
+        long contactId = queryContactId(rawContactId);
+        // ja_JP should match on Hiragana and Romaji but not Pinyin
+        assertContactFilter(contactId, "\u304B\u3044\u304F\u3046");
+        assertContactFilter(contactId, "kaiku");
+        assertContactFilterNoResult("kong");
     }
 
     public void testDisplayNameUpdate() {
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         insertEmail(rawContactId1, "potato@acme.com", true);
 
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         insertPhoneNumber(rawContactId2, "123456789", true);
 
         setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
@@ -3164,14 +3492,14 @@
 
         assertAggregated(rawContactId1, rawContactId2, "123456789");
 
-        insertStructuredName(rawContactId2, "Potato", "Head");
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Potato", "Head");
 
         assertAggregated(rawContactId1, rawContactId2, "Potato Head");
         assertNetworkNotified(true);
     }
 
     public void testDisplayNameFromData() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
 
@@ -3202,12 +3530,12 @@
         values.put(StructuredName.GIVEN_NAME, "James");
         values.put(StructuredName.MIDDLE_NAME, "P.");
         values.put(StructuredName.FAMILY_NAME, "Sullivan");
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
         assertStoredValue(uri, Contacts.DISPLAY_NAME, "James P. Sullivan");
     }
 
     public void testDisplayNameFromOrganizationWithoutPhoneticName() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
 
@@ -3230,11 +3558,17 @@
         values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
         values.put(Contacts.SORT_KEY_PRIMARY, "Monsters Inc");
         values.put(Contacts.SORT_KEY_ALTERNATIVE, "Monsters Inc");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_PRIMARY, "M");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE, "M");
         assertStoredValues(uri, values);
     }
 
     public void testDisplayNameFromOrganizationWithJapanesePhoneticName() {
-        long rawContactId = createRawContact();
+        if (!hasJapaneseCollator()) {
+            return;
+        }
+        ContactLocaleUtils.setLocale(Locale.JAPAN);
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
 
@@ -3252,24 +3586,18 @@
         values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.JAPANESE);
         values.put(Contacts.SORT_KEY_PRIMARY, "\u30C9\u30B3\u30E2");
         values.put(Contacts.SORT_KEY_ALTERNATIVE, "\u30C9\u30B3\u30E2");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_PRIMARY, "\u305F");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE, "\u305F");
         assertStoredValues(uri, values);
     }
 
     public void testDisplayNameFromOrganizationWithChineseName() {
-        boolean hasChineseCollator = false;
-        final Locale locale[] = Collator.getAvailableLocales();
-        for (int i = 0; i < locale.length; i++) {
-            if (locale[i].equals(Locale.CHINA)) {
-                hasChineseCollator = true;
-                break;
-            }
-        }
-
-        if (!hasChineseCollator) {
+        if (!hasChineseCollator()) {
             return;
         }
+        ContactLocaleUtils.setLocale(Locale.SIMPLIFIED_CHINESE);
 
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
 
@@ -3284,13 +3612,15 @@
         values.put(Contacts.DISPLAY_NAME, "\u4E2D\u56FD\u7535\u4FE1");
         values.putNull(Contacts.PHONETIC_NAME);
         values.put(Contacts.PHONETIC_NAME_STYLE, PhoneticNameStyle.UNDEFINED);
-        values.put(Contacts.SORT_KEY_PRIMARY, "ZHONG \u4E2D GUO \u56FD DIAN \u7535 XIN \u4FE1");
-        values.put(Contacts.SORT_KEY_ALTERNATIVE, "ZHONG \u4E2D GUO \u56FD DIAN \u7535 XIN \u4FE1");
+        values.put(Contacts.SORT_KEY_PRIMARY, "\u4E2D\u56FD\u7535\u4FE1");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_PRIMARY, "Z");
+        values.put(Contacts.SORT_KEY_ALTERNATIVE, "\u4E2D\u56FD\u7535\u4FE1");
+        values.put(ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE, "Z");
         assertStoredValues(uri, values);
     }
 
     public void testLookupByOrganization() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
 
@@ -3338,12 +3668,12 @@
     }
 
     private void assertContactFilterNoResult(String filter) {
-        Uri filterUri4 = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, filter);
-        assertEquals(0, getCount(filterUri4, null, null));
+        Uri filterUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
+        assertEquals(0, getCount(filterUri, null, null));
     }
 
     public void testSearchSnippetOrganization() throws Exception {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
 
         // Some random data element
@@ -3375,11 +3705,11 @@
     }
 
     public void testSearchSnippetEmail() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
 
-        insertStructuredName(rawContactId, "John", "Doe");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "John", "Doe");
         Uri dataUri = insertEmail(rawContactId, "acme@corp.com", true, Email.TYPE_CUSTOM, "Custom");
 
         Uri filterUri = buildFilterUri("acme", true);
@@ -3404,11 +3734,11 @@
     }
 
     public void testSearchSnippetPhone() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
 
-        insertStructuredName(rawContactId, "Cave", "Johnson");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Cave", "Johnson");
         insertPhoneNumber(rawContactId, "(860) 555-1234");
 
         values.clear();
@@ -3441,7 +3771,7 @@
     }
 
     public void testSearchSnippetNickname() throws Exception {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
 
@@ -3456,9 +3786,9 @@
     }
 
     public void testSearchSnippetEmptyForNameInDisplayName() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
-        insertStructuredName(rawContactId, "Cave", "Johnson");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Cave", "Johnson");
         insertEmail(rawContactId, "cave@aperturescience.com", true);
 
         ContentValues emptySnippet = new ContentValues();
@@ -3471,7 +3801,7 @@
     }
 
     public void testSearchSnippetEmptyForNicknameInDisplayName() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         insertNickname(rawContactId, "Caveman");
         insertEmail(rawContactId, "cave@aperturescience.com", true);
@@ -3485,7 +3815,7 @@
     }
 
     public void testSearchSnippetEmptyForCompanyInDisplayName() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues company = new ContentValues();
         company.clear();
@@ -3503,7 +3833,7 @@
     }
 
     public void testSearchSnippetEmptyForPhoneInDisplayName() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         insertPhoneNumber(rawContactId, "860-555-1234");
         insertEmail(rawContactId, "860@aperturescience.com", true);
@@ -3517,7 +3847,7 @@
     }
 
     public void testSearchSnippetEmptyForEmailInDisplayName() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         insertEmail(rawContactId, "cave@aperturescience.com", true);
         insertNote(rawContactId, "Cave Johnson is president of Aperture Science");
@@ -3531,8 +3861,8 @@
     }
 
     public void testDisplayNameUpdateFromStructuredNameUpdate() {
-        long rawContactId = createRawContact();
-        Uri nameUri = insertStructuredName(rawContactId, "Slinky", "Dog");
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
+        Uri nameUri = DataUtil.insertStructuredName(mResolver, rawContactId, "Slinky", "Dog");
 
         long contactId = queryContactId(rawContactId);
 
@@ -3574,7 +3904,7 @@
     }
 
     public void testSendToVoicemailDefault() {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
 
         Cursor c = queryContact(contactId);
@@ -3585,7 +3915,7 @@
     }
 
     public void testSetSendToVoicemailAndRingtone() {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
 
         updateSendToVoicemailAndRingtone(contactId, true, "foo");
@@ -3598,11 +3928,11 @@
     }
 
     public void testSendToVoicemailAndRingtoneAfterAggregation() {
-        long rawContactId1 = createRawContactWithName("a", "b");
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "a", "b");
         long contactId1 = queryContactId(rawContactId1);
         updateSendToVoicemailAndRingtone(contactId1, true, "foo");
 
-        long rawContactId2 = createRawContactWithName("c", "d");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "c", "d");
         long contactId2 = queryContactId(rawContactId2);
         updateSendToVoicemailAndRingtone(contactId2, true, "bar");
 
@@ -3615,11 +3945,11 @@
     }
 
     public void testDoNotSendToVoicemailAfterAggregation() {
-        long rawContactId1 = createRawContactWithName("e", "f");
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "e", "f");
         long contactId1 = queryContactId(rawContactId1);
         updateSendToVoicemailAndRingtone(contactId1, true, null);
 
-        long rawContactId2 = createRawContactWithName("g", "h");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "g", "h");
         long contactId2 = queryContactId(rawContactId2);
         updateSendToVoicemailAndRingtone(contactId2, false, null);
 
@@ -3632,11 +3962,11 @@
     }
 
     public void testSetSendToVoicemailAndRingtonePreservedAfterJoinAndSplit() {
-        long rawContactId1 = createRawContactWithName("i", "j");
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "i", "j");
         long contactId1 = queryContactId(rawContactId1);
         updateSendToVoicemailAndRingtone(contactId1, true, "foo");
 
-        long rawContactId2 = createRawContactWithName("k", "l");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "k", "l");
         long contactId2 = queryContactId(rawContactId2);
         updateSendToVoicemailAndRingtone(contactId2, false, "bar");
 
@@ -3653,7 +3983,7 @@
     }
 
     public void testStatusUpdateInsert() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         Uri imUri = insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
         long dataId = ContentUris.parseId(imUri);
 
@@ -3706,7 +4036,7 @@
     }
 
     public void testStatusUpdateInferAttribution() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         Uri imUri = insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
         long dataId = ContentUris.parseId(imUri);
 
@@ -3727,7 +4057,7 @@
     }
 
     public void testStatusUpdateMatchingImOrEmail() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
         insertImHandle(rawContactId, Im.PROTOCOL_CUSTOM, "my_im_proto", "my_im");
         insertEmail(rawContactId, "m@acme.com");
@@ -3771,7 +4101,7 @@
     }
 
     public void testStatusUpdateUpdateAndDelete() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
 
         long contactId = queryContactId(rawContactId);
@@ -3846,7 +4176,7 @@
     }
 
     public void testStatusUpdateUpdateToNull() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
 
         long contactId = queryContactId(rawContactId);
@@ -3874,7 +4204,7 @@
     }
 
     public void testStatusUpdateWithTimestamp() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         insertImHandle(rawContactId, Im.PROTOCOL_AIM, null, "aim");
         insertImHandle(rawContactId, Im.PROTOCOL_GOOGLE_TALK, null, "gtalk");
 
@@ -3907,7 +4237,7 @@
     // Stream item query test cases.
 
     public void testQueryStreamItemsByRawContactId() {
-        long rawContactId = createRawContact(mAccount);
+        long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
         ContentValues values = buildGenericStreamItemValues();
         insertStreamItem(rawContactId, values, mAccount);
         assertStoredValues(
@@ -3918,7 +4248,7 @@
     }
 
     public void testQueryStreamItemsByContactId() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = buildGenericStreamItemValues();
         insertStreamItem(rawContactId, values, null);
@@ -3930,7 +4260,7 @@
     }
 
     public void testQueryStreamItemsByLookupKey() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         String lookupKey = queryLookupKey(contactId);
         ContentValues values = buildGenericStreamItemValues();
@@ -3943,7 +4273,7 @@
     }
 
     public void testQueryStreamItemsByLookupKeyAndContactId() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         String lookupKey = queryLookupKey(contactId);
         ContentValues values = buildGenericStreamItemValues();
@@ -3958,14 +4288,14 @@
     }
 
     public void testQueryStreamItems() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = buildGenericStreamItemValues();
         insertStreamItem(rawContactId, values, null);
         assertStoredValues(StreamItems.CONTENT_URI, values);
     }
 
     public void testQueryStreamItemsWithSelection() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues firstValues = buildGenericStreamItemValues();
         insertStreamItem(rawContactId, firstValues, null);
 
@@ -3983,7 +4313,7 @@
     }
 
     public void testQueryStreamItemById() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues firstValues = buildGenericStreamItemValues();
         Uri resultUri = insertStreamItem(rawContactId, firstValues, null);
         long firstStreamItemId = ContentUris.parseId(resultUri);
@@ -4005,7 +4335,7 @@
     // Stream item photo insertion + query test cases.
 
     public void testQueryStreamItemPhotoWithSelection() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = buildGenericStreamItemValues();
         Uri resultUri = insertStreamItem(rawContactId, values, null);
         long streamItemId = ContentUris.parseId(resultUri);
@@ -4022,7 +4352,7 @@
     }
 
     public void testQueryStreamItemPhotoByStreamItemId() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
 
         // Insert a first stream item.
         ContentValues firstValues = buildGenericStreamItemValues();
@@ -4053,7 +4383,7 @@
     }
 
     public void testQueryStreamItemPhotoByStreamItemPhotoId() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
 
         // Insert a first stream item.
         ContentValues firstValues = buildGenericStreamItemValues();
@@ -4119,7 +4449,7 @@
     }
 
     public void testInsertStreamItemWithContentValues() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = buildGenericStreamItemValues();
         values.put(StreamItems.RAW_CONTACT_ID, rawContactId);
         mResolver.insert(StreamItems.CONTENT_URI, values);
@@ -4129,7 +4459,7 @@
     }
 
     public void testInsertStreamItemOverLimit() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = buildGenericStreamItemValues();
         values.put(StreamItems.RAW_CONTACT_ID, rawContactId);
 
@@ -4164,7 +4494,7 @@
     }
 
     public void testInsertStreamItemOlderThanOldestInLimit() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = buildGenericStreamItemValues();
         values.put(StreamItems.RAW_CONTACT_ID, rawContactId);
 
@@ -4187,7 +4517,7 @@
     // Stream item photo insertion test cases.
 
     public void testInsertStreamItemsAndPhotosInBatch() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues streamItemValues = buildGenericStreamItemValues();
         ContentValues streamItemPhotoValues = buildGenericStreamItemPhotoValues(0);
 
@@ -4241,7 +4571,7 @@
     // Stream item update test cases.
 
     public void testUpdateStreamItemById() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = buildGenericStreamItemValues();
         Uri resultUri = insertStreamItem(rawContactId, values, null);
         long streamItemId = ContentUris.parseId(resultUri);
@@ -4254,7 +4584,7 @@
     }
 
     public void testUpdateStreamItemWithContentValues() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = buildGenericStreamItemValues();
         Uri resultUri = insertStreamItem(rawContactId, values, null);
         long streamItemId = ContentUris.parseId(resultUri);
@@ -4269,7 +4599,7 @@
     // Stream item photo update test cases.
 
     public void testUpdateStreamItemPhotoById() throws IOException {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = buildGenericStreamItemValues();
         Uri resultUri = insertStreamItem(rawContactId, values, null);
         long streamItemId = ContentUris.parseId(resultUri);
@@ -4297,7 +4627,7 @@
     }
 
     public void testUpdateStreamItemPhotoWithContentValues() throws IOException {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = buildGenericStreamItemValues();
         Uri resultUri = insertStreamItem(rawContactId, values, null);
         long streamItemId = ContentUris.parseId(resultUri);
@@ -4326,7 +4656,7 @@
     // Stream item deletion test cases.
 
     public void testDeleteStreamItemById() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues firstValues = buildGenericStreamItemValues();
         Uri resultUri = insertStreamItem(rawContactId, firstValues, null);
         long firstStreamItemId = ContentUris.parseId(resultUri);
@@ -4346,7 +4676,7 @@
     }
 
     public void testDeleteStreamItemWithSelection() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues firstValues = buildGenericStreamItemValues();
         insertStreamItem(rawContactId, firstValues, null);
 
@@ -4367,7 +4697,7 @@
     // Stream item photo deletion test cases.
 
     public void testDeleteStreamItemPhotoById() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long streamItemId = ContentUris.parseId(
                 insertStreamItem(rawContactId, buildGenericStreamItemValues(), null));
         long streamItemPhotoId = ContentUris.parseId(
@@ -4391,7 +4721,7 @@
     }
 
     public void testDeleteStreamItemPhotoWithSelection() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long streamItemId = ContentUris.parseId(
                 insertStreamItem(rawContactId, buildGenericStreamItemValues(), null));
         ContentValues firstPhotoValues = buildGenericStreamItemPhotoValues(0);
@@ -4408,7 +4738,7 @@
     }
 
     public void testDeleteStreamItemsWhenRawContactDeleted() {
-        long rawContactId = createRawContact(mAccount);
+        long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
         Uri streamItemUri = insertStreamItem(rawContactId,
                 buildGenericStreamItemValues(), mAccount);
         Uri streamItemPhotoUri = insertStreamItemPhoto(ContentUris.parseId(streamItemUri),
@@ -4497,7 +4827,7 @@
     }
 
     public void testStreamItemReadRequiresReadSocialStreamPermission() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         String lookupKey = queryLookupKey(contactId);
         long streamItemId = ContentUris.parseId(
@@ -4549,7 +4879,7 @@
     }
 
     public void testStreamItemPhotoReadRequiresReadSocialStreamPermission() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long streamItemId = ContentUris.parseId(
                 insertStreamItem(rawContactId, buildGenericStreamItemValues(), null));
         long streamItemPhotoId = ContentUris.parseId(
@@ -4573,7 +4903,7 @@
     }
 
     public void testStreamItemModificationRequiresWriteSocialStreamPermission() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long streamItemId = ContentUris.parseId(
                 insertStreamItem(rawContactId, buildGenericStreamItemValues(), null));
         mActor.removePermissions("android.permission.WRITE_SOCIAL_STREAM");
@@ -4602,7 +4932,7 @@
     }
 
     public void testStreamItemPhotoModificationRequiresWriteSocialStreamPermission() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long streamItemId = ContentUris.parseId(
                 insertStreamItem(rawContactId, buildGenericStreamItemValues(), null));
         long streamItemPhotoId = ContentUris.parseId(
@@ -4639,7 +4969,7 @@
     public void testStatusUpdateDoesNotRequireReadOrWriteSocialStreamPermission() {
         int protocol1 = Im.PROTOCOL_GOOGLE_TALK;
         String handle1 = "test@gmail.com";
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         insertImHandle(rawContactId, protocol1, null, handle1);
         mActor.removePermissions("android.permission.READ_SOCIAL_STREAM");
         mActor.removePermissions("android.permission.WRITE_SOCIAL_STREAM");
@@ -4676,7 +5006,7 @@
         int protocol1 = Im.PROTOCOL_GOOGLE_TALK;
         String handle1 = "test@gmail.com";
 
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         insertImHandle(rawContactId1, protocol1, null, handle1);
 
         insertStatusUpdate(protocol1, null, handle1, StatusUpdates.AVAILABLE, "Green",
@@ -4738,7 +5068,7 @@
     }
 
     public void testContactVisibilityUpdateOnMembershipChange() {
-        long rawContactId = createRawContact(mAccount);
+        long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
         assertVisibility(rawContactId, "0");
 
         long visibleGroupId = createGroup(mAccount, "123", "Visible", 1);
@@ -4804,30 +5134,33 @@
         long groupId1 = createGroup(mAccount, "gsid1", "title1");
         long groupId2 = createGroup(mAccount, "gsid2", "title2");
 
-        id = createRawContact(mAccount, RawContacts.SOURCE_ID, "c0");
+        id = RawContactUtil.createRawContact(mResolver, mAccount, RawContacts.SOURCE_ID, "c0");
         insertGroupMembership(id, "gsid1");
         insertEmail(id, "c0@email.com");
         insertPhoneNumber(id, "5551212c0");
 
-        long c1 = id = createRawContact(mAccount, RawContacts.SOURCE_ID, "c1");
+        long c1 = id = RawContactUtil.createRawContact(mResolver, mAccount, RawContacts.SOURCE_ID,
+                "c1");
         Uri id_1_0 = insertGroupMembership(id, "gsid1");
         Uri id_1_1 = insertGroupMembership(id, "gsid2");
         Uri id_1_2 = insertEmail(id, "c1@email.com");
         Uri id_1_3 = insertPhoneNumber(id, "5551212c1");
 
-        long c2 = id = createRawContact(mAccount, RawContacts.SOURCE_ID, "c2");
+        long c2 = id = RawContactUtil.createRawContact(mResolver, mAccount, RawContacts.SOURCE_ID,
+                "c2");
         Uri id_2_0 = insertGroupMembership(id, "gsid1");
         Uri id_2_1 = insertEmail(id, "c2@email.com");
         Uri id_2_2 = insertPhoneNumber(id, "5551212c2");
 
-        long c3 = id = createRawContact(mAccount, RawContacts.SOURCE_ID, "c3");
+        long c3 = id = RawContactUtil.createRawContact(mResolver, mAccount, RawContacts.SOURCE_ID,
+                "c3");
         Uri id_3_0 = insertGroupMembership(id, groupId2);
         Uri id_3_1 = insertEmail(id, "c3@email.com");
         Uri id_3_2 = insertPhoneNumber(id, "5551212c3");
 
         EntityIterator iterator = RawContacts.newEntityIterator(mResolver.query(
-                maybeAddAccountQueryParameters(RawContactsEntity.CONTENT_URI, mAccount), null,
-                RawContacts.SOURCE_ID + " in ('c1', 'c2', 'c3')", null, null));
+                TestUtil.maybeAddAccountQueryParameters(RawContactsEntity.CONTENT_URI, mAccount),
+                null, RawContacts.SOURCE_ID + " in ('c1', 'c2', 'c3')", null, null));
         Entity entity;
         ContentValues[] subValues;
         entity = iterator.next();
@@ -4884,7 +5217,7 @@
     }
 
     public void testDataCreateUpdateDeleteByMimeType() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
 
         ContentValues values = new ContentValues();
         values.put(Data.RAW_CONTACT_ID, rawContactId);
@@ -4947,11 +5280,11 @@
     public void testRawContactQuery() {
         Account account1 = new Account("a", "b");
         Account account2 = new Account("c", "d");
-        long rawContactId1 = createRawContact(account1);
-        long rawContactId2 = createRawContact(account2);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, account1);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, account2);
 
-        Uri uri1 = maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, account1);
-        Uri uri2 = maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, account2);
+        Uri uri1 = TestUtil.maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, account1);
+        Uri uri2 = TestUtil.maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, account2);
         assertEquals(1, getCount(uri1, null, null));
         assertEquals(1, getCount(uri2, null, null));
         assertStoredValue(uri1, RawContacts._ID, rawContactId1) ;
@@ -4964,7 +5297,7 @@
     }
 
     public void testRawContactDeletion() {
-        long rawContactId = createRawContact(mAccount);
+        long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
         Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
 
         insertImHandle(rawContactId, Im.PROTOCOL_GOOGLE_TALK, null, "deleteme@android.com");
@@ -4995,8 +5328,8 @@
     }
 
     public void testRawContactDeletionKeepingAggregateContact() {
-        long rawContactId1 = createRawContactWithName(mAccount);
-        long rawContactId2 = createRawContactWithName(mAccount);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, mAccount);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, mAccount);
         setAggregationException(
                 AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
 
@@ -5010,7 +5343,7 @@
     }
 
     public void testRawContactDeletion_byAccountParam() {
-        long rawContactId = createRawContact(mAccount);
+        long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
         Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
 
         insertImHandle(rawContactId, Im.PROTOCOL_GOOGLE_TALK, null, "deleteme@android.com");
@@ -5046,7 +5379,7 @@
     }
 
     public void testRawContactDeletion_byAccountSelection() {
-        long rawContactId = createRawContact(mAccount);
+        long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
         Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
 
         // Do not delete if we are deleting with wrong account.
@@ -5072,14 +5405,14 @@
      */
     public void testAccountsToString() {
         final Set<Account> EXPECTED_0 = Sets.newHashSet();
-        final Set<Account> EXPECTED_1 = Sets.newHashSet(ACCOUNT_1);
-        final Set<Account> EXPECTED_2 = Sets.newHashSet(ACCOUNT_2);
-        final Set<Account> EXPECTED_1_2 = Sets.newHashSet(ACCOUNT_1, ACCOUNT_2);
+        final Set<Account> EXPECTED_1 = Sets.newHashSet(TestUtil.ACCOUNT_1);
+        final Set<Account> EXPECTED_2 = Sets.newHashSet(TestUtil.ACCOUNT_2);
+        final Set<Account> EXPECTED_1_2 = Sets.newHashSet(TestUtil.ACCOUNT_1, TestUtil.ACCOUNT_2);
 
         final Set<Account> ACTUAL_0 = Sets.newHashSet();
-        final Set<Account> ACTUAL_1 = Sets.newHashSet(ACCOUNT_1);
-        final Set<Account> ACTUAL_2 = Sets.newHashSet(ACCOUNT_2);
-        final Set<Account> ACTUAL_1_2 = Sets.newHashSet(ACCOUNT_2, ACCOUNT_1);
+        final Set<Account> ACTUAL_1 = Sets.newHashSet(TestUtil.ACCOUNT_1);
+        final Set<Account> ACTUAL_2 = Sets.newHashSet(TestUtil.ACCOUNT_2);
+        final Set<Account> ACTUAL_1_2 = Sets.newHashSet(TestUtil.ACCOUNT_2, TestUtil.ACCOUNT_1);
 
         assertTrue(EXPECTED_0.equals(accountsToStringToAccounts(ACTUAL_0)));
         assertFalse(EXPECTED_0.equals(accountsToStringToAccounts(ACTUAL_1)));
@@ -5120,10 +5453,10 @@
         final ContactsProvider2 cp = (ContactsProvider2) getProvider();
 
         final Account[] ACCOUNTS_0 = new Account[] {};
-        final Account[] ACCOUNTS_1 = new Account[] {ACCOUNT_1};
-        final Account[] ACCOUNTS_2 = new Account[] {ACCOUNT_2};
-        final Account[] ACCOUNTS_1_2 = new Account[] {ACCOUNT_1, ACCOUNT_2};
-        final Account[] ACCOUNTS_2_1 = new Account[] {ACCOUNT_2, ACCOUNT_1};
+        final Account[] ACCOUNTS_1 = new Account[] {TestUtil.ACCOUNT_1};
+        final Account[] ACCOUNTS_2 = new Account[] {TestUtil.ACCOUNT_2};
+        final Account[] ACCOUNTS_1_2 = new Account[] {TestUtil.ACCOUNT_1, TestUtil.ACCOUNT_2};
+        final Account[] ACCOUNTS_2_1 = new Account[] {TestUtil.ACCOUNT_2, TestUtil.ACCOUNT_1};
 
         // Add ACCOUNT_1
 
@@ -5166,7 +5499,7 @@
     public void testAccountsUpdated() {
         // This is to ensure we do not delete contacts with null, null (account name, type)
         // accidentally.
-        long rawContactId3 = createRawContactWithName("James", "Sullivan");
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "James", "Sullivan");
         insertPhoneNumber(rawContactId3, "5234567890");
         Uri rawContact3 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId3);
         assertEquals(1, getCount(RawContacts.CONTENT_URI, null, null));
@@ -5178,9 +5511,9 @@
         assertStoredValue(rawContact3, RawContacts.ACCOUNT_NAME, null);
         assertStoredValue(rawContact3, RawContacts.ACCOUNT_TYPE, null);
 
-        long rawContactId1 = createRawContact(mAccount);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount);
         insertEmail(rawContactId1, "account1@email.com");
-        long rawContactId2 = createRawContact(mAccountTwo);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, mAccountTwo);
         insertEmail(rawContactId2, "account2@email.com");
         insertImHandle(rawContactId2, Im.PROTOCOL_GOOGLE_TALK, null, "deleteme@android.com");
         insertStatusUpdate(Im.PROTOCOL_GOOGLE_TALK, null, "deleteme@android.com",
@@ -5200,9 +5533,11 @@
         mActor.setAccounts(new Account[]{readOnlyAccount, mAccount});
         cp.onAccountsUpdated(new Account[]{readOnlyAccount, mAccount});
 
-        long rawContactId1 = createRawContactWithName("John", "Doe", readOnlyAccount);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                readOnlyAccount);
         Uri photoUri1 = insertPhoto(rawContactId1);
-        long rawContactId2 = createRawContactWithName("john", "doe", mAccount);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "john", "doe",
+                mAccount);
         Uri photoUri2 = insertPhoto(rawContactId2);
         storeValue(photoUri2, Photo.IS_SUPER_PRIMARY, "1");
 
@@ -5243,7 +5578,7 @@
         cp.onAccountsUpdated(new Account[]{doomedAccount, safeAccount});
 
         // Create a doomed raw contact, stream item, and photo.
-        long doomedRawContactId = createRawContactWithName(doomedAccount);
+        long doomedRawContactId = RawContactUtil.createRawContactWithName(mResolver, doomedAccount);
         Uri doomedStreamItemUri =
                 insertStreamItem(doomedRawContactId, buildGenericStreamItemValues(), doomedAccount);
         long doomedStreamItemId = ContentUris.parseId(doomedStreamItemUri);
@@ -5251,7 +5586,7 @@
                 doomedStreamItemId, buildGenericStreamItemPhotoValues(0), doomedAccount);
 
         // Create a safe raw contact, stream item, and photo.
-        long safeRawContactId = createRawContactWithName(safeAccount);
+        long safeRawContactId = RawContactUtil.createRawContactWithName(mResolver, safeAccount);
         Uri safeStreamItemUri =
                 insertStreamItem(safeRawContactId, buildGenericStreamItemValues(), safeAccount);
         long safeStreamItemId = ContentUris.parseId(safeStreamItemUri);
@@ -5278,8 +5613,10 @@
     }
 
     public void testContactDeletion() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_2);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                TestUtil.ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                TestUtil.ACCOUNT_2);
 
         long contactId = queryContactId(rawContactId1);
 
@@ -5292,10 +5629,10 @@
     }
 
     public void testMarkAsDirtyParameter() {
-        long rawContactId = createRawContact(mAccount);
+        long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
         Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
 
-        Uri uri = insertStructuredName(rawContactId, "John", "Doe");
+        Uri uri = DataUtil.insertStructuredName(mResolver, rawContactId, "John", "Doe");
         clearDirty(rawContactUri);
         Uri updateUri = setCallerIsSyncAdapter(uri, mAccount);
 
@@ -5308,7 +5645,7 @@
     }
 
     public void testRawContactDirtyAndVersion() {
-        final long rawContactId = createRawContact(mAccount);
+        final long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
         Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId);
         assertDirty(uri, false);
         long version = getVersion(uri);
@@ -5349,7 +5686,7 @@
     }
 
     public void testRawContactClearDirty() {
-        final long rawContactId = createRawContact(mAccount);
+        final long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
         Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
                 rawContactId);
         long version = getVersion(uri);
@@ -5364,7 +5701,7 @@
     }
 
     public void testRawContactDeletionSetsDirty() {
-        final long rawContactId = createRawContact(mAccount);
+        final long rawContactId = RawContactUtil.createRawContact(mResolver, mAccount);
         Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
                 rawContactId);
         long version = getVersion(uri);
@@ -5435,7 +5772,7 @@
         ContentValues values = new ContentValues();
         Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         long rawContactId = ContentUris.parseId(rawContactUri);
-        insertStructuredName(rawContactId, "John", "Doe");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "John", "Doe");
         long dataId = ContentUris.parseId(insertPhoto(rawContactId, R.drawable.earth_normal));
         long photoFileId = getStoredLongValue(Data.CONTENT_URI, Data._ID + "=?",
                 new String[]{String.valueOf(dataId)}, Photo.PHOTO_FILE_ID);
@@ -5448,7 +5785,7 @@
     }
 
     public void testGetPhotoViaLookupUri() throws IOException {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
         Uri lookupUri = Contacts.getLookupUri(mResolver, contactUri);
@@ -5475,7 +5812,7 @@
     }
 
     public void testInputStreamForPhoto() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
         insertPhoto(rawContactId);
@@ -5498,11 +5835,11 @@
     }
 
     public void testSuperPrimaryPhoto() {
-        long rawContactId1 = createRawContact(new Account("a", "a"));
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
         Uri photoUri1 = insertPhoto(rawContactId1, R.drawable.earth_normal);
         long photoId1 = ContentUris.parseId(photoUri1);
 
-        long rawContactId2 = createRawContact(new Account("b", "b"));
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, new Account("b", "b"));
         Uri photoUri2 = insertPhoto(rawContactId2, R.drawable.earth_normal);
         long photoId2 = ContentUris.parseId(photoUri2);
 
@@ -5540,7 +5877,7 @@
         ContentValues values = new ContentValues();
         Uri rawContactUri = mResolver.insert(RawContacts.CONTENT_URI, values);
         long rawContactId = ContentUris.parseId(rawContactUri);
-        insertStructuredName(rawContactId, "John", "Doe");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "John", "Doe");
 
         Uri twigUri = Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI,
                 queryContactId(rawContactId)), Contacts.Photo.CONTENT_DIRECTORY);
@@ -5599,7 +5936,7 @@
     }
 
     public void testOpenDisplayPhotoForContactId() throws IOException {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
         insertPhoto(rawContactId, R.drawable.earth_normal);
         Uri photoUri = Contacts.CONTENT_URI.buildUpon()
@@ -5611,7 +5948,7 @@
     }
 
     public void testOpenDisplayPhotoForContactLookupKey() throws IOException {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
         String lookupKey = queryLookupKey(contactId);
         insertPhoto(rawContactId, R.drawable.earth_normal);
@@ -5624,7 +5961,7 @@
     }
 
     public void testOpenDisplayPhotoForContactLookupKeyAndId() throws IOException {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
         String lookupKey = queryLookupKey(contactId);
         insertPhoto(rawContactId, R.drawable.earth_normal);
@@ -5638,7 +5975,7 @@
     }
 
     public void testOpenDisplayPhotoForRawContactId() throws IOException {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         insertPhoto(rawContactId, R.drawable.earth_normal);
         Uri photoUri = RawContacts.CONTENT_URI.buildUpon()
                 .appendPath(String.valueOf(rawContactId))
@@ -5649,7 +5986,7 @@
     }
 
     public void testOpenDisplayPhotoByPhotoUri() throws IOException {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
         insertPhoto(rawContactId, R.drawable.earth_normal);
 
@@ -5663,7 +6000,7 @@
     }
 
     public void testPhotoUriForDisplayPhoto() {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
 
         // Photo being inserted is larger than a thumbnail, so it will be stored as a file.
@@ -5686,7 +6023,7 @@
     }
 
     public void testPhotoUriForThumbnailPhoto() throws IOException {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
 
         // Photo being inserted is a thumbnail, so it will only be stored in a BLOB.  The photo URI
@@ -5715,7 +6052,7 @@
     }
 
     public void testWriteNewPhotoToAssetFile() throws Exception {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
 
         // Load in a huge photo.
@@ -5755,7 +6092,7 @@
     }
 
     public void testWriteUpdatedPhotoToAssetFile() throws Exception {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
 
         // Insert a large photo first.
@@ -5826,14 +6163,14 @@
         provider.cleanupPhotoStore();
 
         // Insert a couple of contacts with photos.
-        long rawContactId1 = createRawContactWithName();
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver);
         long contactId1 = queryContactId(rawContactId1);
         long dataId1 = ContentUris.parseId(insertPhoto(rawContactId1, R.drawable.earth_normal));
         long photoFileId1 =
                 getStoredLongValue(ContentUris.withAppendedId(Data.CONTENT_URI, dataId1),
                         Photo.PHOTO_FILE_ID);
 
-        long rawContactId2 = createRawContactWithName();
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver);
         long contactId2 = queryContactId(rawContactId2);
         long dataId2 = ContentUris.parseId(insertPhoto(rawContactId2, R.drawable.earth_normal));
         long photoFileId2 =
@@ -5853,7 +6190,7 @@
 
         // Insert a third raw contact that has a bogus photo file ID.
         long bogusFileId = 1234567;
-        long rawContactId3 = createRawContactWithName();
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver);
         long contactId3 = queryContactId(rawContactId3);
         values.clear();
         values.put(Data.RAW_CONTACT_ID, rawContactId3);
@@ -5867,7 +6204,7 @@
         // Insert a fourth raw contact with a stream item that has a photo, then remove that photo
         // from the photo store.
         Account socialAccount = new Account("social", "social");
-        long rawContactId4 = createRawContactWithName(socialAccount);
+        long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, socialAccount);
         Uri streamItemUri =
                 insertStreamItem(rawContactId4, buildGenericStreamItemValues(), socialAccount);
         long streamItemId = ContentUris.parseId(streamItemUri);
@@ -5967,7 +6304,7 @@
     }
 
     public void testOverwritePhotoWithThumbnail() throws IOException {
-        long rawContactId = createRawContactWithName();
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
         long contactId = queryContactId(rawContactId);
         Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
 
@@ -5995,9 +6332,9 @@
     }
 
     public void testUpdateRawContactSetStarred() {
-        long rawContactId1 = createRawContactWithName();
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver);
         Uri rawContactUri1 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1);
-        long rawContactId2 = createRawContactWithName();
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver);
         Uri rawContactUri2 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId2);
         setAggregationException(
                 AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1, rawContactId2);
@@ -6031,11 +6368,11 @@
     }
 
     public void testSetAndClearSuperPrimaryEmail() {
-        long rawContactId1 = createRawContact(new Account("a", "a"));
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
         Uri mailUri11 = insertEmail(rawContactId1, "test1@domain1.com");
         Uri mailUri12 = insertEmail(rawContactId1, "test2@domain1.com");
 
-        long rawContactId2 = createRawContact(new Account("b", "b"));
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, new Account("b", "b"));
         Uri mailUri21 = insertEmail(rawContactId2, "test1@domain2.com");
         Uri mailUri22 = insertEmail(rawContactId2, "test2@domain2.com");
 
@@ -6132,7 +6469,7 @@
      * are each called from its own test
      */
     public void testChangingPrimary(boolean inUpdate, boolean withSuperPrimary) {
-        long rawContactId = createRawContact(new Account("a", "a"));
+        long rawContactId = RawContactUtil.createRawContact(mResolver, new Account("a", "a"));
         Uri mailUri1 = insertEmail(rawContactId, "test1@domain1.com", true);
 
         if (withSuperPrimary) {
@@ -6186,22 +6523,39 @@
         testChangingPrimary(true, true);
     }
 
+    public void testContactSortOrder() {
+        assertEquals(ContactsColumns.PHONEBOOK_BUCKET_PRIMARY + ", "
+                     + Contacts.SORT_KEY_PRIMARY,
+                     ContactsProvider2.getLocalizedSortOrder(Contacts.SORT_KEY_PRIMARY));
+        assertEquals(ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + ", "
+                     + Contacts.SORT_KEY_ALTERNATIVE,
+                     ContactsProvider2.getLocalizedSortOrder(Contacts.SORT_KEY_ALTERNATIVE));
+        assertEquals(ContactsColumns.PHONEBOOK_BUCKET_PRIMARY + " DESC, "
+                     + Contacts.SORT_KEY_PRIMARY + " DESC",
+                     ContactsProvider2.getLocalizedSortOrder(Contacts.SORT_KEY_PRIMARY + " DESC"));
+        String suffix = " COLLATE LOCALIZED DESC";
+        assertEquals(ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + suffix
+                     + ", " + Contacts.SORT_KEY_ALTERNATIVE + suffix,
+                     ContactsProvider2.getLocalizedSortOrder(Contacts.SORT_KEY_ALTERNATIVE
+                                                             + suffix));
+    }
+
     public void testContactCounts() {
         Uri uri = Contacts.CONTENT_URI.buildUpon()
                 .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
 
-        createRawContact();
-        createRawContactWithName("James", "Sullivan");
-        createRawContactWithName("The Abominable", "Snowman");
-        createRawContactWithName("Mike", "Wazowski");
-        createRawContactWithName("randall", "boggs");
-        createRawContactWithName("Boo", null);
-        createRawContactWithName("Mary", null);
-        createRawContactWithName("Roz", null);
+        RawContactUtil.createRawContact(mResolver);
+        RawContactUtil.createRawContactWithName(mResolver, "James", "Sullivan");
+        RawContactUtil.createRawContactWithName(mResolver, "The Abominable", "Snowman");
+        RawContactUtil.createRawContactWithName(mResolver, "Mike", "Wazowski");
+        RawContactUtil.createRawContactWithName(mResolver, "randall", "boggs");
+        RawContactUtil.createRawContactWithName(mResolver, "Boo", null);
+        RawContactUtil.createRawContactWithName(mResolver, "Mary", null);
+        RawContactUtil.createRawContactWithName(mResolver, "Roz", null);
 
         Cursor cursor = mResolver.query(uri,
                 new String[]{Contacts.DISPLAY_NAME},
-                null, null, Contacts.SORT_KEY_PRIMARY + " COLLATE LOCALIZED");
+                null, null, Contacts.SORT_KEY_PRIMARY);
 
         assertFirstLetterValues(cursor, "", "B", "J", "M", "R", "T");
         assertFirstLetterCounts(cursor,    1,   1,   1,   2,   2,   1);
@@ -6216,6 +6570,32 @@
         cursor.close();
     }
 
+    public void testContactCountsWithGermanNames() {
+        if (!hasGermanCollator()) {
+            return;
+        }
+        ContactLocaleUtils.setLocale(Locale.GERMANY);
+
+        Uri uri = Contacts.CONTENT_URI.buildUpon()
+                .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
+
+        RawContactUtil.createRawContactWithName(mResolver, "Josef", "Sacher");
+        RawContactUtil.createRawContactWithName(mResolver, "Franz", "Schiller");
+        RawContactUtil.createRawContactWithName(mResolver, "Eckart", "Steiff");
+        RawContactUtil.createRawContactWithName(mResolver, "Klaus", "Seiler");
+        RawContactUtil.createRawContactWithName(mResolver, "Lars", "Sultan");
+        RawContactUtil.createRawContactWithName(mResolver, "Heidi", "Rilke");
+        RawContactUtil.createRawContactWithName(mResolver, "Suse", "Thomas");
+
+        Cursor cursor = mResolver.query(uri,
+                new String[]{Contacts.DISPLAY_NAME},
+                null, null, Contacts.SORT_KEY_ALTERNATIVE);
+
+        assertFirstLetterValues(cursor, "R", "S", "Sch", "St", "T");
+        assertFirstLetterCounts(cursor,   1,   3,     1,    1,   1);
+        cursor.close();
+    }
+
     private void assertFirstLetterValues(Cursor cursor, String... expected) {
         String[] actual = cursor.getExtras()
                 .getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
@@ -6293,7 +6673,8 @@
         values.put(RawContacts.ACCOUNT_NAME, red.name);
         values.put(RawContacts.ACCOUNT_TYPE, red.type);
 
-        final Uri insertUri = maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, blue);
+        final Uri insertUri = TestUtil.maybeAddAccountQueryParameters(RawContacts.CONTENT_URI,
+                blue);
         try {
             mResolver.insert(insertUri, values);
             fail("Able to insert RawContact with inconsistent account details");
@@ -6307,7 +6688,7 @@
     }
 
     public void testProviderStatusOnlyLocalContacts() throws Exception {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         assertProviderStatus(ProviderStatus.STATUS_NORMAL);
         mResolver.delete(
                 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), null, null);
@@ -6316,8 +6697,8 @@
 
     public void testProviderStatusWithAccounts() throws Exception {
         assertProviderStatus(ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS);
-        mActor.setAccounts(new Account[]{ACCOUNT_1});
-        ((ContactsProvider2)getProvider()).onAccountsUpdated(new Account[]{ACCOUNT_1});
+        mActor.setAccounts(new Account[]{TestUtil.ACCOUNT_1});
+        ((ContactsProvider2)getProvider()).onAccountsUpdated(new Account[]{TestUtil.ACCOUNT_1});
         assertProviderStatus(ProviderStatus.STATUS_NORMAL);
         mActor.setAccounts(new Account[0]);
         ((ContactsProvider2)getProvider()).onAccountsUpdated(new Account[0]);
@@ -6372,11 +6753,13 @@
     }
 
     private VCardTestUriCreator createVCardTestContacts() {
-        final long rawContactId1 = createRawContact(mAccount, RawContacts.SOURCE_ID, "4:12");
-        insertStructuredName(rawContactId1, "John", "Doe");
+        final long rawContactId1 = RawContactUtil.createRawContact(mResolver, mAccount,
+                RawContacts.SOURCE_ID, "4:12");
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "John", "Doe");
 
-        final long rawContactId2 = createRawContact(mAccount, RawContacts.SOURCE_ID, "3:4%121");
-        insertStructuredName(rawContactId2, "Jane", "Doh");
+        final long rawContactId2 = RawContactUtil.createRawContact(mResolver, mAccount,
+                RawContacts.SOURCE_ID, "3:4%121");
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Jane", "Doh");
 
         final long contactId1 = queryContactId(rawContactId1);
         final long contactId2 = queryContactId(rawContactId2);
@@ -6489,9 +6872,9 @@
         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);
+        long r1 = RawContactUtil.createRawContact(mResolver, mAccount);
+        long r2 = RawContactUtil.createRawContact(mResolver, mAccountTwo);
+        long r3 = RawContactUtil.createRawContact(mResolver, null);
 
         Cursor c = queryGroupMemberships(mAccount);
         try {
@@ -6515,12 +6898,12 @@
     }
 
     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);
+        long r1 = RawContactUtil.createRawContact(mResolver, mAccount);
+        long r2 = RawContactUtil.createRawContact(mResolver, mAccount);
+        long r3 = RawContactUtil.createRawContact(mResolver, mAccount);
+        long r4 = RawContactUtil.createRawContact(mResolver, mAccountTwo);
+        long r5 = RawContactUtil.createRawContact(mResolver, mAccountTwo);
+        long r6 = RawContactUtil.createRawContact(mResolver, null);
 
         assertNoRowsAndClose(queryGroupMemberships(mAccount));
         assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
@@ -6539,13 +6922,13 @@
     // 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);
+        long r1 = RawContactUtil.createRawContact(mResolver, mAccount, RawContacts.STARRED, "1");
+        long r2 = RawContactUtil.createRawContact(mResolver, mAccount);
+        long r3 = RawContactUtil.createRawContact(mResolver, mAccount, RawContacts.STARRED, "1");
+        long r4 = RawContactUtil.createRawContact(mResolver, mAccountTwo, RawContacts.STARRED, "1");
+        long r5 = RawContactUtil.createRawContact(mResolver, mAccountTwo);
+        long r6 = RawContactUtil.createRawContact(mResolver, null, RawContacts.STARRED, "1");
+        long r7 = RawContactUtil.createRawContact(mResolver, null);
 
         assertNoRowsAndClose(queryGroupMemberships(mAccount));
         assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
@@ -6615,9 +6998,9 @@
         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);
+        long r1 = RawContactUtil.createRawContact(mResolver, mAccount, RawContacts.STARRED, "1");
+        long r2 = RawContactUtil.createRawContact(mResolver, mAccount);
+        long r3 = RawContactUtil.createRawContact(mResolver, mAccountTwo);
 
         assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
         Cursor c = queryGroupMemberships(mAccount);
@@ -6690,9 +7073,9 @@
         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);
+        long r1 = RawContactUtil.createRawContact(mResolver, mAccount);
+        long r2 = RawContactUtil.createRawContact(mResolver, mAccount);
+        long r3 = RawContactUtil.createRawContact(mResolver, mAccountTwo);
 
         assertFalse(queryRawContactIsStarred(r1));
         assertFalse(queryRawContactIsStarred(r2));
@@ -6756,7 +7139,7 @@
     }
 
     public void testReadOnlyRawContact() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
         storeValue(rawContactUri, RawContacts.CUSTOM_RINGTONE, "first");
         storeValue(rawContactUri, RawContacts.RAW_CONTACT_IS_READ_ONLY, 1);
@@ -6772,7 +7155,7 @@
     }
 
     public void testReadOnlyDataRow() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         Uri emailUri = insertEmail(rawContactId, "email");
         Uri phoneUri = insertPhoneNumber(rawContactId, "555-1111");
 
@@ -6790,11 +7173,11 @@
     }
 
     public void testContactWithReadOnlyRawContact() {
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         Uri rawContactUri1 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId1);
         storeValue(rawContactUri1, RawContacts.CUSTOM_RINGTONE, "first");
 
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         Uri rawContactUri2 = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId2);
         storeValue(rawContactUri2, RawContacts.CUSTOM_RINGTONE, "second");
         storeValue(rawContactUri2, RawContacts.RAW_CONTACT_IS_READ_ONLY, 1);
@@ -6903,23 +7286,23 @@
 
         final long startTime = sMockClock.currentTimeMillis();
 
-        final long rid1 = createRawContactWithName("contact", "a");
+        final long rid1 = RawContactUtil.createRawContactWithName(mResolver, "contact", "a");
         final long did1a = ContentUris.parseId(insertEmail(rid1, "email_1_a@email.com"));
         final long did1b = ContentUris.parseId(insertEmail(rid1, "email_1_b@email.com"));
         final long did1p = ContentUris.parseId(insertPhoneNumber(rid1, "555-555-5555"));
 
-        final long rid2 = createRawContactWithName("contact", "b");
+        final long rid2 = RawContactUtil.createRawContactWithName(mResolver, "contact", "b");
         final long did2a = ContentUris.parseId(insertEmail(rid2, "email_2_a@email.com"));
         final long did2p = ContentUris.parseId(insertPhoneNumber(rid2, "555-555-5556"));
 
         // Aggregate 1 and 2
         setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rid1, rid2);
 
-        final long rid3 = createRawContactWithName("contact", "c");
+        final long rid3 = RawContactUtil.createRawContactWithName(mResolver, "contact", "c");
         final long did3a = ContentUris.parseId(insertEmail(rid3, "email_3@email.com"));
         final long did3p = ContentUris.parseId(insertPhoneNumber(rid3, "555-3333"));
 
-        final long rid4 = createRawContactWithName("contact", "d");
+        final long rid4 = RawContactUtil.createRawContactWithName(mResolver, "contact", "d");
         final long did4p = ContentUris.parseId(insertPhoneNumber(rid4, "555-4444"));
 
         final long cid1 = queryContactId(rid1);
@@ -7074,8 +7457,271 @@
         assertTrue(mResolver.delete(DataUsageFeedback.DELETE_USAGE_URI, null, null) > 0);
     }
 
+    /*******************************************************
+     * Delta api tests.
+     */
+    public void testContactDelete_hasDeleteLog() {
+        sMockClock.install();
+        long start = sMockClock.currentTimeMillis();
+        DatabaseAsserts.ContactIdPair ids = assertContactCreateDelete();
+        DatabaseAsserts.assertHasDeleteLogGreaterThan(mResolver, ids.mContactId, start);
+
+        // Clean up. Must also remove raw contact.
+        RawContactUtil.delete(mResolver, ids.mRawContactId, true);
+    }
+
+    public void testContactDelete_marksRawContactsForDeletion() {
+        DatabaseAsserts.ContactIdPair ids = assertContactCreateDelete();
+
+        String[] projection = new String[]{ContactsContract.RawContacts.DIRTY,
+                ContactsContract.RawContacts.DELETED};
+        List<String[]> records = RawContactUtil.queryByContactId(mResolver, ids.mContactId,
+                projection);
+        for (String[] arr : records) {
+            assertEquals("1", arr[0]);
+            assertEquals("1", arr[1]);
+        }
+
+        // Clean up
+        RawContactUtil.delete(mResolver, ids.mRawContactId, true);
+    }
+
+    public void testContactUpdate_updatesContactUpdatedTimestamp() {
+        sMockClock.install();
+        DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
+
+        long baseTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+
+        ContentValues values = new ContentValues();
+        values.put(ContactsContract.Contacts.STARRED, 1);
+
+        sMockClock.advance();
+        ContactUtil.update(mResolver, ids.mContactId, values);
+
+        long newTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+        assertTrue(newTime > baseTime);
+
+        // Clean up
+        RawContactUtil.delete(mResolver, ids.mRawContactId, true);
+    }
+
+    // This implicitly tests the Contact create case.
+    public void testRawContactCreate_updatesContactUpdatedTimestamp() {
+        long startTime = System.currentTimeMillis();
+
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver);
+        long lastUpdated = getContactLastUpdatedTimestampByRawContactId(mResolver, rawContactId);
+
+        assertTrue(lastUpdated > startTime);
+
+        // Clean up
+        RawContactUtil.delete(mResolver, rawContactId, true);
+    }
+
+    public void testRawContactUpdate_updatesContactUpdatedTimestamp() {
+        DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
+
+        long baseTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+
+        ContentValues values = new ContentValues();
+        values.put(ContactsContract.RawContacts.STARRED, 1);
+        RawContactUtil.update(mResolver, ids.mRawContactId, values);
+
+        long newTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+        assertTrue(newTime > baseTime);
+
+        // Clean up
+        RawContactUtil.delete(mResolver, ids.mRawContactId, true);
+    }
+
+    public void testRawContactPsuedoDelete_hasDeleteLogForContact() {
+        DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
+
+        long baseTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+
+        RawContactUtil.delete(mResolver, ids.mRawContactId, false);
+
+        DatabaseAsserts.assertHasDeleteLogGreaterThan(mResolver, ids.mContactId, baseTime);
+
+        // clean up
+        RawContactUtil.delete(mResolver, ids.mRawContactId, true);
+    }
+
+    public void testRawContactDelete_hasDeleteLogForContact() {
+        DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
+
+        long baseTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+
+        RawContactUtil.delete(mResolver, ids.mRawContactId, true);
+
+        DatabaseAsserts.assertHasDeleteLogGreaterThan(mResolver, ids.mContactId, baseTime);
+
+        // already clean
+    }
+
+    private long getContactLastUpdatedTimestampByRawContactId(ContentResolver resolver,
+            long rawContactId) {
+        long contactId = RawContactUtil.queryContactIdByRawContactId(mResolver, rawContactId);
+        MoreAsserts.assertNotEqual(CommonDatabaseUtils.NOT_FOUND, contactId);
+
+        return ContactUtil.queryContactLastUpdatedTimestamp(mResolver, contactId);
+    }
+
+    public void testDataInsert_updatesContactLastUpdatedTimestamp() {
+        sMockClock.install();
+        DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
+        long baseTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+
+        sMockClock.advance();
+        insertPhoneNumberAndReturnDataId(ids.mRawContactId);
+
+        long newTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+        assertTrue(newTime > baseTime);
+
+        // Clean up
+        RawContactUtil.delete(mResolver, ids.mRawContactId, true);
+    }
+
+    public void testDataDelete_updatesContactLastUpdatedTimestamp() {
+        sMockClock.install();
+        DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
+
+        long dataId = insertPhoneNumberAndReturnDataId(ids.mRawContactId);
+
+        long baseTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+
+        sMockClock.advance();
+        DataUtil.delete(mResolver, dataId);
+
+        long newTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+        assertTrue(newTime > baseTime);
+
+        // Clean up
+        RawContactUtil.delete(mResolver, ids.mRawContactId, true);
+    }
+
+    public void testDataUpdate_updatesContactLastUpdatedTimestamp() {
+        sMockClock.install();
+        DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
+
+        long dataId = insertPhoneNumberAndReturnDataId(ids.mRawContactId);
+
+        long baseTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+
+        sMockClock.advance();
+        ContentValues values = new ContentValues();
+        values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, "555-5555");
+        DataUtil.update(mResolver, dataId, values);
+
+        long newTime = ContactUtil.queryContactLastUpdatedTimestamp(mResolver, ids.mContactId);
+        assertTrue(newTime > baseTime);
+
+        // Clean up
+        RawContactUtil.delete(mResolver, ids.mRawContactId, true);
+    }
+
+    private long insertPhoneNumberAndReturnDataId(long rawContactId) {
+        Uri uri = insertPhoneNumber(rawContactId, "1-800-GOOG-411");
+        return ContentUris.parseId(uri);
+    }
+
+    public void testDeletedContactsDelete_isUnsupported() {
+        final Uri URI = ContactsContract.DeletedContacts.CONTENT_URI;
+        DatabaseAsserts.assertDeleteIsUnsupported(mResolver, URI);
+
+        Uri uri = ContentUris.withAppendedId(URI, 1L);
+        DatabaseAsserts.assertDeleteIsUnsupported(mResolver, uri);
+    }
+
+    public void testDeletedContactsInsert_isUnsupported() {
+        final Uri URI = ContactsContract.DeletedContacts.CONTENT_URI;
+        DatabaseAsserts.assertInsertIsUnsupported(mResolver, URI);
+    }
+
+
+    public void testQueryDeletedContactsByContactId() {
+        DatabaseAsserts.ContactIdPair ids = assertContactCreateDelete();
+
+        MoreAsserts.assertNotEqual(CommonDatabaseUtils.NOT_FOUND,
+                DeletedContactUtil.queryDeletedTimestampForContactId(mResolver, ids.mContactId));
+    }
+
+    public void testQueryDeletedContactsAll() {
+        final int numDeletes = 10;
+
+        // Since we cannot clean out delete log from previous tests, we need to account for that
+        // by querying for the count first.
+        final long startCount = DeletedContactUtil.getCount(mResolver);
+
+        for (int i = 0; i < numDeletes; i++) {
+            assertContactCreateDelete();
+        }
+
+        final long endCount = DeletedContactUtil.getCount(mResolver);
+
+        assertEquals(numDeletes, endCount - startCount);
+    }
+
+    public void testQueryDeletedContactsSinceTimestamp() {
+        sMockClock.install();
+
+        // Before
+        final HashSet<Long> beforeIds = new HashSet<Long>();
+        beforeIds.add(assertContactCreateDelete().mContactId);
+        beforeIds.add(assertContactCreateDelete().mContactId);
+
+        final long start = sMockClock.currentTimeMillis();
+
+        // After
+        final HashSet<Long> afterIds = new HashSet<Long>();
+        afterIds.add(assertContactCreateDelete().mContactId);
+        afterIds.add(assertContactCreateDelete().mContactId);
+        afterIds.add(assertContactCreateDelete().mContactId);
+
+        final String[] projection = new String[]{
+                ContactsContract.DeletedContacts.CONTACT_ID,
+                ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP
+        };
+        final List<String[]> records = DeletedContactUtil.querySinceTimestamp(mResolver, projection,
+                start);
+        for (String[] record : records) {
+            // Check ids to make sure we only have the ones that came after the time.
+            final long contactId = Long.parseLong(record[0]);
+            assertFalse(beforeIds.contains(contactId));
+            assertTrue(afterIds.contains(contactId));
+
+            // Check times to make sure they came after
+            assertTrue(Long.parseLong(record[1]) > start);
+        }
+    }
+
+    /**
+     * Create a contact. Assert it's not present in the delete log. Delete it.
+     * And assert that the contact record is no longer present.
+     *
+     * @return The contact id and raw contact id that was created.
+     */
+    private DatabaseAsserts.ContactIdPair assertContactCreateDelete() {
+        DatabaseAsserts.ContactIdPair ids = DatabaseAsserts.assertAndCreateContact(mResolver);
+
+        assertEquals(CommonDatabaseUtils.NOT_FOUND,
+                DeletedContactUtil.queryDeletedTimestampForContactId(mResolver, ids.mContactId));
+
+        sMockClock.advance();
+        ContactUtil.delete(mResolver, ids.mContactId);
+
+        assertFalse(ContactUtil.recordExistsForContactId(mResolver, ids.mContactId));
+
+        return ids;
+    }
+    /**
+     * End delta api tests.
+     ******************************************************/
+
+
     private Cursor queryGroupMemberships(Account account) {
-        Cursor c = mResolver.query(maybeAddAccountQueryParameters(Data.CONTENT_URI, account),
+        Cursor c = mResolver.query(TestUtil.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);
@@ -7127,7 +7773,7 @@
             long groupId, int chatMode) {
         long rawContactId = createRawContact(values, phoneNumber, email, presenceStatus,
                 timesContacted, starred, groupId, chatMode);
-        insertStructuredName(rawContactId, firstName, givenName);
+        DataUtil.insertStructuredName(mResolver, rawContactId, firstName, givenName);
         return rawContactId;
     }
 
@@ -7136,7 +7782,7 @@
             long groupId, int chatMode, boolean isUserProfile) {
         long rawContactId = createRawContact(values, phoneNumber, email, presenceStatus,
                 timesContacted, starred, groupId, chatMode, isUserProfile);
-        insertStructuredName(rawContactId, firstName, givenName);
+        DataUtil.insertStructuredName(mResolver, rawContactId, firstName, givenName);
         return rawContactId;
     }
 
@@ -7266,4 +7912,34 @@
                 .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType)
                 .build(), new ContentValues(), null, null);
     }
+
+    private boolean hasChineseCollator() {
+        final Locale locale[] = Collator.getAvailableLocales();
+        for (int i = 0; i < locale.length; i++) {
+            if (locale[i].equals(Locale.CHINA)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasJapaneseCollator() {
+        final Locale locale[] = Collator.getAvailableLocales();
+        for (int i = 0; i < locale.length; i++) {
+            if (locale[i].equals(Locale.JAPAN)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasGermanCollator() {
+        final Locale locale[] = Collator.getAvailableLocales();
+        for (int i = 0; i < locale.length; i++) {
+            if (locale[i].equals(Locale.GERMANY)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/tests/src/com/android/providers/contacts/DirectoryTest.java b/tests/src/com/android/providers/contacts/DirectoryTest.java
index c62824b..99f05ce 100644
--- a/tests/src/com/android/providers/contacts/DirectoryTest.java
+++ b/tests/src/com/android/providers/contacts/DirectoryTest.java
@@ -28,6 +28,8 @@
 import android.provider.ContactsContract.Directory;
 import android.test.suitebuilder.annotation.MediumTest;
 
+import com.android.providers.contacts.testutil.RawContactUtil;
+
 
 /**
  * Unit tests for {@link ContactsProvider2}, directory functionality.
@@ -74,7 +76,8 @@
     }
 
     public void testForwardingToLocalContacts() {
-        long contactId = queryContactId(createRawContactWithName("John", "Doe"));
+        long contactId = queryContactId(RawContactUtil.createRawContactWithName(mResolver, "John",
+                "Doe"));
 
         Uri contentUri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
                 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)).build();
@@ -92,13 +95,14 @@
     public void testForwardingToLocalInvisibleContacts() {
 
         // Visible because there is no account
-        long contactId1 = queryContactId(createRawContactWithName("Bob", "Parr"));
+        long contactId1 = queryContactId(RawContactUtil.createRawContactWithName(mResolver, "Bob",
+                "Parr"));
 
         Account account = new Account("accountName", "accountType");
         long groupId = createGroup(account, "sid", "def",
                 0 /* visible */,  true /* auto-add */, false /* fav */);
-        long contactId2 = queryContactId(createRawContactWithName("Helen", "Parr",
-                account));
+        long contactId2 = queryContactId(RawContactUtil.createRawContactWithName(mResolver, "Helen",
+                "Parr", account));
 
         Uri contentUri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
                 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.LOCAL_INVISIBLE))
diff --git a/tests/src/com/android/providers/contacts/FastScrollingIndexCacheTest.java b/tests/src/com/android/providers/contacts/FastScrollingIndexCacheTest.java
index 281834f..bda7cc9 100644
--- a/tests/src/com/android/providers/contacts/FastScrollingIndexCacheTest.java
+++ b/tests/src/com/android/providers/contacts/FastScrollingIndexCacheTest.java
@@ -54,7 +54,7 @@
         super.setUp();
 
         mPrefs = new MockSharedPreferences();
-        mCache = new FastScrollingIndexCache(mPrefs);
+        mCache = FastScrollingIndexCache.getInstanceForTest(mPrefs);
     }
 
     private void assertBundle(String[] expectedTitles, int[] expectedCounts, Bundle actual) {
@@ -141,7 +141,7 @@
         // Now, create a new cache instance (with the same shared preferences)
         // It should restore the cache content from the preferences...
 
-        FastScrollingIndexCache cache2 = new FastScrollingIndexCache(mPrefs);
+        FastScrollingIndexCache cache2 = FastScrollingIndexCache.getInstanceForTest(mPrefs);
         assertBundle(TITLES_0, COUNTS_0, cache2.get(null, null, null, null, null));
         assertBundle(TITLES_1, COUNTS_1, cache2.get(URI_A, "*s*", PROJECTION_0, "*so*", "*ce*"));
         assertBundle(TITLES_2, COUNTS_2, cache2.get(URI_A, "*s*", PROJECTION_1, "*so*", "*ce*"));
diff --git a/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java b/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
index f63845f..8036289 100644
--- a/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
+++ b/tests/src/com/android/providers/contacts/GlobalSearchSupportTest.java
@@ -25,10 +25,12 @@
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.StatusUpdates;
 import android.test.suitebuilder.annotation.MediumTest;
 
+import com.android.providers.contacts.testutil.DataUtil;
+import com.android.providers.contacts.testutil.RawContactUtil;
+
 /**
  * Unit tests for {@link GlobalSearchSupport}.
  * <p>
@@ -48,8 +50,8 @@
         // Creating an AUTO_ADD group will exclude all ungrouped contacts from global search
         createGroup(account, "any", "any", 0 /* visible */, true /* auto-add */, false /* fav */);
 
-        long rawContactId = createRawContact(account);
-        insertStructuredName(rawContactId, "Deer", "Dough");
+        long rawContactId = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId, "Deer", "Dough");
 
         // Remove the new contact from all groups
         mResolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=" + rawContactId
diff --git a/tests/src/com/android/providers/contacts/GroupsTest.java b/tests/src/com/android/providers/contacts/GroupsTest.java
index 15cfc71..fa4e3ff 100644
--- a/tests/src/com/android/providers/contacts/GroupsTest.java
+++ b/tests/src/com/android/providers/contacts/GroupsTest.java
@@ -34,6 +34,8 @@
 
 import com.google.android.collect.Lists;
 
+import com.android.providers.contacts.testutil.RawContactUtil;
+
 import java.util.ArrayList;
 
 /**
@@ -254,7 +256,7 @@
         final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
 
         // Create contact with specific membership
-        final long rawContactId = this.createRawContact(sTestAccount);
+        final long rawContactId = RawContactUtil.createRawContact(this.mResolver, sTestAccount);
         final long contactId = this.queryContactId(rawContactId);
         final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
 
@@ -277,7 +279,7 @@
     }
 
     public void testLocalSingleVisible() {
-        final long rawContactId = this.createRawContact();
+        final long rawContactId = RawContactUtil.createRawContact(this.mResolver);
 
         // Single, local contacts should always be visible
         assertRawContactVisible(rawContactId, true);
@@ -285,8 +287,8 @@
 
     public void testLocalMixedVisible() {
         // Aggregate, when mixed with local, should become visible
-        final long rawContactId1 = this.createRawContact();
-        final long rawContactId2 = this.createRawContact(sTestAccount);
+        final long rawContactId1 = RawContactUtil.createRawContact(this.mResolver);
+        final long rawContactId2 = RawContactUtil.createRawContact(this.mResolver, sTestAccount);
 
         final long groupId = this.createGroup(sTestAccount, GROUP_ID, GROUP_ID, 0);
         this.insertGroupMembership(rawContactId2, groupId);
@@ -308,7 +310,7 @@
     }
 
     public void testUngroupedVisible() {
-        final long rawContactId = this.createRawContact(sTestAccount);
+        final long rawContactId = RawContactUtil.createRawContact(this.mResolver, sTestAccount);
 
         final ContentValues values = new ContentValues();
         values.put(Settings.ACCOUNT_NAME, sTestAccount.name);
@@ -329,8 +331,8 @@
     }
 
     public void testMultipleSourcesVisible() {
-        final long rawContactId1 = this.createRawContact(sTestAccount);
-        final long rawContactId2 = this.createRawContact(sSecondAccount);
+        final long rawContactId1 = RawContactUtil.createRawContact(this.mResolver, sTestAccount);
+        final long rawContactId2 = RawContactUtil.createRawContact(this.mResolver, sSecondAccount);
 
         final long groupId = this.createGroup(sTestAccount, GROUP_ID, GROUP_ID, 0);
         this.insertGroupMembership(rawContactId1, groupId);
diff --git a/tests/src/com/android/providers/contacts/HanziToPinyinTest.java b/tests/src/com/android/providers/contacts/HanziToPinyinTest.java
index dc01b91..4db1d34 100644
--- a/tests/src/com/android/providers/contacts/HanziToPinyinTest.java
+++ b/tests/src/com/android/providers/contacts/HanziToPinyinTest.java
@@ -16,17 +16,16 @@
 
 package com.android.providers.contacts;
 
+import android.os.SystemClock;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.providers.contacts.HanziToPinyin.Token;
 
 import junit.framework.TestCase;
 
-import java.text.Collator;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Locale;
 
 @SmallTest
 public class HanziToPinyinTest extends TestCase {
@@ -36,22 +35,48 @@
     private final static String ONE_UNKNOWN = "\uFF71";
     private final static String MISC = "test\u675C   Test with space\uFF71\uFF71\u675C";
 
+    private static int testCount = 0;
+    private static long startTime = 0;
+
+    private boolean hasChineseTransliterator() {
+        return HanziToPinyin.getInstance().hasChineseTransliterator();
+    }
+
     private void test(final char hanzi, final String expectedPinyin) throws Exception {
+        if (startTime == 0) {
+            startTime = SystemClock.elapsedRealtime();
+        }
         final String hanziString = Character.toString(hanzi);
         ArrayList<Token> tokens = HanziToPinyin.getInstance().get(hanziString);
         assertEquals(tokens.size(), 1);
+        final String newString = tokens.get(0).target;
         if (TextUtils.isEmpty(expectedPinyin)) {
-            assertEquals(tokens.get(0).type, Token.UNKNOWN);
-            assertTrue(tokens.get(0).target.equals(hanziString));
+            assertEquals("Expected no transliteration for '" + hanziString
+                         + "' but got '" + newString + "'",
+                         tokens.get(0).type, Token.UNKNOWN);
+            assertTrue("Expected to get back original string for '"
+                       + hanziString  + "' but got '" + newString + "'",
+                       newString.equals(hanziString));
         } else {
-            assertEquals(tokens.get(0).type, Token.PINYIN);
-            assertTrue(tokens.get(0).target.equalsIgnoreCase(expectedPinyin));
+            assertEquals("Expected transliteration for '" + hanziString
+                         + "' of '" + expectedPinyin + "' but got none",
+                         tokens.get(0).type, Token.PINYIN);
+            assertTrue("Expected transliteration for '" + hanziString + "' of '"
+                       + expectedPinyin + "' but got '" + newString + "'",
+                       newString.equalsIgnoreCase(expectedPinyin));
+        }
+        ++testCount;
+        if ((testCount%1000) == 0) {
+            long elapsedTimeMS = SystemClock.elapsedRealtime() - startTime;
+            Log.i("HanziToPinyinTest", "Transliteration calls: " + testCount
+                  + " [" + elapsedTimeMS + "ms total/"
+                  + (float) elapsedTimeMS/testCount + "ms avg]");
         }
     }
 
     @SmallTest
     public void testGetToken() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         ArrayList<Token> tokens = HanziToPinyin.getInstance().get(ONE_HANZI);
@@ -90,7 +115,7 @@
      */
     @SmallTest
     public void test_0() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u4e00', "YI");
@@ -1121,7 +1146,7 @@
 
     @SmallTest
     public void test_1() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u5200', "DAO");
@@ -2152,7 +2177,7 @@
 
     @SmallTest
     public void test_2() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u5600', "DI");
@@ -3183,7 +3208,7 @@
 
     @SmallTest
     public void test_3() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u5a00', "SONG");
@@ -4214,7 +4239,7 @@
 
     @SmallTest
     public void test_4() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u5e00', "ZA");
@@ -5245,7 +5270,7 @@
 
     @SmallTest
     public void test_5() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u6200', "LIAN");
@@ -6276,7 +6301,7 @@
 
     @SmallTest
     public void test_6() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u6600', "YUN");
@@ -7307,7 +7332,7 @@
 
     @SmallTest
     public void test_7() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u6a00', "DI");
@@ -8338,7 +8363,7 @@
 
     @SmallTest
     public void test_8() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u6e00', "BEN");
@@ -9369,7 +9394,7 @@
 
     @SmallTest
     public void test_9() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u7200', "HE");
@@ -10241,7 +10266,7 @@
         test('\u7562', "BI");
         test('\u7563', "DA");
         test('\u7564', "ZHI");
-        test('\u7565', "E");
+        test('\u7565', "LUE");
         test('\u7566', "QI");
         test('\u7567', "LUE");
         test('\u7568', "PAN");
@@ -10400,7 +10425,7 @@
 
     @SmallTest
     public void test_10() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u7600', "YU");
@@ -11431,7 +11456,7 @@
 
     @SmallTest
     public void test_11() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u7a00', "XI");
@@ -12462,7 +12487,7 @@
 
     @SmallTest
     public void test_12() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u7e00', "XIA");
@@ -13493,7 +13518,7 @@
 
     @SmallTest
     public void test_13() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u8200', "YAO");
@@ -14524,7 +14549,7 @@
 
     @SmallTest
     public void test_14() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u8600', "TUO");
@@ -15555,7 +15580,7 @@
 
     @SmallTest
     public void test_15() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u8a00', "YAN");
@@ -16586,7 +16611,7 @@
 
     @SmallTest
     public void test_16() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u8e00', "CHU");
@@ -17617,7 +17642,7 @@
 
     @SmallTest
     public void test_17() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u9200', "BA");
@@ -18648,7 +18673,7 @@
 
     @SmallTest
     public void test_18() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u9600', "FA");
@@ -19679,7 +19704,7 @@
 
     @SmallTest
     public void test_19() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u9a00', "E");
@@ -20710,7 +20735,7 @@
 
     @SmallTest
     public void test_20() throws Exception {
-        if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
+        if (!hasChineseTransliterator()) {
             return;
         }
         test('\u9e00', "CHU");
diff --git a/tests/src/com/android/providers/contacts/SearchIndexManagerTest.java b/tests/src/com/android/providers/contacts/SearchIndexManagerTest.java
index 3abdc3f..dd1bc71 100644
--- a/tests/src/com/android/providers/contacts/SearchIndexManagerTest.java
+++ b/tests/src/com/android/providers/contacts/SearchIndexManagerTest.java
@@ -29,6 +29,9 @@
 import android.test.suitebuilder.annotation.MediumTest;
 import android.test.suitebuilder.annotation.Suppress;
 
+import com.android.providers.contacts.testutil.DataUtil;
+import com.android.providers.contacts.testutil.RawContactUtil;
+
 import java.text.Collator;
 import java.util.Arrays;
 import java.util.Locale;
@@ -46,12 +49,12 @@
 public class SearchIndexManagerTest extends BaseContactsProvider2Test {
 
     public void testSearchIndexForStructuredName() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
-        insertStructuredName(rawContactId, "John", "Doe");
+        DataUtil.insertStructuredName(mResolver, rawContactId, "John", "Doe");
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, "Bob I. Parr");
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
         values.clear();
         values.put(StructuredName.PREFIX, "Mrs.");
         values.put(StructuredName.GIVEN_NAME, "Helen");
@@ -60,7 +63,7 @@
         values.put(StructuredName.SUFFIX, "PhD");
         values.put(StructuredName.PHONETIC_FAMILY_NAME, "par");
         values.put(StructuredName.PHONETIC_GIVEN_NAME, "helen");
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         assertSearchIndex(
                 contactId, null, "John Doe Bob I Parr Helen I Parr PhD par helen parhelen", null);
@@ -72,11 +75,11 @@
             return;
         }
 
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, "\u695A\u8FAD");    // CHUCI
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         assertSearchIndex(
                 contactId, null, "\u695A\u8FAD \u695A\u8FAD CI \u8FAD CHUCI CC C", null);
@@ -87,11 +90,12 @@
         if (!Arrays.asList(Collator.getAvailableLocales()).contains(Locale.CHINA)) {
             return;
         }
+        ContactLocaleUtils.setLocale(Locale.SIMPLIFIED_CHINESE);
 
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, "\u695A\u8FAD");    // CHUCI
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         assertStoredValue(buildSearchUri("\u695A\u8FAD"), SearchSnippetColumns.SNIPPET, null);
         assertStoredValue(buildSearchUri("\u8FAD"), SearchSnippetColumns.SNIPPET, null);
@@ -107,11 +111,11 @@
             return;
         }
 
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, "\uC774\uC0C1\uC77C");    // Lee Sang Il
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         assertSearchIndex(contactId, null,
                 "\uC774\uC0C1\uC77C \uC0C1\uC77C \u1109\u110B \u110B\u1109\u110B", null);
@@ -123,10 +127,10 @@
             return;
         }
 
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, "\uC774\uC0C1\uC77C");   // Lee Sang Il
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         // Full name: Lee Sang Il
         assertStoredValue(buildSearchUri("\uC774\uC0C1\uC77C"), SearchSnippetColumns.SNIPPET, null);
@@ -147,13 +151,13 @@
             return;
         }
 
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
 
         // Sun Woo Young Nyeu
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, "\uC120\uC6B0\uC6A9\uB140");
 
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
 
         // Full name: Sun Woo Young Nyeu
         assertStoredValue(
@@ -171,7 +175,7 @@
     }
 
     public void testSearchIndexForOrganization() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         ContentValues values = new ContentValues();
         values.put(Organization.COMPANY, "Acme Inc.");
@@ -189,7 +193,7 @@
     }
 
     public void testSearchIndexForPhoneNumber() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         insertPhoneNumber(rawContactId, "800555GOOG");
         insertPhoneNumber(rawContactId, "8005551234");
@@ -198,7 +202,7 @@
     }
 
     public void testSearchIndexForEmail() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         insertEmail(rawContactId, "Bob Parr <incredible@android.com>");
         insertEmail(rawContactId, "bob_parr@android.com");
@@ -208,7 +212,7 @@
     }
 
     public void testSearchIndexForNickname() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         insertNickname(rawContactId, "incredible");
 
@@ -216,7 +220,7 @@
     }
 
     public void testSearchIndexForStructuredPostal() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         insertPostalAddress(rawContactId, "1600 Amphitheatre Pkwy\nMountain View, CA 94043");
         ContentValues values = new ContentValues();
@@ -231,7 +235,7 @@
     }
 
     public void testSearchIndexForIm() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         insertImHandle(rawContactId, Im.PROTOCOL_JABBER, null, "bp@android.com");
         insertImHandle(rawContactId, Im.PROTOCOL_CUSTOM, "android_im", "android@android.com");
@@ -241,7 +245,7 @@
     }
 
     public void testSearchIndexForNote() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
         insertNote(rawContactId, "Please note: three notes or more make up a chord.");
 
@@ -250,7 +254,7 @@
     }
 
     public void testSnippetArgs() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         insertNote(rawContactId, "Please note: three notes or more make up a chord.");
 
         assertStoredValue(
@@ -259,12 +263,12 @@
     }
 
     public void testEmptyFilter() {
-        createRawContactWithName("John", "Doe");
+        RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
         assertEquals(0, getCount(buildSearchUri(""), null, null));
     }
 
     public void testSearchByName() {
-        createRawContactWithName("John Jay", "Doe");
+        RawContactUtil.createRawContactWithName(mResolver, "John Jay", "Doe");
 
         // We are supposed to find the contact, but return a null snippet
         assertStoredValue(buildSearchUri("john"), SearchSnippetColumns.SNIPPET, null);
@@ -273,7 +277,7 @@
     }
 
     public void testSearchByPrefixName() {
-        createRawContactWithName("John Jay", "Doe");
+        RawContactUtil.createRawContactWithName(mResolver, "John Jay", "Doe");
 
         // prefix searches
         assertStoredValue(buildSearchUri("jo ja"), SearchSnippetColumns.SNIPPET, null);
@@ -282,7 +286,7 @@
     }
 
     public void testGermanUmlautFullameCapitalizationSearch() {
-        createRawContactWithName("Matthäus BJÖRN Bünyamin", "Reißer");
+        RawContactUtil.createRawContactWithName(mResolver, "Matthäus BJÖRN Bünyamin", "Reißer");
 
         // make sure we can find those, independent of the capitalization
         assertStoredValue(buildSearchUri("matthäus"), SearchSnippetColumns.SNIPPET, null);
@@ -370,7 +374,7 @@
     }
 
     public void testNameWithHyphen() {
-        createRawContactWithName("First", "Last-name");
+        RawContactUtil.createRawContactWithName(mResolver, "First", "Last-name");
 
         assertStoredValue(buildSearchUri("First"), SearchSnippetColumns.SNIPPET, null);
         assertStoredValue(buildSearchUri("Last"), SearchSnippetColumns.SNIPPET, null);
@@ -387,7 +391,7 @@
 
     /** Same as {@link #testNameWithHyphen} except the name has double hyphens. */
     public void testNameWithDoubleHyphens() {
-        createRawContactWithName("First", "Last--name");
+        RawContactUtil.createRawContactWithName(mResolver, "First", "Last--name");
 
         assertStoredValue(buildSearchUri("First"), SearchSnippetColumns.SNIPPET, null);
         assertStoredValue(buildSearchUri("Last"), SearchSnippetColumns.SNIPPET, null);
@@ -400,7 +404,7 @@
     }
 
     public void testNameWithPunctuations() {
-        createRawContactWithName("First", "O'Neill");
+        RawContactUtil.createRawContactWithName(mResolver, "First", "O'Neill");
 
         assertStoredValue(buildSearchUri("first"), SearchSnippetColumns.SNIPPET, null);
         assertStoredValue(buildSearchUri("oneill"), SearchSnippetColumns.SNIPPET, null);
@@ -408,7 +412,7 @@
     }
 
     public void testSearchByEmailAddress() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         insertPhoneNumber(rawContactId, "1234567890");
         insertEmail(rawContactId, "john@doe.com");
         insertNote(rawContactId, "a hundred dollar note for doe@john.com and bob parr");
@@ -421,7 +425,7 @@
     }
 
     public void testSearchByPhoneNumber() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         insertPhoneNumber(rawContactId, "330142685300");
         insertPhoneNumber(rawContactId, "(800)GOOG-123");
         insertEmail(rawContactId, "john@doe.com");
@@ -443,7 +447,7 @@
      * Test case for bug 5904515
      */
     public void testSearchByPhoneNumber_diferSnippetting() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         insertPhoneNumber(rawContactId, "505-123-4567");
 
         // The bug happened with the old code only when we use \u0001 as the snippet marker.
@@ -461,7 +465,7 @@
      * there's no visible breakage.)
      */
     public void testSearchByEmail_diferSnippetting() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         insertEmail(rawContactId, "john@doe.com");
 
         assertStoredValue(buildSearchUri("john", "\u0001,\u0001,\u2026,5", true),
@@ -505,10 +509,10 @@
     }
 
     private void createRawContactWithDisplayName(String name) {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, name);
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
     }
 
     // TODO: expectedName must be tested. Many tests in here are quite useless at the moment
diff --git a/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java b/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java
index 7b3fe95..e7b80a0 100644
--- a/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java
+++ b/tests/src/com/android/providers/contacts/SqlInjectionDetectionTest.java
@@ -27,6 +27,8 @@
 import android.provider.ContactsContract.Contacts;
 import android.test.suitebuilder.annotation.MediumTest;
 
+import com.android.providers.contacts.testutil.RawContactUtil;
+
 /**
  * Unit tests for {@link ContactsProvider2}, to make sure the queries don't allow sql injection.
  *
@@ -41,7 +43,7 @@
     private static final String[] PHONE_ID_PROJECTION = new String[] { Phone._ID };
 
     public void testPhoneQueryValid() {
-        long rawContactId = createRawContactWithName("Hot", "Tamale");
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
         insertPhoneNumber(rawContactId, "555-123-4567");
 
         assertQueryValid(Phone.CONTENT_URI, PHONE_ID_PROJECTION,
@@ -49,7 +51,7 @@
     }
 
     public void testPhoneQueryBadProjection() {
-        long rawContactId = createRawContactWithName("Hot", "Tamale");
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
         insertPhoneNumber(rawContactId, "555-123-4567");
 
         assertQueryThrows(IllegalArgumentException.class, Phone.CONTENT_URI,
@@ -57,7 +59,7 @@
     }
 
     public void testPhoneQueryBadSelection() {
-        long rawContactId = createRawContactWithName("Hot", "Tamale");
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
         insertPhoneNumber(rawContactId, "555-123-4567");
 
         assertQueryThrows(SQLiteException.class, Phone.CONTENT_URI, PHONE_ID_PROJECTION,
@@ -65,7 +67,7 @@
     }
 
     public void testPhoneQueryBadSortOrder() {
-        long rawContactId = createRawContactWithName("Hot", "Tamale");
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
         insertPhoneNumber(rawContactId, "555-123-4567");
 
         assertQueryThrows(SQLiteException.class, Phone.CONTENT_URI,
@@ -74,7 +76,7 @@
 
     public void testPhoneQueryBadLimit() {
         // Non-numeric query parameters are ignored by the provider
-        long rawContactId = createRawContactWithName("Hot", "Tamale");
+        long rawContactId = RawContactUtil.createRawContactWithName(mResolver, "Hot", "Tamale");
         insertPhoneNumber(rawContactId, "555-123-4567");
 
         Builder builder = Contacts.CONTENT_FILTER_URI.buildUpon();
diff --git a/tests/src/com/android/providers/contacts/TransactionContextTest.java b/tests/src/com/android/providers/contacts/TransactionContextTest.java
new file mode 100644
index 0000000..084a51f
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/TransactionContextTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Unit tests for TransactionContext.
+ */
+@SmallTest
+public class TransactionContextTest extends TestCase {
+
+    public void testClearExceptSearchIndexUpdates_returnsNewSets() {
+        TransactionContext context = new TransactionContext(false);
+        context.markRawContactDirtyAndChanged(1L, false);
+        context.rawContactUpdated(1L);
+        context.rawContactInserted(1L, 1L);
+        context.syncStateUpdated(1L, new Object());
+
+        context.clearExceptSearchIndexUpdates();
+
+        Set<Long> newDirty = context.getDirtyRawContactIds();
+        Set<Long> newChanged = context.getChangedRawContactIds();
+        Set<Long> newInserted = context.getInsertedRawContactIds();
+        Set<Long> newUpdated = context.getUpdatedRawContactIds();
+        Set<Map.Entry<Long, Object>> newSync = context.getUpdatedSyncStates();
+
+        assertTrue(newDirty.isEmpty());
+        assertTrue(newChanged.isEmpty());
+        assertTrue(newInserted.isEmpty());
+        assertTrue(newUpdated.isEmpty());
+        assertTrue(newSync.isEmpty());
+    }
+
+    public void testMarkDirtyAndChanged_onlyUpdatesChanged() {
+        TransactionContext context = new TransactionContext(false);
+
+        context.markRawContactDirtyAndChanged(1L, true /* isSyncAdapter */);
+
+        assertEquals(1, context.getChangedRawContactIds().size());
+        assertEquals(0, context.getDirtyRawContactIds().size());
+    }
+
+    public void testMarkDirtyAndChanged_onlyUpdatesDirtyAndChanged() {
+        TransactionContext context = new TransactionContext(false);
+
+        context.markRawContactDirtyAndChanged(1L, false /* isSyncAdapter */);
+
+        assertEquals(1, context.getChangedRawContactIds().size());
+        assertEquals(1, context.getDirtyRawContactIds().size());
+    }
+
+    public void testRawContactInserted_affectsChangedContacts() {
+        TransactionContext context = new TransactionContext(false);
+        assertTrue(context.getChangedRawContactIds().isEmpty());
+
+        context.rawContactInserted(1L, 2L);
+        assertEquals(1, context.getChangedRawContactIds().size());
+        assertTrue(context.getChangedRawContactIds().contains(1L));
+
+        context.rawContactInserted(5L, 10L);
+        assertEquals(2, context.getChangedRawContactIds().size());
+        assertTrue(context.getChangedRawContactIds().contains(5L));
+    }
+
+    public void testMarkRawContactChangedOrDeletedOrInserted_affectsChangedContacts() {
+        TransactionContext context = new TransactionContext(false);
+        assertTrue(context.getChangedRawContactIds().isEmpty());
+
+        context.markRawContactChangedOrDeletedOrInserted(1L);
+        assertEquals(1, context.getChangedRawContactIds().size());
+        assertTrue(context.getChangedRawContactIds().contains(1L));
+
+        context.rawContactInserted(5L, 10L);
+        assertEquals(2, context.getChangedRawContactIds().size());
+        assertTrue(context.getChangedRawContactIds().contains(5L));
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/VCardTest.java b/tests/src/com/android/providers/contacts/VCardTest.java
index 820c263..e2d205e 100644
--- a/tests/src/com/android/providers/contacts/VCardTest.java
+++ b/tests/src/com/android/providers/contacts/VCardTest.java
@@ -19,6 +19,7 @@
 import android.content.ContentResolver;
 import android.test.suitebuilder.annotation.MediumTest;
 
+import com.android.providers.contacts.testutil.RawContactUtil;
 import com.android.vcard.VCardComposer;
 import com.android.vcard.VCardConfig;
 
@@ -37,7 +38,7 @@
      * a vCard string.
      */
     public void testCompose() {
-        createRawContactWithName("John", "Doe");
+        RawContactUtil.createRawContactWithName(mResolver, "John", "Doe");
         final VCardComposer composer = new VCardComposer(
                 getContext(), mResolver, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
         assertTrue(composer.init());
diff --git a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
index 16d06c8..acd830f 100644
--- a/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
+++ b/tests/src/com/android/providers/contacts/aggregation/ContactAggregatorTest.java
@@ -39,6 +39,9 @@
 import com.android.providers.contacts.BaseContactsProvider2Test;
 import com.android.providers.contacts.TestUtils;
 import com.android.providers.contacts.tests.R;
+import com.android.providers.contacts.testutil.DataUtil;
+import com.android.providers.contacts.testutil.RawContactUtil;
+
 import com.google.android.collect.Lists;
 
 /**
@@ -64,8 +67,8 @@
     };
 
     public void testCrudAggregationExceptions() throws Exception {
-        long rawContactId1 = createRawContactWithName("zz", "top");
-        long rawContactId2 = createRawContactWithName("aa", "bottom");
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "zz", "top");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "aa", "bottom");
 
         setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
                 rawContactId1, rawContactId2);
@@ -112,9 +115,9 @@
     }
 
     public void testAggregationCreatesNewAggregate() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
 
-        Uri resultUri = insertStructuredName(rawContactId, "Johna", "Smitha");
+        Uri resultUri = DataUtil.insertStructuredName(mResolver, rawContactId, "Johna", "Smitha");
 
         // Parse the URI and confirm that it contains an ID
         assertTrue(ContentUris.parseId(resultUri) != 0);
@@ -127,11 +130,11 @@
     }
 
     public void testAggregationOfExactFullNameMatch() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Johnb", "Smithb");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnb", "Smithb");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Johnb", "Smithb");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnb", "Smithb");
 
         assertAggregated(rawContactId1, rawContactId2, "Johnb Smithb");
     }
@@ -140,17 +143,17 @@
         Account account = new Account("accountName", "accountType");
         createAutoAddGroup(account);
 
-        long rawContactId1 = createRawContact(account);
-        insertStructuredName(rawContactId1, "Flynn", "Ryder");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
 
         // Hide by removing from all groups
         removeGroupMemberships(rawContactId1);
 
-        long rawContactId2 = createRawContact(account);
-        insertStructuredName(rawContactId2, "Flynn", "Ryder");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
 
-        long rawContactId3 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId3, "Flynn", "Ryder");
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
 
         assertNotAggregated(rawContactId1, rawContactId2);
         assertNotAggregated(rawContactId1, rawContactId3);
@@ -158,261 +161,262 @@
     }
 
     public void testAggregationOfCaseInsensitiveFullNameMatch() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Johnc", "Smithc");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnc", "Smithc");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Johnc", "smithc");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnc", "smithc");
 
         assertAggregated(rawContactId1, rawContactId2, "Johnc Smithc");
     }
 
     public void testAggregationOfLastNameMatch() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, null, "Johnd");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, null, "Johnd");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, null, "johnd");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, null, "johnd");
 
         assertAggregated(rawContactId1, rawContactId2, "Johnd");
     }
 
     public void testNonAggregationOfFirstNameMatch() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Johne", "Smithe");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johne", "Smithe");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Johne", null);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johne", null);
 
         assertNotAggregated(rawContactId1, rawContactId2);
     }
 
     public void testNonAggregationOfLastNameMatch() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Johnf", "Smithf");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnf", "Smithf");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, null, "Smithf");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, null, "Smithf");
 
         assertNotAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationOfConcatenatedFullNameMatch() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Johng", "Smithg");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johng", "Smithg");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "johngsmithg", null);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "johngsmithg", null);
 
         assertAggregated(rawContactId1, rawContactId2, "Johng Smithg");
     }
 
     public void testAggregationOfNormalizedFullNameMatch() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "H\u00e9l\u00e8ne", "Bj\u00f8rn");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "H\u00e9l\u00e8ne", "Bj\u00f8rn");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "helene bjorn", null);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "helene bjorn", null);
 
         assertAggregated(rawContactId1, rawContactId2, "H\u00e9l\u00e8ne Bj\u00f8rn");
     }
 
     public void testAggregationOfNormalizedFullNameMatchWithReadOnlyAccount() {
-        long rawContactId1 = createRawContact(new Account("acct", READ_ONLY_ACCOUNT_TYPE));
-        insertStructuredName(rawContactId1, "H\u00e9l\u00e8ne", "Bj\u00f8rn");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, new Account("acct",
+                READ_ONLY_ACCOUNT_TYPE));
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "H\u00e9l\u00e8ne", "Bj\u00f8rn");
 
-        long rawContactId2 = createRawContact();
-        insertStructuredName(rawContactId2, "helene bjorn", null);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "helene bjorn", null);
 
         assertAggregated(rawContactId1, rawContactId2, "helene bjorn");
     }
 
     public void testAggregationOfNumericNames() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "123", null);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "123", null);
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "1-2-3", null);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "1-2-3", null);
 
         assertAggregated(rawContactId1, rawContactId2, "1-2-3");
     }
 
     public void testAggregationOfInconsistentlyParsedNames() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
 
         ContentValues values = new ContentValues();
         values.put(StructuredName.DISPLAY_NAME, "604 Arizona Ave");
         values.put(StructuredName.GIVEN_NAME, "604");
         values.put(StructuredName.MIDDLE_NAME, "Arizona");
         values.put(StructuredName.FAMILY_NAME, "Ave");
-        insertStructuredName(rawContactId1, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, values);
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         values.clear();
         values.put(StructuredName.DISPLAY_NAME, "604 Arizona Ave");
         values.put(StructuredName.GIVEN_NAME, "604");
         values.put(StructuredName.FAMILY_NAME, "Arizona Ave");
-        insertStructuredName(rawContactId2, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, values);
 
         assertAggregated(rawContactId1, rawContactId2, "604 Arizona Ave");
     }
 
     public void testAggregationBasedOnMiddleName() {
         ContentValues values = new ContentValues();
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
         values.put(StructuredName.GIVEN_NAME, "John");
         values.put(StructuredName.GIVEN_NAME, "Abigale");
         values.put(StructuredName.FAMILY_NAME, "James");
 
-        insertStructuredName(rawContactId1, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, values);
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         values.clear();
         values.put(StructuredName.GIVEN_NAME, "John");
         values.put(StructuredName.GIVEN_NAME, "Marie");
         values.put(StructuredName.FAMILY_NAME, "James");
-        insertStructuredName(rawContactId2, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, values);
 
         assertNotAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationBasedOnPhoneNumberNoNameData() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
         insertPhoneNumber(rawContactId1, "(888)555-1231");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         insertPhoneNumber(rawContactId2, "1(888)555-1231");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationBasedOnPhoneNumberWhenTargetAggregateHasNoName() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
         insertPhoneNumber(rawContactId1, "(888)555-1232");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Johnl", "Smithl");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnl", "Smithl");
         insertPhoneNumber(rawContactId2, "1(888)555-1232");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationBasedOnPhoneNumberWhenNewContactHasNoName() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Johnm", "Smithm");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnm", "Smithm");
         insertPhoneNumber(rawContactId1, "(888)555-1233");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         insertPhoneNumber(rawContactId2, "1(888)555-1233");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationBasedOnPhoneNumberWithDifferentNames() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Baby", "Bear");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Baby", "Bear");
         insertPhoneNumber(rawContactId1, "(888)555-1235");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Blind", "Mouse");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Blind", "Mouse");
         insertPhoneNumber(rawContactId2, "1(888)555-1235");
 
         assertNotAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationBasedOnPhoneNumberWithJustFirstName() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Chick", "Notnull");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Chick", "Notnull");
         insertPhoneNumber(rawContactId1, "(888)555-1236");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Chick", null);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Chick", null);
         insertPhoneNumber(rawContactId2, "1(888)555-1236");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationBasedOnEmailNoNameData() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
         insertEmail(rawContactId1, "lightning@android.com");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         insertEmail(rawContactId2, "lightning@android.com");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationBasedOnEmailWhenTargetAggregateHasNoName() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
         insertEmail(rawContactId1, "mcqueen@android.com");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Lightning", "McQueen");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Lightning", "McQueen");
         insertEmail(rawContactId2, "mcqueen@android.com");
 
         assertAggregated(rawContactId1, rawContactId2, "Lightning McQueen");
     }
 
     public void testAggregationBasedOnEmailWhenNewContactHasNoName() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Doc", "Hudson");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Doc", "Hudson");
         insertEmail(rawContactId1, "doc@android.com");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         insertEmail(rawContactId2, "doc@android.com");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationBasedOnEmailWithDifferentNames() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Chick", "Hicks");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Chick", "Hicks");
         insertEmail(rawContactId1, "hicky@android.com");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Luigi", "Guido");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Luigi", "Guido");
         insertEmail(rawContactId2, "hicky@android.com");
 
         assertNotAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationByCommonNicknameWithLastName() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Bill", "Gore");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Bill", "Gore");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "William", "Gore");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "William", "Gore");
 
         assertAggregated(rawContactId1, rawContactId2, "William Gore");
     }
 
     public void testAggregationByCommonNicknameOnly() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Lawrence", null);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Lawrence", null);
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Larry", null);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Larry", null);
 
         assertAggregated(rawContactId1, rawContactId2, "Lawrence");
     }
 
     public void testAggregationByNicknameNoStructuredName() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
         insertNickname(rawContactId1, "Frozone");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         insertNickname(rawContactId2, "Frozone");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationByNicknameWithDifferentNames() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Helen", "Parr");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Helen", "Parr");
         insertNickname(rawContactId1, "Elastigirl");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Shawn", "Johnson");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Shawn", "Johnson");
         insertNickname(rawContactId2, "Elastigirl");
 
         assertNotAggregated(rawContactId1, rawContactId2);
@@ -421,11 +425,11 @@
     public void testNonAggregationOnOrganization() {
         ContentValues values = new ContentValues();
         values.put(Organization.TITLE, "Monsters, Inc");
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
         insertOrganization(rawContactId1, values);
         insertNickname(rawContactId1, "Boo");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         insertOrganization(rawContactId2, values);
         insertNickname(rawContactId2, "Rendall");   // To force reaggregation
 
@@ -433,21 +437,21 @@
     }
 
     public void testAggregationByIdentity() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
         insertIdentity(rawContactId1, "iden1", "namespace1");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         insertIdentity(rawContactId2, "iden1", "namespace1");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationExceptionKeepIn() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Johnk", "Smithk");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnk", "Smithk");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Johnkx", "Smithkx");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnkx", "Smithkx");
 
         long contactId1 = queryContactId(rawContactId1);
         long contactId2 = queryContactId(rawContactId2);
@@ -471,11 +475,11 @@
     }
 
     public void testAggregationExceptionKeepOut() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Johnh", "Smithh");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johnh", "Smithh");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Johnh", "Smithh");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnh", "Smithh");
 
         setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
                 rawContactId1, rawContactId2);
@@ -484,14 +488,14 @@
     }
 
     public void testAggregationExceptionKeepOutCheckUpdatesDisplayName() {
-        long rawContactId1 = createRawContact(ACCOUNT_1);
-        insertStructuredName(rawContactId1, "Johni", "Smithi");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Johni", "Smithi");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Johnj", "Smithj");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Johnj", "Smithj");
 
-        long rawContactId3 = createRawContact(ACCOUNT_3);
-        insertStructuredName(rawContactId3, "Johnm", "Smithm");
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver, ACCOUNT_3);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Johnm", "Smithm");
 
         setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
                 rawContactId1, rawContactId2);
@@ -533,9 +537,9 @@
     }
 
     public void testAggregationExceptionKeepOutCheckResultDisplayNames() {
-        long rawContactId1 = createRawContactWithName("c", "c", ACCOUNT_1);
-        long rawContactId2 = createRawContactWithName("b", "b", ACCOUNT_2);
-        long rawContactId3 = createRawContactWithName("a", "a", ACCOUNT_3);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "c", "c", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "b", "b", ACCOUNT_2);
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "a", "a", ACCOUNT_3);
 
         // Join all contacts
         setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
@@ -569,12 +573,15 @@
     }
 
     public void testNonAggregationWithMultipleAffinities() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         assertNotAggregated(rawContactId1, rawContactId2);
 
         // There are two aggregates this raw contact could join, so it should join neither
-        long rawContactId3 = createRawContactWithName("John", "Doe", ACCOUNT_2);
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_2);
         assertNotAggregated(rawContactId1, rawContactId3);
         assertNotAggregated(rawContactId2, rawContactId3);
 
@@ -583,13 +590,16 @@
     }
 
     public void testSplitBecauseOfMultipleAffinities() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_2);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_2);
         assertAggregated(rawContactId1, rawContactId2);
 
         // The aggregate this raw contact could join has a raw contact from the same account,
         // let's not aggregate and break up the existing aggregate because of the ambiguity
-        long rawContactId3 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         assertNotAggregated(rawContactId1, rawContactId3);
         assertNotAggregated(rawContactId2, rawContactId3);
         assertNotAggregated(rawContactId1, rawContactId2);
@@ -599,14 +609,14 @@
         Account account = new Account("accountName", "accountType");
         createAutoAddGroup(account);
 
-        long rawContactId1 = createRawContact(account);
-        insertStructuredName(rawContactId1, "Flynn", "Ryder");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Flynn", "Ryder");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
 
-        long rawContactId3 = createRawContact(account);
-        insertStructuredName(rawContactId3, "Flynn", "Ryder");
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
 
         assertNotAggregated(rawContactId1, rawContactId3);
         assertNotAggregated(rawContactId2, rawContactId3);
@@ -624,15 +634,15 @@
         Account account = new Account("accountName", "accountType");
         createAutoAddGroup(account);
 
-        long rawContactId1 = createRawContact(account);
-        insertStructuredName(rawContactId1, "Flynn", "Ryder");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
         insertPhoneNumber(rawContactId1, "1234567890");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         insertPhoneNumber(rawContactId2, "1234567890");
 
-        long rawContactId3 = createRawContact(account);
-        insertStructuredName(rawContactId3, "Flynn", "Ryder");
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
 
         assertNotAggregated(rawContactId1, rawContactId3);
         assertNotAggregated(rawContactId2, rawContactId3);
@@ -650,15 +660,15 @@
         Account account = new Account("accountName", "accountType");
         long groupId = createAutoAddGroup(account);
 
-        long rawContactId1 = createRawContact(account);
-        insertStructuredName(rawContactId1, "Flynn", "Ryder");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, account);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Flynn", "Ryder");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
-        insertStructuredName(rawContactId2, "Flynn", "Ryder");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Flynn", "Ryder");
 
-        long rawContactId3 = createRawContact(account);
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver, account);
         removeGroupMemberships(rawContactId3);
-        insertStructuredName(rawContactId3, "Flynn", "Ryder");
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Flynn", "Ryder");
 
         assertAggregated(rawContactId1, rawContactId2, "Flynn Ryder");
         assertNotAggregated(rawContactId1, rawContactId3);
@@ -672,9 +682,12 @@
     }
 
     public void testNonSplitBecauseOfMultipleAffinitiesWhenOverridden() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_2);
-        long rawContactId3 = createRawContactWithName("John", "Doe", ACCOUNT_3);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_2);
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_3);
         assertAggregated(rawContactId1, rawContactId2);
         assertAggregated(rawContactId1, rawContactId3);
         setAggregationException(
@@ -684,7 +697,8 @@
 
         // The aggregate this raw contact could join has a raw contact from the same account,
         // let's not aggregate and break up the existing aggregate because of the ambiguity
-        long rawContactId4 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId4 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         assertAggregated(rawContactId1, rawContactId2);     // Aggregation exception
         assertNotAggregated(rawContactId1, rawContactId3);
         assertNotAggregated(rawContactId1, rawContactId4);
@@ -692,18 +706,22 @@
     }
 
     public void testNonAggregationFromSameAccount() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         assertNotAggregated(rawContactId1, rawContactId2);
     }
 
     public void testNonAggregationFromSameAccountNoCommonData() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertEmail(rawContactId1, "lightning1@android.com");
         insertPhoneNumber(rawContactId1, "111-222-3333");
         insertIdentity(rawContactId1, "iden1", "namespace");
 
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertEmail(rawContactId2, "lightning2@android.com");
         insertPhoneNumber(rawContactId2, "555-666-7777");
         insertIdentity(rawContactId1, "iden2", "namespace");
@@ -712,20 +730,24 @@
     }
 
     public void testAggregationFromSameAccountEmailSame() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertEmail(rawContactId1, "lightning@android.com");
 
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertEmail(rawContactId2, "lightning@android.com");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testNonAggregationFromSameAccountEmailDifferent() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertEmail(rawContactId1, "lightning1@android.com");
 
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertEmail(rawContactId2, "lightning2@android.com");
         insertEmail(rawContactId2, "lightning3@android.com");
 
@@ -733,21 +755,25 @@
     }
 
     public void testAggregationFromSameAccountIdentitySame() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertIdentity(rawContactId1, "iden", "namespace");
 
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertIdentity(rawContactId2, "iden", "namespace");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testNonAggregationFromSameAccountIdentityDifferent() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertIdentity(rawContactId1, "iden1", "namespace1");
         insertIdentity(rawContactId1, "iden2", "namespace2");
 
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertIdentity(rawContactId2, "iden2", "namespace1");
         insertIdentity(rawContactId2, "iden1", "namespace2");
 
@@ -755,52 +781,58 @@
     }
 
     public void testAggregationFromSameAccountPhoneNumberSame() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertPhoneNumber(rawContactId1, "111-222-3333");
 
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertPhoneNumber(rawContactId2, "111-222-3333");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationFromSameAccountPhoneNumberNormalizedSame() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertPhoneNumber(rawContactId1, "111-222-3333");
 
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertPhoneNumber(rawContactId2, "+1-111-222-3333");
 
         assertAggregated(rawContactId1, rawContactId2);
     }
 
     public void testNonAggregationFromSameAccountPhoneNumberDifferent() {
-        long rawContactId1 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertPhoneNumber(rawContactId1, "111-222-3333");
 
-        long rawContactId2 = createRawContactWithName("John", "Doe", ACCOUNT_1);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "John", "Doe",
+                ACCOUNT_1);
         insertPhoneNumber(rawContactId2, "111-222-3334");
 
         assertNotAggregated(rawContactId1, rawContactId2);
     }
 
     public void testAggregationSuggestionsBasedOnName() {
-        long rawContactId1 = createRawContact();
-        insertStructuredName(rawContactId1, "Duane", null);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Duane", null);
 
         // Exact name match
-        long rawContactId2 = createRawContact();
-        insertStructuredName(rawContactId2, "Duane", null);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Duane", null);
         setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
                 rawContactId1, rawContactId2);
 
         // Edit distance == 0.84
-        long rawContactId3 = createRawContact();
-        insertStructuredName(rawContactId3, "Dwayne", null);
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId3, "Dwayne", null);
 
         // Edit distance == 0.6
-        long rawContactId4 = createRawContact();
-        insertStructuredName(rawContactId4, "Donny", null);
+        long rawContactId4 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId4, "Donny", null);
 
         long contactId1 = queryContactId(rawContactId1);
         long contactId2 = queryContactId(rawContactId2);
@@ -812,12 +844,12 @@
     public void testAggregationSuggestionsBasedOnPhoneNumber() {
 
         // Create two contacts that would not be aggregated because of name mismatch
-        long rawContactId1 = createRawContact();
-        insertStructuredName(rawContactId1, "Lord", "Farquaad");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Lord", "Farquaad");
         insertPhoneNumber(rawContactId1, "(888)555-1236");
 
-        long rawContactId2 = createRawContact();
-        insertStructuredName(rawContactId2, "Talking", "Donkey");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Talking", "Donkey");
         insertPhoneNumber(rawContactId2, "1(888)555-1236");
 
         long contactId1 = queryContactId(rawContactId1);
@@ -830,12 +862,12 @@
     public void testAggregationSuggestionsBasedOnEmailAddress() {
 
         // Create two contacts that would not be aggregated because of name mismatch
-        long rawContactId1 = createRawContact();
-        insertStructuredName(rawContactId1, "Carl", "Fredricksen");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Carl", "Fredricksen");
         insertEmail(rawContactId1, "up@android.com");
 
-        long rawContactId2 = createRawContact();
-        insertStructuredName(rawContactId2, "Charles", "Muntz");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Charles", "Muntz");
         insertEmail(rawContactId2, "up@android.com");
 
         long contactId1 = queryContactId(rawContactId1);
@@ -848,12 +880,12 @@
     public void testAggregationSuggestionsBasedOnEmailAddressApproximateMatch() {
 
         // Create two contacts that would not be aggregated because of name mismatch
-        long rawContactId1 = createRawContact();
-        insertStructuredName(rawContactId1, "Bob", null);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Bob", null);
         insertEmail(rawContactId1, "incredible@android.com");
 
-        long rawContactId2 = createRawContact();
-        insertStructuredName(rawContactId2, "Lucius", "Best");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Lucius", "Best");
         insertEmail(rawContactId2, "incrediball@android.com");
 
         long contactId1 = queryContactId(rawContactId1);
@@ -864,12 +896,12 @@
     }
 
     public void testAggregationSuggestionsBasedOnNickname() {
-        long rawContactId1 = createRawContact();
-        insertStructuredName(rawContactId1, "Peter", "Parker");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Peter", "Parker");
         insertNickname(rawContactId1, "Spider-Man");
 
-        long rawContactId2 = createRawContact();
-        insertStructuredName(rawContactId2, "Manny", "Spider");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Manny", "Spider");
 
         long contactId1 = queryContactId(rawContactId1);
         setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
@@ -880,12 +912,12 @@
     }
 
     public void testAggregationSuggestionsBasedOnNicknameMatchingName() {
-        long rawContactId1 = createRawContact();
-        insertStructuredName(rawContactId1, "Clark", "Kent");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Clark", "Kent");
         insertNickname(rawContactId1, "Superman");
 
-        long rawContactId2 = createRawContact();
-        insertStructuredName(rawContactId2, "Roy", "Williams");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Roy", "Williams");
         insertNickname(rawContactId2, "superman");
 
         long contactId1 = queryContactId(rawContactId1);
@@ -897,11 +929,11 @@
     }
 
     public void testAggregationSuggestionsBasedOnCommonNickname() {
-        long rawContactId1 = createRawContact();
-        insertStructuredName(rawContactId1, "Dick", "Cherry");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Dick", "Cherry");
 
-        long rawContactId2 = createRawContact();
-        insertStructuredName(rawContactId2, "Richard", "Cherry");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Richard", "Cherry");
 
         setAggregationException(AggregationExceptions.TYPE_KEEP_SEPARATE,
                 rawContactId1, rawContactId2);
@@ -914,12 +946,12 @@
     public void testAggregationSuggestionsBasedOnPhoneNumberWithFilter() {
 
         // Create two contacts that would not be aggregated because of name mismatch
-        long rawContactId1 = createRawContact();
-        insertStructuredName(rawContactId1, "Lord", "Farquaad");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId1, "Lord", "Farquaad");
         insertPhoneNumber(rawContactId1, "(888)555-1236");
 
-        long rawContactId2 = createRawContact();
-        insertStructuredName(rawContactId2, "Talking", "Donkey");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "Talking", "Donkey");
         insertPhoneNumber(rawContactId2, "1(888)555-1236");
 
         long contactId1 = queryContactId(rawContactId1);
@@ -933,13 +965,15 @@
     }
 
     public void testAggregationSuggestionsDontSuggestInvisible() {
-        long rawContactId1 = createRawContactWithName("first", "last", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "first", "last",
+                ACCOUNT_1);
         insertPhoneNumber(rawContactId1, "111-222-3333");
         insertNickname(rawContactId1, "Superman");
         insertEmail(rawContactId1, "incredible@android.com");
 
         // Create another with the exact same name, phone number, nickname and email.
-        long rawContactId2 = createRawContactWithName("first", "last", ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "first", "last",
+                ACCOUNT_2);
         insertPhoneNumber(rawContactId2, "111-222-3333");
         insertNickname(rawContactId2, "Superman");
         insertEmail(rawContactId2, "incredible@android.com");
@@ -965,15 +999,15 @@
     }
 
     public void testChoosePhotoSetBeforeAggregation() {
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId1, "donut", "donut_act");
         insertPhoto(rawContactId1);
 
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId2, "cupcake", "cupcake_act");
         long cupcakeId = ContentUris.parseId(insertPhoto(rawContactId2));
 
-        long rawContactId3 = createRawContact();
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId3, "froyo", "froyo_act");
         insertPhoto(rawContactId3);
 
@@ -985,17 +1019,17 @@
     }
 
     public void testChoosePhotoSetAfterAggregation() {
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId1, "donut", "donut_act");
         insertPhoto(rawContactId1);
 
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
                 rawContactId1, rawContactId2);
         setContactAccount(rawContactId2, "cupcake", "cupcake_act");
         long cupcakeId = ContentUris.parseId(insertPhoto(rawContactId2));
 
-        long rawContactId3 = createRawContact();
+        long rawContactId3 = RawContactUtil.createRawContact(mResolver);
         setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER,
                 rawContactId1, rawContactId3);
         setContactAccount(rawContactId3, "froyo", "froyo_act");
@@ -1015,7 +1049,7 @@
 
     public void testChooseLargerPhotoByDimensions() {
         // Donut photo is 256x256.
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId1, "donut", "donut_act");
         long normalEarthDataId = ContentUris.parseId(
                 insertPhoto(rawContactId1, R.drawable.earth_normal));
@@ -1024,7 +1058,7 @@
                 Photo.PHOTO_FILE_ID);
 
         // Cupcake would normally have priority, but its photo is 200x200.
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId2, "cupcake", "cupcake_act");
         insertPhoto(rawContactId2, R.drawable.earth_200);
 
@@ -1037,7 +1071,7 @@
 
     public void testChooseLargerPhotoByFileSize() {
         // Donut photo is a 256x256 photo of a nebula.
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId1, "donut", "donut_act");
         long nebulaDataId = ContentUris.parseId(
                 insertPhoto(rawContactId1, R.drawable.nebula));
@@ -1047,7 +1081,7 @@
 
         // Cupcake would normally have priority, but its photo (of a galaxy) has the same dimensions
         // as Donut's, but a smaller filesize.
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId2, "cupcake", "cupcake_act");
         insertPhoto(rawContactId2, R.drawable.galaxy);
 
@@ -1060,7 +1094,7 @@
 
     public void testChooseFilePhotoOverThumbnail() {
         // Donut photo is a 256x256 photo of Earth.
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId1, "donut", "donut_act");
         long normalEarthDataId = ContentUris.parseId(
                 insertPhoto(rawContactId1, R.drawable.earth_normal));
@@ -1069,7 +1103,7 @@
                 Photo.PHOTO_FILE_ID);
 
         // Cupcake would normally have priority, but its photo of Earth is thumbnail-sized.
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId2, "cupcake", "cupcake_act");
         insertPhoto(rawContactId2, R.drawable.earth_small);
 
@@ -1082,12 +1116,12 @@
 
     public void testFallbackToAccountPriorityForSamePhoto() {
         // Donut photo is a 256x256 photo of Earth.
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId1, "donut", "donut_act");
         insertPhoto(rawContactId1, R.drawable.earth_normal);
 
         // Cupcake has the same 256x256 photo of Earth.
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId2, "cupcake", "cupcake_act");
         long cupcakeEarthDataId = ContentUris.parseId(
                 insertPhoto(rawContactId2, R.drawable.earth_normal));
@@ -1104,13 +1138,13 @@
 
     public void testFallbackToAccountPriorityForDifferingThumbnails() {
         // Donut photo is a 96x96 thumbnail of Earth.
-        long rawContactId1 = createRawContact();
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId1, "donut", "donut_act");
         insertPhoto(rawContactId1, R.drawable.earth_small);
 
         // Cupcake photo is the 96x96 "no contact" placeholder (smaller filesize than the Earth
         // picture, but thumbnail filesizes are ignored in the aggregator).
-        long rawContactId2 = createRawContact();
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
         setContactAccount(rawContactId2, "cupcake", "cupcake_act");
         long cupcakeDataId = ContentUris.parseId(
                 insertPhoto(rawContactId2, R.drawable.ic_contact_picture));
@@ -1123,7 +1157,7 @@
     }
 
     public void testDisplayNameSources() {
-        long rawContactId = createRawContact();
+        long rawContactId = RawContactUtil.createRawContact(mResolver);
         long contactId = queryContactId(rawContactId);
 
         assertNull(queryDisplayName(contactId));
@@ -1145,15 +1179,18 @@
         values.clear();
         values.put(StructuredName.GIVEN_NAME, "Eclair");
         values.put(StructuredName.FAMILY_NAME, "Android");
-        insertStructuredName(rawContactId, values);
+        DataUtil.insertStructuredName(mResolver, rawContactId, values);
         assertEquals("Eclair Android", queryDisplayName(contactId));
     }
 
     public void testVerifiedName() {
-        long rawContactId1 = createRawContactWithName("test1", "TEST1", ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "test1", "TEST1",
+                ACCOUNT_1);
         storeValue(RawContacts.CONTENT_URI, rawContactId1, RawContacts.NAME_VERIFIED, "1");
-        long rawContactId2 = createRawContactWithName("test2", "TEST2", ACCOUNT_2);
-        long rawContactId3 = createRawContactWithName("test3", "TEST3 LONG", ACCOUNT_3);
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "test2", "TEST2",
+                ACCOUNT_2);
+        long rawContactId3 = RawContactUtil.createRawContactWithName(mResolver, "test3",
+                "TEST3 LONG", ACCOUNT_3);
 
         setAggregationException(AggregationExceptions.TYPE_KEEP_TOGETHER, rawContactId1,
                 rawContactId2);
@@ -1179,15 +1216,15 @@
     public void testAggregationModeSuspendedSeparateTransactions() {
 
         // Setting aggregation mode to SUSPENDED should prevent aggregation from happening
-        long rawContactId1 = createRawContact(ACCOUNT_1);
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver, ACCOUNT_1);
         storeValue(RawContacts.CONTENT_URI, rawContactId1,
                 RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
-        Uri name1 = insertStructuredName(rawContactId1, "THE", "SAME");
+        Uri name1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "THE", "SAME");
 
-        long rawContactId2 = createRawContact(ACCOUNT_2);
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver, ACCOUNT_2);
         storeValue(RawContacts.CONTENT_URI, rawContactId2,
                 RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
-        insertStructuredName(rawContactId2, "THE", "SAME");
+        DataUtil.insertStructuredName(mResolver, rawContactId2, "THE", "SAME");
 
         assertNotAggregated(rawContactId1, rawContactId2);
 
@@ -1365,12 +1402,12 @@
     }
 
     public void testAggregatedStatusUpdate() {
-        long rawContactId1 = createRawContact();
-        Uri dataUri1 = insertStructuredName(rawContactId1, "john", "doe");
+        long rawContactId1 = RawContactUtil.createRawContact(mResolver);
+        Uri dataUri1 = DataUtil.insertStructuredName(mResolver, rawContactId1, "john", "doe");
         insertStatusUpdate(ContentUris.parseId(dataUri1), StatusUpdates.AWAY, "Green", 100,
                 StatusUpdates.CAPABILITY_HAS_CAMERA);
-        long rawContactId2 = createRawContact();
-        Uri dataUri2 = insertStructuredName(rawContactId2, "john", "doe");
+        long rawContactId2 = RawContactUtil.createRawContact(mResolver);
+        Uri dataUri2 = DataUtil.insertStructuredName(mResolver, rawContactId2, "john", "doe");
         insertStatusUpdate(ContentUris.parseId(dataUri2), StatusUpdates.AVAILABLE, "Red", 50,
                 StatusUpdates.CAPABILITY_HAS_CAMERA);
         setAggregationException(
@@ -1391,8 +1428,8 @@
     }
 
     public void testAggregationSuggestionsByName() throws Exception {
-        long rawContactId1 = createRawContactWithName("first1", "last1");
-        long rawContactId2 = createRawContactWithName("first2", "last2");
+        long rawContactId1 = RawContactUtil.createRawContactWithName(mResolver, "first1", "last1");
+        long rawContactId2 = RawContactUtil.createRawContactWithName(mResolver, "first2", "last2");
 
         Uri uri = AggregationSuggestions.builder()
                 .addParameter(AggregationSuggestions.PARAMETER_MATCH_NAME, "last1 first1")
diff --git a/tests/src/com/android/providers/contacts/database/MoreDatabaseUtilTest.java b/tests/src/com/android/providers/contacts/database/MoreDatabaseUtilTest.java
new file mode 100644
index 0000000..6eb8b55
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/database/MoreDatabaseUtilTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.database;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for MoreDatabaseutil.
+ */
+@SmallTest
+public class MoreDatabaseUtilTest extends TestCase {
+
+    public void testBuildBindArgString() {
+        assertEquals("?", MoreDatabaseUtils.buildBindArgString(1));
+        assertEquals("?,?", MoreDatabaseUtils.buildBindArgString(2));
+        assertEquals("?,?,?", MoreDatabaseUtils.buildBindArgString(3));
+        assertEquals("?,?,?,?", MoreDatabaseUtils.buildBindArgString(4));
+    }
+
+    public void testBuildIndex() {
+        String expected = "create index testtable_testfield_index on testtable(testfield)";
+        String actual = MoreDatabaseUtils.buildCreateIndexSql("testtable", "testfield")
+                .toLowerCase();
+        assertEquals(expected, actual);
+
+        expected = "create index test_table_test_field_index on test_table(test_field)";
+        actual = MoreDatabaseUtils.buildCreateIndexSql("test_table", "test_field").toLowerCase();
+        assertEquals(expected, actual);
+    }
+
+    public void testDropIndex() {
+        String expected = "drop index if exists testtable_testfield_index";
+        String actual = MoreDatabaseUtils.buildDropIndexSql("testtable", "testfield").toLowerCase();
+        assertEquals(expected, actual);
+    }
+
+    public void testBuildIndexName() {
+        assertEquals("testtable_testfield_index",
+                MoreDatabaseUtils.buildIndexName("testtable", "testfield"));
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/testutil/CommonDatabaseUtils.java b/tests/src/com/android/providers/contacts/testutil/CommonDatabaseUtils.java
new file mode 100644
index 0000000..bba5978
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/testutil/CommonDatabaseUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.testutil;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Common database methods.
+ */
+public class CommonDatabaseUtils {
+
+    // primitive value used when record is not found.
+    public static final long NOT_FOUND = -1;
+
+    public static String[] singleRecordToArray(Cursor cursor) {
+        String[] result = null;
+        try {
+            if (cursor.moveToNext()) {
+                result = new String[cursor.getColumnCount()];
+                fillArray(cursor, result);
+            }
+        } finally {
+            closeQuietly(cursor);
+        }
+        return result;
+    }
+
+    public static List<String[]> multiRecordToArray(Cursor cursor) {
+        ArrayList<String[]> result = new ArrayList<String[]>();
+        try {
+            while (cursor.moveToNext()) {
+                String[] record = new String[cursor.getColumnCount()];
+                fillArray(cursor, record);
+                result.add(record);
+            }
+        } finally {
+            closeQuietly(cursor);
+        }
+        return result;
+    }
+
+    private static void fillArray(Cursor cursor, String[] array) {
+        for (int i = 0; i < array.length; i++) {
+            array[i] = cursor.getString(i);
+        }
+    }
+
+    public static void closeQuietly(Cursor cursor) {
+        if (cursor != null) {
+            cursor.close();
+        }
+    }
+
+    public static void extrasVarArgsToValues(ContentValues values, String... extras) {
+        for (int i = 0; i < extras.length; ) {
+            values.put(extras[i], extras[i + 1]);
+            i += 2;
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/testutil/ContactUtil.java b/tests/src/com/android/providers/contacts/testutil/ContactUtil.java
new file mode 100644
index 0000000..442c5e7
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/testutil/ContactUtil.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.testutil;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+/**
+ * Convenience methods for operating on the Contacts table.
+ */
+public class ContactUtil {
+
+    private static final Uri URI = ContactsContract.Contacts.CONTENT_URI;
+
+    public static void update(ContentResolver resolver, long contactId,
+            ContentValues values) {
+        Uri uri = ContentUris.withAppendedId(URI, contactId);
+        resolver.update(uri, values, null, null);
+    }
+
+    public static void delete(ContentResolver resolver, long contactId) {
+        Uri uri = ContentUris.withAppendedId(URI, contactId);
+        resolver.delete(uri, null, null);
+    }
+
+    public static boolean recordExistsForContactId(ContentResolver resolver, long contactId) {
+        String[] projection = new String[]{
+                ContactsContract.Contacts._ID
+        };
+        Uri uri = ContentUris.withAppendedId(URI, contactId);
+        Cursor cursor = resolver.query(uri, projection, null, null, null);
+        if (cursor.moveToNext()) {
+            return true;
+        }
+        return false;
+    }
+
+    public static long queryContactLastUpdatedTimestamp(ContentResolver resolver, long contactId) {
+        String[] projection = new String[]{
+                ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP
+        };
+
+        Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
+        Cursor cursor = resolver.query(uri, projection, null, null, null);
+        try {
+            if (cursor.moveToNext()) {
+                return cursor.getLong(0);
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return CommonDatabaseUtils.NOT_FOUND;
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/testutil/DataUtil.java b/tests/src/com/android/providers/contacts/testutil/DataUtil.java
new file mode 100644
index 0000000..874aacf
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/testutil/DataUtil.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.testutil;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.test.mock.MockContentResolver;
+
+/**
+ * Convenience methods for operating on the Data table.
+ */
+public class DataUtil {
+
+    private static final Uri URI = ContactsContract.Data.CONTENT_URI;
+
+    public static void delete(ContentResolver resolver, long dataId) {
+        Uri uri = ContentUris.withAppendedId(URI, dataId);
+        resolver.delete(uri, null, null);
+    }
+
+    public static void update(ContentResolver resolver, long dataId, ContentValues values) {
+        Uri uri = ContentUris.withAppendedId(URI, dataId);
+        resolver.update(uri, values, null, null);
+    }
+
+    public static Uri insertStructuredName(ContentResolver resolver, long rawContactId,
+            ContentValues values) {
+        values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
+        values.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
+        Uri resultUri = resolver.insert(ContactsContract.Data.CONTENT_URI, values);
+        return resultUri;
+    }
+
+    public static Uri insertStructuredName(ContentResolver resolver, long rawContactId,
+            String givenName, String familyName) {
+        ContentValues values = new ContentValues();
+        StringBuilder sb = new StringBuilder();
+        if (givenName != null) {
+            sb.append(givenName);
+        }
+        if (givenName != null && familyName != null) {
+            sb.append(" ");
+        }
+        if (familyName != null) {
+            sb.append(familyName);
+        }
+        values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, sb.toString());
+        values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, givenName);
+        values.put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, familyName);
+
+        return insertStructuredName(resolver, rawContactId, values);
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/testutil/DatabaseAsserts.java b/tests/src/com/android/providers/contacts/testutil/DatabaseAsserts.java
new file mode 100644
index 0000000..ac4df17
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/testutil/DatabaseAsserts.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.testutil;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.net.Uri;
+import android.test.MoreAsserts;
+
+import junit.framework.Assert;
+
+/**
+ * Common methods for asserting database related operations.
+ */
+public class DatabaseAsserts {
+
+    public static void assertDeleteIsUnsupported(ContentResolver resolver, Uri uri) {
+        try {
+            resolver.delete(uri, null, null);
+            Assert.fail("delete operation should have failed with UnsupportedOperationException on"
+                    + uri);
+        } catch (UnsupportedOperationException e) {
+            // pass
+        }
+    }
+
+    public static void assertInsertIsUnsupported(ContentResolver resolver, Uri  uri) {
+        try {
+            ContentValues values = new ContentValues();
+            resolver.insert(uri, values);
+            Assert.fail("insert operation should have failed with UnsupportedOperationException on"
+                    + uri);
+        } catch (UnsupportedOperationException e) {
+            // pass
+        }
+    }
+
+    /**
+     * Create a contact and assert that the record exists.
+     *
+     * @return The created contact id pair.
+     */
+    public static ContactIdPair assertAndCreateContact(ContentResolver resolver) {
+        long rawContactId = RawContactUtil.createRawContactWithName(resolver);
+
+        long contactId = RawContactUtil.queryContactIdByRawContactId(resolver, rawContactId);
+        MoreAsserts.assertNotEqual(CommonDatabaseUtils.NOT_FOUND, contactId);
+
+        return new ContactIdPair(contactId, rawContactId);
+    }
+
+    /**
+     * Asserts that a contact id was deleted, has a delete log, and that log has a timestamp greater
+     * than the given timestamp.
+     *
+     * @param contactId The contact id to check.
+     * @param start The timestamp that the delete log should be greater than.
+     */
+    public static void assertHasDeleteLogGreaterThan(ContentResolver resolver, long contactId,
+            long start) {
+        Assert.assertFalse(ContactUtil.recordExistsForContactId(resolver, contactId));
+
+        long deletedTimestamp = DeletedContactUtil.queryDeletedTimestampForContactId(resolver,
+                contactId);
+        MoreAsserts.assertNotEqual(CommonDatabaseUtils.NOT_FOUND, deletedTimestamp);
+        Assert.assertTrue(deletedTimestamp > start);
+    }
+
+    /**
+     * Holds a single contact id and raw contact id relationship.
+     */
+    public static class ContactIdPair {
+        public long mContactId;
+        public long mRawContactId;
+
+        public ContactIdPair(long contactId, long rawContactId) {
+            this.mContactId = contactId;
+            this.mRawContactId = rawContactId;
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/testutil/DeletedContactUtil.java b/tests/src/com/android/providers/contacts/testutil/DeletedContactUtil.java
new file mode 100644
index 0000000..2dab7f9
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/testutil/DeletedContactUtil.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.testutil;
+
+import static android.provider.ContactsContract.DeletedContacts;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.database.Cursor;
+import android.net.Uri;
+
+import java.util.List;
+
+/**
+ * Convenience methods for operating on the DeletedContacts table.
+ */
+public class DeletedContactUtil {
+
+    private static final Uri URI = DeletedContacts.CONTENT_URI;
+
+    public static long queryDeletedTimestampForContactId(ContentResolver resolver, long contactId) {
+        String[] projection = new String[]{
+                DeletedContacts.CONTACT_DELETED_TIMESTAMP
+        };
+        Uri uri = ContentUris.withAppendedId(URI, contactId);
+        Cursor cursor = resolver.query(uri, projection, null, null, null);
+        if (cursor.moveToNext()) {
+            return cursor.getLong(0);
+        }
+        return CommonDatabaseUtils.NOT_FOUND;
+    }
+
+    public static long getCount(ContentResolver resolver) {
+        String[] projection = new String[] {
+                DeletedContacts.CONTACT_ID
+        };
+        Cursor cursor = resolver.query(URI, projection, null, null, null);
+        try {
+            return cursor.getCount();
+        } finally {
+            CommonDatabaseUtils.closeQuietly(cursor);
+        }
+    }
+
+    /**
+     * Queries all records.
+     *
+     * @return A list of records.  Where each record is represented as an array of strings.
+     */
+    public static List<String[]> query(ContentResolver resolver, String[] projection) {
+        Cursor cursor = resolver.query(URI, projection, null, null, null);
+        return CommonDatabaseUtils.multiRecordToArray(cursor);
+    }
+
+    /**
+     * Queries all records after a given timestamp.
+     *
+     * @return A list of records.  Where each record is represented as an array of strings.
+     */
+    public static List<String[]> querySinceTimestamp(ContentResolver resolver, String[] projection,
+            long timestamp) {
+        String selection = DeletedContacts.CONTACT_DELETED_TIMESTAMP + ">?";
+        String[] args = new String[] {timestamp + ""};
+        Cursor cursor = resolver.query(URI, projection, selection, args, null);
+        return CommonDatabaseUtils.multiRecordToArray(cursor);
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java b/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java
new file mode 100644
index 0000000..e9cd3b5
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/testutil/RawContactUtil.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.testutil;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.test.mock.MockContentResolver;
+
+import java.util.List;
+
+/**
+ * Convenience methods for operating on the RawContacts table.
+ */
+public class RawContactUtil {
+
+    private static final Uri URI = ContactsContract.RawContacts.CONTENT_URI;
+
+    public static void update(ContentResolver resolver, long rawContactId,
+            ContentValues values) {
+        Uri uri = ContentUris.withAppendedId(URI, rawContactId);
+        resolver.update(uri, values, null, null);
+    }
+
+    public static String[] queryByRawContactId(ContentResolver resolver,
+            long rawContactId, String[] projection) {
+        Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI,
+                rawContactId);
+        Cursor cursor = resolver.query(uri, projection, null, null, null);
+        return CommonDatabaseUtils.singleRecordToArray(cursor);
+    }
+
+    /**
+     * Returns a list of raw contact records.
+     *
+     * @return A list of records.  Where each record is represented as an array of strings.
+     */
+    public static List<String[]> queryByContactId(ContentResolver resolver, long contactId,
+            String[] projection) {
+        Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, contactId);
+        Cursor cursor = resolver.query(uri, projection, null, null, null);
+        return CommonDatabaseUtils.multiRecordToArray(cursor);
+    }
+
+    public static void delete(ContentResolver resolver, long rawContactId,
+            boolean isSyncAdapter) {
+        Uri uri = ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, rawContactId)
+                .buildUpon()
+                .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, isSyncAdapter + "")
+                .build();
+        resolver.delete(uri, null, null);
+    }
+
+    public static long queryContactIdByRawContactId(ContentResolver resolver, long rawContactid) {
+        String[] projection = new String[]{
+                ContactsContract.RawContacts.CONTACT_ID
+        };
+        String[] result = RawContactUtil.queryByRawContactId(resolver, rawContactid,
+                projection);
+        if (result == null) {
+            return CommonDatabaseUtils.NOT_FOUND;
+        }
+        return Long.parseLong(result[0]);
+    }
+
+    public static boolean rawContactExistsById(ContentResolver resolver, long rawContactid) {
+        long contactId = queryContactIdByRawContactId(resolver, rawContactid);
+        return contactId != CommonDatabaseUtils.NOT_FOUND;
+    }
+
+    public static long createRawContact(ContentResolver resolver, Account account,
+            String... extras) {
+        ContentValues values = new ContentValues();
+        CommonDatabaseUtils.extrasVarArgsToValues(values, extras);
+        final Uri uri = TestUtil.maybeAddAccountQueryParameters(ContactsContract.RawContacts.CONTENT_URI, account);
+        Uri contactUri = resolver.insert(uri, values);
+        return ContentUris.parseId(contactUri);
+    }
+
+    public static long createRawContactWithName(ContentResolver resolver) {
+        return createRawContactWithName(resolver, null);
+    }
+
+    public static long createRawContactWithName(ContentResolver resolver, Account account) {
+        return createRawContactWithName(resolver, "John", "Doe", account);
+    }
+
+    public static long createRawContactWithName(ContentResolver resolver, String firstName,
+            String lastName) {
+        return createRawContactWithName(resolver, firstName, lastName, null);
+    }
+
+    public static long createRawContactWithName(ContentResolver resolver, String firstName,
+            String lastName, Account account) {
+        long rawContactId = createRawContact(resolver, account);
+        DataUtil.insertStructuredName(resolver, rawContactId, firstName, lastName);
+        return rawContactId;
+    }
+
+    public static long createRawContact(ContentResolver resolver) {
+        return createRawContact(resolver, null);
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/testutil/TestUtil.java b/tests/src/com/android/providers/contacts/testutil/TestUtil.java
new file mode 100644
index 0000000..2020f6d
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/testutil/TestUtil.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.providers.contacts.testutil;
+
+import android.accounts.Account;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+/**
+ * Common methods used for testing.
+ */
+public class TestUtil {
+    private static String TAG = TestUtil.class.getSimpleName();
+
+    public static final Account ACCOUNT_1 = new Account("account_name_1", "account_type_1");
+    public static final Account ACCOUNT_2 = new Account("account_name_2", "account_type_2");
+
+    /**
+     * Sleep for 1ms.
+     */
+    public static void sleep() {
+        try {
+            Thread.sleep(1);
+        } catch (InterruptedException e) {
+            Log.w(TAG, "Sleep interrupted.");
+        }
+    }
+
+    public static Uri maybeAddAccountQueryParameters(Uri uri, Account account) {
+        if (account == null) {
+            return uri;
+        }
+        return uri.buildUpon()
+                .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
+                .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
+                .build();
+    }
+}