Support secondary locales

Add support for tracking a secondary locale in addition to the current
primary locale for CP2. Switch to using parseable ICU language tag
(eg, "en-US") for locale tag written to DB. Secondary locale is set to
previous locale on locale change and persisted in CP2 DB and prefs.

Bug:8715226
Change-Id: Ia68397fd9118d89f3a45ac54f991f86bad42870e
diff --git a/src/com/android/providers/contacts/ContactLocaleUtils.java b/src/com/android/providers/contacts/ContactLocaleUtils.java
index 3d3ae82..340b6a5 100644
--- a/src/com/android/providers/contacts/ContactLocaleUtils.java
+++ b/src/com/android/providers/contacts/ContactLocaleUtils.java
@@ -22,6 +22,7 @@
 import android.util.Log;
 
 import com.android.providers.contacts.HanziToPinyin.Token;
+import com.google.common.annotations.VisibleForTesting;
 
 import java.lang.Character.UnicodeBlock;
 import java.util.Arrays;
@@ -70,8 +71,9 @@
         protected final ImmutableIndex mAlphabeticIndex;
         private final int mAlphabeticIndexBucketCount;
         private final int mNumberBucketIndex;
+        private final boolean mEnableSecondaryLocalePinyin;
 
-        public ContactLocaleUtilsBase(Locale locale) {
+        public ContactLocaleUtilsBase(LocaleSet locales) {
             // 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
@@ -84,9 +86,14 @@
             // Latin based alphabets. Ukrainian and Serbian are chosen for
             // Cyrillic because their alphabets are complementary supersets
             // of Russian.
-            mAlphabeticIndex = new AlphabeticIndex(locale)
-                .setMaxLabelCount(300)
-                .addLabels(Locale.ENGLISH)
+            final Locale secondaryLocale = locales.getSecondaryLocale();
+            mEnableSecondaryLocalePinyin = locales.isSecondaryLocaleSimplifiedChinese();
+            AlphabeticIndex ai = new AlphabeticIndex(locales.getPrimaryLocale())
+                .setMaxLabelCount(300);
+            if (secondaryLocale != null) {
+                ai.addLabels(secondaryLocale);
+            }
+            mAlphabeticIndex = ai.addLabels(Locale.ENGLISH)
                 .addLabels(Locale.JAPANESE)
                 .addLabels(Locale.KOREAN)
                 .addLabels(LOCALE_THAI)
@@ -136,6 +143,13 @@
                 return mNumberBucketIndex;
             }
 
+            /**
+             * TODO: ICU 52 AlphabeticIndex doesn't support Simplified Chinese
+             * as a secondary locale. Remove the following if that is added.
+             */
+            if (mEnableSecondaryLocalePinyin) {
+                name = HanziToPinyin.getInstance().transliterate(name);
+            }
             final int bucket = mAlphabeticIndex.getBucketIndex(name);
             if (bucket < 0) {
                 return -1;
@@ -199,8 +213,8 @@
         private static final String JAPANESE_MISC_LABEL = "\u4ed6";
         private final int mMiscBucketIndex;
 
-        public JapaneseContactUtils(Locale locale) {
-            super(locale);
+        public JapaneseContactUtils(LocaleSet locales) {
+            super(locales);
             // 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
@@ -341,8 +355,8 @@
      */
     private static class SimplifiedChineseContactUtils
         extends ContactLocaleUtilsBase {
-        public SimplifiedChineseContactUtils(Locale locale) {
-            super(locale);
+        public SimplifiedChineseContactUtils(LocaleSet locales) {
+            super(locales);
         }
 
         @Override
@@ -357,7 +371,7 @@
         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);
+            ArrayList<Token> tokens = HanziToPinyin.getInstance().getTokens(name);
             final int tokenCount = tokens.size();
             final StringBuilder keyPinyin = new StringBuilder();
             final StringBuilder keyInitial = new StringBuilder();
@@ -393,48 +407,49 @@
         }
     }
 
-    private static final String CHINESE_LANGUAGE = Locale.CHINESE.getLanguage().toLowerCase();
     private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase();
-    private static final String KOREAN_LANGUAGE = Locale.KOREAN.getLanguage().toLowerCase();
 
     private static ContactLocaleUtils sSingleton;
 
-    private final Locale mLocale;
-    private final String mLanguage;
+    private final LocaleSet mLocales;
     private final ContactLocaleUtilsBase mUtils;
 
-    private ContactLocaleUtils(Locale locale) {
-        if (locale == null) {
-            mLocale = Locale.getDefault();
+    private ContactLocaleUtils(LocaleSet locales) {
+        if (locales == null) {
+            mLocales = LocaleSet.getDefault();
         } else {
-            mLocale = locale;
+            mLocales = locales;
         }
-        mLanguage = mLocale.getLanguage().toLowerCase();
-        if (mLanguage.equals(JAPANESE_LANGUAGE)) {
-            mUtils = new JapaneseContactUtils(mLocale);
-        } else if (mLocale.equals(Locale.CHINA)) {
-            mUtils = new SimplifiedChineseContactUtils(mLocale);
+        if (mLocales.isPrimaryLanguage(JAPANESE_LANGUAGE)) {
+            mUtils = new JapaneseContactUtils(mLocales);
+        } else if (mLocales.isPrimaryLocaleSimplifiedChinese()) {
+            mUtils = new SimplifiedChineseContactUtils(mLocales);
         } else {
-            mUtils = new ContactLocaleUtilsBase(mLocale);
+            mUtils = new ContactLocaleUtilsBase(mLocales);
         }
-        Log.i(TAG, "AddressBook Labels [" + mLocale.toString() + "]: "
-              + getLabels().toString());
+        Log.i(TAG, "AddressBook Labels [" + mLocales.toString() + "]: "
+                + getLabels().toString());
     }
 
-    public boolean isLocale(Locale locale) {
-        return mLocale.equals(locale);
+    public boolean isLocale(LocaleSet locales) {
+        return mLocales.equals(locales);
     }
 
     public static synchronized ContactLocaleUtils getInstance() {
         if (sSingleton == null) {
-            sSingleton = new ContactLocaleUtils(null);
+            sSingleton = new ContactLocaleUtils(LocaleSet.getDefault());
         }
         return sSingleton;
     }
 
+    @VisibleForTesting
     public static synchronized void setLocale(Locale locale) {
-        if (sSingleton == null || !sSingleton.isLocale(locale)) {
-            sSingleton = new ContactLocaleUtils(locale);
+        setLocales(new LocaleSet(locale));
+    }
+
+    public static synchronized void setLocales(LocaleSet locales) {
+        if (sSingleton == null || !sSingleton.isLocale(locales)) {
+            sSingleton = new ContactLocaleUtils(locales);
         }
     }
 
@@ -464,7 +479,7 @@
 
     /**
      *  Determine which utility should be used for generating NameLookupKey.
-     *  (ie, whether we generate Pinyin lookup keys or not)
+     *  (ie, whether we generate Romaji or 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
@@ -475,10 +490,17 @@
      *  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);
+        if (!mLocales.isPrimaryLocaleCJK()) {
+            if (mLocales.isSecondaryLocaleSimplifiedChinese()) {
+                if (nameStyle == FullNameStyle.CHINESE ||
+                        nameStyle == FullNameStyle.CJK) {
+                    return SimplifiedChineseContactUtils.getPinyinNameLookupKeys(name);
+                }
+            } else {
+                if (nameStyle == FullNameStyle.JAPANESE) {
+                    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 82cf310..265846c 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -3266,9 +3266,9 @@
      * Checks whether the current ICU code version matches that used to build
      * the locale specific data in the ContactsDB.
      */
-    public boolean needsToUpdateLocaleData(Locale locale) {
+    public boolean needsToUpdateLocaleData(LocaleSet locales) {
         final String dbLocale = getProperty(DbProperties.LOCALE, "");
-        if (!dbLocale.equals(locale.toString())) {
+        if (!dbLocale.equals(locales.toString())) {
             return true;
         }
         final String curICUVersion = ICU.getIcuVersion();
@@ -3283,16 +3283,17 @@
     }
 
     private void upgradeLocaleData(SQLiteDatabase db, boolean rebuildSqliteStats) {
-        final Locale locale = Locale.getDefault();
-        Log.i(TAG, "Upgrading locale data for " + locale
+        final String dbLocale = getProperty(DbProperties.LOCALE, "");
+        final LocaleSet locales = LocaleSet.getDefault();
+        Log.i(TAG, "Upgrading locale data for " + locales
                 + " (ICU v" + ICU.getIcuVersion() + ")");
         final long start = SystemClock.elapsedRealtime();
         initializeCache(db);
-        rebuildLocaleData(db, locale, rebuildSqliteStats);
+        rebuildLocaleData(db, locales, rebuildSqliteStats);
         Log.i(TAG, "Locale update completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
     }
 
-    private void rebuildLocaleData(SQLiteDatabase db, Locale locale, boolean rebuildSqliteStats) {
+    private void rebuildLocaleData(SQLiteDatabase db, LocaleSet locales, 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");
@@ -3306,7 +3307,7 @@
         // 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());
+        setProperty(db, DbProperties.LOCALE, locales.toString());
     }
 
     /**
@@ -3314,19 +3315,19 @@
      * nickname_lookup, name_lookup and sort keys. Invalidates the fast
      * scrolling index cache.
      */
-    public void setLocale(Locale locale) {
-        if (!needsToUpdateLocaleData(locale)) {
+    public void setLocale(LocaleSet locales) {
+        if (!needsToUpdateLocaleData(locales)) {
             return;
         }
-        Log.i(TAG, "Switching to locale " + locale
+        Log.i(TAG, "Switching to locale " + locales
                 + " (ICU v" + ICU.getIcuVersion() + ")");
 
         final long start = SystemClock.elapsedRealtime();
         SQLiteDatabase db = getWritableDatabase();
-        db.setLocale(locale);
+        db.setLocale(locales.getPrimaryLocale());
         db.beginTransaction();
         try {
-            rebuildLocaleData(db, locale, true);
+            rebuildLocaleData(db, locales, true);
             db.setTransactionSuccessful();
         } finally {
             db.endTransaction();
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index cafd86b..e610a26 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -1406,7 +1406,7 @@
 
     private boolean mSyncToNetwork;
 
-    private Locale mCurrentLocale;
+    private LocaleSet mCurrentLocales;
     private int mContactsAccountCount;
 
     private HandlerThread mBackgroundThread;
@@ -1504,6 +1504,40 @@
         return true;
     }
 
+    // Updates the locale set to reflect a new system locale.
+    private static LocaleSet updateLocaleSet(LocaleSet oldLocales, Locale newLocale) {
+        final Locale prevLocale = oldLocales.getPrimaryLocale();
+        // If primary locale is unchanged then no change to locale set.
+        if (newLocale.equals(prevLocale)) {
+            return oldLocales;
+        }
+        // Otherwise, construct a new locale set based on the new locale
+        // and the previous primary locale.
+        return new LocaleSet(newLocale, prevLocale).normalize();
+    }
+
+    private static LocaleSet getProviderPrefLocales(SharedPreferences prefs) {
+        final String providerLocaleString = prefs.getString(PREF_LOCALE, null);
+        return LocaleSet.getLocaleSet(providerLocaleString);
+    }
+
+    // Called by initForDefaultLocale. Returns an updated locale set using the
+    // current system locale.
+    private LocaleSet getLocaleSet() {
+        final Locale curLocale = getLocale();
+        if (mCurrentLocales != null) {
+            return updateLocaleSet(mCurrentLocales, curLocale);
+        }
+        // On startup need to reload the locale set from prefs for update.
+        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
+        return updateLocaleSet(getProviderPrefLocales(prefs), curLocale);
+    }
+
+    // Static routine called on startup by updateLocaleOffline.
+    private static LocaleSet getLocaleSet(SharedPreferences prefs, Locale curLocale) {
+        return updateLocaleSet(getProviderPrefLocales(prefs), curLocale);
+    }
+
     /**
      * (Re)allocates all locale-sensitive structures.
      */
@@ -1511,12 +1545,12 @@
         Context context = getContext();
         mLegacyApiSupport =
                 new LegacyApiSupport(context, mContactsHelper, this, mGlobalSearchSupport);
-        mCurrentLocale = getLocale();
-        mNameSplitter = mContactsHelper.createNameSplitter(mCurrentLocale);
+        mCurrentLocales = getLocaleSet();
+        mNameSplitter = mContactsHelper.createNameSplitter(mCurrentLocales.getPrimaryLocale());
         mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
-        mPostalSplitter = new PostalSplitter(mCurrentLocale);
+        mPostalSplitter = new PostalSplitter(mCurrentLocales.getPrimaryLocale());
         mCommonNicknameCache = new CommonNicknameCache(mContactsHelper.getReadableDatabase());
-        ContactLocaleUtils.setLocale(mCurrentLocale);
+        ContactLocaleUtils.setLocales(mCurrentLocales);
         mContactAggregator = new ContactAggregator(this, mContactsHelper,
                 createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
         mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
@@ -1694,20 +1728,20 @@
     }
 
     private static boolean needsToUpdateLocaleData(SharedPreferences prefs,
-            Locale locale,ContactsDatabaseHelper contactsHelper,
+            LocaleSet locales, ContactsDatabaseHelper contactsHelper,
             ProfileDatabaseHelper profileHelper) {
-        final String providerLocale = prefs.getString(PREF_LOCALE, null);
+        final String providerLocales = 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());
+        if (!locales.toString().equals(providerLocales)) {
+            Log.i(TAG, "Locale has changed from " + providerLocales
+                    + " to " + locales);
             return true;
         }
-        if (contactsHelper.needsToUpdateLocaleData(locale) ||
-                profileHelper.needsToUpdateLocaleData(locale)) {
+        if (contactsHelper.needsToUpdateLocaleData(locales) ||
+                profileHelper.needsToUpdateLocaleData(locales)) {
             return true;
         }
         return false;
@@ -1727,18 +1761,18 @@
             return;
         }
 
-        final Locale currentLocale = mCurrentLocale;
+        final LocaleSet currentLocales = mCurrentLocales;
         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
-        if (!needsToUpdateLocaleData(prefs, currentLocale, mContactsHelper, mProfileHelper)) {
+        if (!needsToUpdateLocaleData(prefs, currentLocales, mContactsHelper, mProfileHelper)) {
             return;
         }
 
         int providerStatus = mProviderStatus;
         setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE);
-        mContactsHelper.setLocale(currentLocale);
-        mProfileHelper.setLocale(currentLocale);
+        mContactsHelper.setLocale(currentLocales);
+        mProfileHelper.setLocale(currentLocales);
         mSearchIndexManager.updateIndex(true);
-        prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).commit();
+        prefs.edit().putString(PREF_LOCALE, currentLocales.toString()).commit();
         setProviderStatus(providerStatus);
     }
 
@@ -1750,17 +1784,16 @@
             Context context,
             ContactsDatabaseHelper contactsHelper,
             ProfileDatabaseHelper profileHelper) {
-
-        final Locale currentLocale = Locale.getDefault();
         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
-        if (!needsToUpdateLocaleData(prefs, currentLocale, contactsHelper, profileHelper)) {
+        final LocaleSet currentLocales = getLocaleSet(prefs, Locale.getDefault());
+        if (!needsToUpdateLocaleData(prefs, currentLocales, contactsHelper, profileHelper)) {
             return;
         }
 
-        contactsHelper.setLocale(currentLocale);
-        profileHelper.setLocale(currentLocale);
+        contactsHelper.setLocale(currentLocales);
+        profileHelper.setLocale(currentLocales);
         contactsHelper.rebuildSearchIndex();
-        prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).commit();
+        prefs.edit().putString(PREF_LOCALE, currentLocales.toString()).commit();
     }
 
     /**
diff --git a/src/com/android/providers/contacts/HanziToPinyin.java b/src/com/android/providers/contacts/HanziToPinyin.java
index 0c35e21..d140439 100644
--- a/src/com/android/providers/contacts/HanziToPinyin.java
+++ b/src/com/android/providers/contacts/HanziToPinyin.java
@@ -121,12 +121,19 @@
         }
     }
 
+    public String transliterate(final String input) {
+        if (!hasChineseTransliterator() || TextUtils.isEmpty(input)) {
+            return null;
+        }
+        return mPinyinTransliterator.transliterate(input);
+    }
+
     /**
      * 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 there is no Chinese transliterator, the empty token array is returned.
      */
-    public ArrayList<Token> get(final String input) {
+    public ArrayList<Token> getTokens(final String input) {
         ArrayList<Token> tokens = new ArrayList<Token>();
         if (!hasChineseTransliterator() || TextUtils.isEmpty(input)) {
             // return empty tokens.
diff --git a/src/com/android/providers/contacts/LocaleSet.java b/src/com/android/providers/contacts/LocaleSet.java
new file mode 100644
index 0000000..63638c6
--- /dev/null
+++ b/src/com/android/providers/contacts/LocaleSet.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2014 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.text.TextUtils;
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Locale;
+
+public class LocaleSet {
+    private static final String CHINESE_LANGUAGE = Locale.CHINESE.getLanguage().toLowerCase();
+    private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase();
+    private static final String KOREAN_LANGUAGE = Locale.KOREAN.getLanguage().toLowerCase();
+
+    private static class LocaleWrapper {
+        private final Locale mLocale;
+        private final String mLanguage;
+        private final boolean mLocaleIsCJK;
+
+        private static boolean isLanguageCJK(String language) {
+            return CHINESE_LANGUAGE.equals(language) ||
+                JAPANESE_LANGUAGE.equals(language) ||
+                KOREAN_LANGUAGE.equals(language);
+        }
+
+        public LocaleWrapper(Locale locale) {
+            mLocale = locale;
+            if (mLocale != null) {
+                mLanguage = mLocale.getLanguage().toLowerCase();
+                mLocaleIsCJK = isLanguageCJK(mLanguage);
+            } else {
+                mLanguage = null;
+                mLocaleIsCJK = false;
+            }
+        }
+
+        public boolean hasLocale() {
+            return mLocale != null;
+        }
+
+        public Locale getLocale() {
+            return mLocale;
+        }
+
+        public boolean isLocale(Locale locale) {
+            return mLocale == null ? (locale == null) : mLocale.equals(locale);
+        }
+
+        public boolean isLocaleCJK() {
+            return mLocaleIsCJK;
+        }
+
+        public boolean isLanguage(String language) {
+            return mLanguage == null ? (language == null)
+                : mLanguage.equalsIgnoreCase(language);
+        }
+
+        public String toString() {
+            return mLocale != null ? mLocale.toLanguageTag() : "(null)";
+        }
+    }
+
+    public static LocaleSet getDefault() {
+        return new LocaleSet(Locale.getDefault());
+    }
+
+    public LocaleSet(Locale locale) {
+        this(locale, null);
+    }
+
+    /**
+     * Returns locale set for a given set of IETF BCP-47 tags separated by ';'.
+     * BCP-47 tags are what is used by ICU 52's toLanguageTag/forLanguageTag
+     * methods to represent individual Locales: "en-US" for Locale.US,
+     * "zh-CN" for Locale.CHINA, etc. So eg "en-US;zh-CN" specifies the locale
+     * set LocaleSet(Locale.US, Locale.CHINA).
+     *
+     * @param localeString One or more BCP-47 tags separated by ';'.
+     * @return LocaleSet for specified locale string, or default set if null
+     * or unable to parse.
+     */
+    public static LocaleSet getLocaleSet(String localeString) {
+        // Locale.toString() generates strings like "en_US" and "zh_CN_#Hans".
+        // Locale.toLanguageTag() generates strings like "en-US" and "zh-Hans-CN".
+        // We can only parse language tags.
+        if (localeString != null && localeString.indexOf('_') == -1) {
+            final String[] locales = localeString.split(";");
+            final Locale primaryLocale = Locale.forLanguageTag(locales[0]);
+            // ICU tags undefined/unparseable locales "und"
+            if (primaryLocale != null &&
+                    !TextUtils.equals(primaryLocale.toLanguageTag(), "und")) {
+                if (locales.length > 1 && locales[1] != null) {
+                    final Locale secondaryLocale = Locale.forLanguageTag(locales[1]);
+                    if (secondaryLocale != null &&
+                            !TextUtils.equals(secondaryLocale.toLanguageTag(), "und")) {
+                        return new LocaleSet(primaryLocale, secondaryLocale);
+                    }
+                }
+                return new LocaleSet(primaryLocale);
+            }
+        }
+        return getDefault();
+    }
+
+    private final LocaleWrapper mPrimaryLocale;
+    private final LocaleWrapper mSecondaryLocale;
+
+    public LocaleSet(Locale primaryLocale, Locale secondaryLocale) {
+        mPrimaryLocale = new LocaleWrapper(primaryLocale);
+        mSecondaryLocale = new LocaleWrapper(
+                mPrimaryLocale.equals(secondaryLocale) ? null : secondaryLocale);
+    }
+
+    public LocaleSet normalize() {
+        final Locale primaryLocale = getPrimaryLocale();
+        if (primaryLocale == null) {
+            return getDefault();
+        }
+        Locale secondaryLocale = getSecondaryLocale();
+        // disallow both locales with same language (redundant and/or conflicting)
+        // disallow both locales CJK (conflicting rules)
+        if (secondaryLocale == null ||
+                isPrimaryLanguage(secondaryLocale.getLanguage()) ||
+                (isPrimaryLocaleCJK() && isSecondaryLocaleCJK())) {
+            return new LocaleSet(primaryLocale);
+        }
+        // unnecessary to specify English as secondary locale (redundant)
+        if (isSecondaryLanguage(Locale.ENGLISH.getLanguage())) {
+            return new LocaleSet(primaryLocale);
+        }
+        return this;
+    }
+
+    public boolean hasSecondaryLocale() {
+        return mSecondaryLocale.hasLocale();
+    }
+
+    public Locale getPrimaryLocale() {
+        return mPrimaryLocale.getLocale();
+    }
+
+    public Locale getSecondaryLocale() {
+        return mSecondaryLocale.getLocale();
+    }
+
+    public boolean isPrimaryLocale(Locale locale) {
+        return mPrimaryLocale.isLocale(locale);
+    }
+
+    public boolean isSecondaryLocale(Locale locale) {
+        return mSecondaryLocale.isLocale(locale);
+    }
+
+    private static final String SCRIPT_SIMPLIFIED_CHINESE = "Hans";
+    private static final String SCRIPT_TRADITIONAL_CHINESE = "Hant";
+
+    @VisibleForTesting
+    public static boolean isLocaleSimplifiedChinese(Locale locale) {
+        // language must match
+        if (locale == null || !TextUtils.equals(locale.getLanguage(), CHINESE_LANGUAGE)) {
+            return false;
+        }
+        // script is optional but if present must match
+        if (!TextUtils.isEmpty(locale.getScript())) {
+            return locale.getScript().equals(SCRIPT_SIMPLIFIED_CHINESE);
+        }
+        // if no script, must match known country
+        return locale.equals(Locale.SIMPLIFIED_CHINESE);
+    }
+
+    public boolean isPrimaryLocaleSimplifiedChinese() {
+        return isLocaleSimplifiedChinese(getPrimaryLocale());
+    }
+
+    public boolean isSecondaryLocaleSimplifiedChinese() {
+        return isLocaleSimplifiedChinese(getSecondaryLocale());
+    }
+
+    @VisibleForTesting
+    public static boolean isLocaleTraditionalChinese(Locale locale) {
+        // language must match
+        if (locale == null || !TextUtils.equals(locale.getLanguage(), CHINESE_LANGUAGE)) {
+            return false;
+        }
+        // script is optional but if present must match
+        if (!TextUtils.isEmpty(locale.getScript())) {
+            return locale.getScript().equals(SCRIPT_TRADITIONAL_CHINESE);
+        }
+        // if no script, must match known country
+        return locale.equals(Locale.TRADITIONAL_CHINESE);
+    }
+
+    public boolean isPrimaryLocaleTraditionalChinese() {
+        return isLocaleTraditionalChinese(getPrimaryLocale());
+    }
+
+    public boolean isSecondaryLocaleTraditionalChinese() {
+        return isLocaleTraditionalChinese(getSecondaryLocale());
+    }
+
+    public boolean isPrimaryLocaleCJK() {
+        return mPrimaryLocale.isLocaleCJK();
+    }
+
+    public boolean isSecondaryLocaleCJK() {
+        return mSecondaryLocale.isLocaleCJK();
+    }
+
+    public boolean isPrimaryLanguage(String language) {
+        return mPrimaryLocale.isLanguage(language);
+    }
+
+    public boolean isSecondaryLanguage(String language) {
+        return mSecondaryLocale.isLanguage(language);
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        if (object == this) {
+            return true;
+        }
+        if (object instanceof LocaleSet) {
+            final LocaleSet other = (LocaleSet) object;
+            return other.isPrimaryLocale(mPrimaryLocale.getLocale())
+                && other.isSecondaryLocale(mSecondaryLocale.getLocale());
+        }
+        return false;
+    }
+
+    @Override
+    public final String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append(mPrimaryLocale.toString());
+        if (hasSecondaryLocale()) {
+            builder.append(";");
+            builder.append(mSecondaryLocale.toString());
+        }
+        return builder.toString();
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
index 7213941..394a1aa 100644
--- a/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
+++ b/tests/src/com/android/providers/contacts/ContactLocaleUtilsTest.java
@@ -19,6 +19,7 @@
 import android.provider.ContactsContract.FullNameStyle;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
 
 import java.text.Collator;
 import java.util.ArrayList;
@@ -30,6 +31,8 @@
 
 @SmallTest
 public class ContactLocaleUtilsTest extends AndroidTestCase {
+    private static final String TAG = "ContactLocaleUtilsTest";
+
     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";
@@ -94,7 +97,8 @@
     private static final Locale LOCALE_ARABIC = new Locale("ar");
     private static final Locale LOCALE_SERBIAN = new Locale("sr");
     private static final Locale LOCALE_UKRAINIAN = new Locale("uk");
-    private boolean hasChineseCollator;
+    private boolean hasSimplifiedChineseCollator;
+    private boolean hasTraditionalChineseCollator;
     private boolean hasJapaneseCollator;
     private boolean hasKoreanCollator;
     private boolean hasArabicCollator;
@@ -107,8 +111,10 @@
         super.setUp();
         final Locale locale[] = Collator.getAvailableLocales();
         for (int i = 0; i < locale.length; i++) {
-            if (locale[i].equals(Locale.CHINA)) {
-                hasChineseCollator = true;
+            if (LocaleSet.isLocaleSimplifiedChinese(locale[i])) {
+                hasSimplifiedChineseCollator = true;
+            } else if (LocaleSet.isLocaleTraditionalChinese(locale[i])) {
+                hasTraditionalChineseCollator = true;
             } else if (locale[i].equals(Locale.JAPAN)) {
                 hasJapaneseCollator = true;
             } else if (locale[i].equals(Locale.KOREA)) {
@@ -166,6 +172,7 @@
 
     public void testJapaneseContactLocaleUtils() throws Exception {
         if (!hasJapaneseCollator) {
+            Log.w(TAG, "Japanese collator not found; skipping test");
             return;
         }
 
@@ -180,13 +187,13 @@
         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"));
+        verifyLabels(getLabels(), LABELS_JA_JP);
     }
 
     public void testChineseContactLocaleUtils() throws Exception {
-        if (!hasChineseCollator) {
+        if (!hasSimplifiedChineseCollator) {
+            Log.w(TAG, "Simplified Chinese collator not found; skipping test");
             return;
         }
 
@@ -199,12 +206,16 @@
         assertEquals("B", getLabel("Bob Smith"));
         verifyLabels(getLabels(), LABELS_EN_US);
 
-        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));
+        if (hasTraditionalChineseCollator) {
+            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));
+        } else {
+            Log.w(TAG, "Traditional Chinese collator not found");
+        }
 
         ContactLocaleUtils.setLocale(Locale.SIMPLIFIED_CHINESE);
         Iterator<String> keys = getNameLookupKeys(CHINESE_NAME,
@@ -217,14 +228,45 @@
         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"));
+        if (hasTraditionalChineseCollator) {
+            ContactLocaleUtils.setLocale(Locale.TRADITIONAL_CHINESE);
+            assertEquals("B", getLabel("Bob Smith"));
+            verifyLabels(getLabels(), LABELS_ZH_TW);
+        }
+    }
+
+    public void testPinyinEnabledSecondaryLocale() throws Exception {
+        if (!hasSimplifiedChineseCollator) {
+            Log.w(TAG, "Simplified Chinese collator not found; skipping test");
+            return;
+        }
+
+        ContactLocaleUtils.setLocales(
+                new LocaleSet(Locale.ENGLISH, Locale.SIMPLIFIED_CHINESE));
+        assertEquals("D", getLabel(CHINESE_NAME));
+
+        Iterator<String> keys = getNameLookupKeys(CHINESE_NAME,
+                FullNameStyle.CHINESE);
+        verifyKeys(keys, CHINESE_NAME_KEY);
+    }
+
+    public void testPinyinDisabledSecondaryLocale() throws Exception {
+        if (!hasSimplifiedChineseCollator) {
+            Log.w(TAG, "Simplified Chinese collator not found; skipping test");
+            return;
+        }
+
+        ContactLocaleUtils.setLocales(
+                new LocaleSet(Locale.ENGLISH, Locale.JAPAN));
+        assertEquals("", getLabel(CHINESE_NAME));
+
+        assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CHINESE));
+        assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CJK));
     }
 
     public void testChineseStyleNameWithDifferentLocale() throws Exception {
-        if (!hasChineseCollator) {
+        if (!hasSimplifiedChineseCollator) {
+            Log.w(TAG, "Simplified Chinese collator not found; skipping test");
             return;
         }
 
@@ -232,7 +274,7 @@
         assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CHINESE));
         assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CJK));
 
-        ContactLocaleUtils.setLocale(Locale.CHINA);
+        ContactLocaleUtils.setLocale(Locale.SIMPLIFIED_CHINESE);
         Iterator<String> keys = getNameLookupKeys(CHINESE_NAME,
                 FullNameStyle.CJK);
         verifyKeys(keys, CHINESE_NAME_KEY);
@@ -241,12 +283,15 @@
         keys = getNameLookupKeys(LATIN_NAME_2, FullNameStyle.WESTERN);
         verifyKeys(keys, LATIN_NAME_KEY_2);
 
-        ContactLocaleUtils.setLocale(Locale.TRADITIONAL_CHINESE);
-        assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CJK));
+        if (hasTraditionalChineseCollator) {
+            ContactLocaleUtils.setLocale(Locale.TRADITIONAL_CHINESE);
+            assertNull(getNameLookupKeys(CHINESE_NAME, FullNameStyle.CJK));
+        }
     }
 
     public void testKoreanContactLocaleUtils() throws Exception {
         if (!hasKoreanCollator) {
+            Log.w(TAG, "Korean collator not found; skipping test");
             return;
         }
 
@@ -261,6 +306,7 @@
 
     public void testArabicContactLocaleUtils() throws Exception {
         if (!hasArabicCollator) {
+            Log.w(TAG, "Arabic collator not found; skipping test");
             return;
         }
 
@@ -272,6 +318,7 @@
 
     public void testSerbianContactLocaleUtils() throws Exception {
         if (!hasSerbianCollator) {
+            Log.w(TAG, "Serbian collator not found; skipping test");
             return;
         }
 
@@ -282,6 +329,7 @@
 
     public void testUkrainianContactLocaleUtils() throws Exception {
         if (!hasUkrainianCollator) {
+            Log.w(TAG, "Ukrainian collator not found; skipping test");
             return;
         }
 
diff --git a/tests/src/com/android/providers/contacts/HanziToPinyinTest.java b/tests/src/com/android/providers/contacts/HanziToPinyinTest.java
index e797f2c..a39b5df 100644
--- a/tests/src/com/android/providers/contacts/HanziToPinyinTest.java
+++ b/tests/src/com/android/providers/contacts/HanziToPinyinTest.java
@@ -40,7 +40,7 @@
 
     private void test(final char hanzi, final String expectedPinyin) throws Exception {
         final String hanziString = Character.toString(hanzi);
-        ArrayList<Token> tokens = HanziToPinyin.getInstance().get(hanziString);
+        ArrayList<Token> tokens = HanziToPinyin.getInstance().getTokens(hanziString);
         assertEquals(tokens.size(), 1);
         final String newString = tokens.get(0).target;
         if (TextUtils.isEmpty(expectedPinyin)) {
@@ -65,27 +65,27 @@
         if (!hasChineseTransliterator()) {
             return;
         }
-        ArrayList<Token> tokens = HanziToPinyin.getInstance().get(ONE_HANZI);
+        ArrayList<Token> tokens = HanziToPinyin.getInstance().getTokens(ONE_HANZI);
         assertEquals(tokens.size(), 1);
         assertEquals(tokens.get(0).type, Token.PINYIN);
         assertTrue(tokens.get(0).target.equalsIgnoreCase("DU"));
 
-        tokens = HanziToPinyin.getInstance().get(TWO_HANZI);
+        tokens = HanziToPinyin.getInstance().getTokens(TWO_HANZI);
         assertEquals(tokens.size(), 2);
         assertEquals(tokens.get(0).type, Token.PINYIN);
         assertEquals(tokens.get(1).type, Token.PINYIN);
         assertTrue(tokens.get(0).target.equalsIgnoreCase("DU"));
         assertTrue(tokens.get(1).target.equalsIgnoreCase("JUAN"));
 
-        tokens = HanziToPinyin.getInstance().get(ASSIC);
+        tokens = HanziToPinyin.getInstance().getTokens(ASSIC);
         assertEquals(tokens.size(), 1);
         assertEquals(tokens.get(0).type, Token.LATIN);
 
-        tokens = HanziToPinyin.getInstance().get(ONE_UNKNOWN);
+        tokens = HanziToPinyin.getInstance().getTokens(ONE_UNKNOWN);
         assertEquals(tokens.size(), 1);
         assertEquals(tokens.get(0).type, Token.UNKNOWN);
 
-        tokens = HanziToPinyin.getInstance().get(MISC);
+        tokens = HanziToPinyin.getInstance().getTokens(MISC);
         assertEquals(tokens.size(), 7);
         assertEquals(tokens.get(0).type, Token.LATIN);
         assertEquals(tokens.get(1).type, Token.PINYIN);
diff --git a/tests/src/com/android/providers/contacts/LocaleSetTest.java b/tests/src/com/android/providers/contacts/LocaleSetTest.java
new file mode 100644
index 0000000..4c1b8d3
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/LocaleSetTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014 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 java.util.Locale;
+import junit.framework.TestCase;
+
+@SmallTest
+public class LocaleSetTest extends TestCase {
+    private void testLocaleStringsHelper(Locale primaryLocale,
+            Locale secondaryLocale, final String expectedString) throws Exception {
+        final LocaleSet locales = new LocaleSet(primaryLocale, secondaryLocale);
+        final String localeString = locales.toString();
+        assertEquals(expectedString, localeString);
+
+        final LocaleSet parseLocales = LocaleSet.getLocaleSet(localeString);
+        assertEquals(locales, parseLocales);
+    }
+
+    @SmallTest
+    public void testLocaleStrings() throws Exception {
+        testLocaleStringsHelper(Locale.US, null, "en-US");
+        testLocaleStringsHelper(Locale.US, Locale.CHINA, "en-US;zh-CN");
+        testLocaleStringsHelper(Locale.JAPAN, Locale.GERMANY, "ja-JP;de-DE");
+    }
+
+    private void testNormalizationHelper(String localeString,
+            Locale expectedPrimary, Locale expectedSecondary) throws Exception {
+        final LocaleSet expected = new LocaleSet(expectedPrimary, expectedSecondary);
+        final LocaleSet actual = LocaleSet.getLocaleSet(localeString).normalize();
+        assertEquals(expected, actual);
+    }
+
+    @SmallTest
+    public void testNormalization() throws Exception {
+        // Single locale
+        testNormalizationHelper("en-US", Locale.US, null);
+        // Disallow secondary with same language as primary
+        testNormalizationHelper("fr-CA;fr-FR", Locale.CANADA_FRENCH, null);
+        testNormalizationHelper("en-US;zh-CN", Locale.US, Locale.CHINA);
+        // Disallow both locales CJK
+        testNormalizationHelper("ja-JP;zh-CN", Locale.JAPAN, null);
+        // Disallow en as secondary (happens by default)
+        testNormalizationHelper("zh-CN;en-US", Locale.CHINA, null);
+        testNormalizationHelper("zh-CN;de-DE", Locale.CHINA, Locale.GERMANY);
+    }
+}