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);
+ }
+}