summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk3
-rw-r--r--src/com/android/launcher3/AppDrawerListAdapter.java101
-rw-r--r--src/com/android/launcher3/locale/HanziToPinyin.java186
-rw-r--r--src/com/android/launcher3/locale/LocaleSet.java253
-rw-r--r--src/com/android/launcher3/locale/LocaleSetManager.java82
-rw-r--r--src/com/android/launcher3/locale/LocaleUtils.java484
6 files changed, 1073 insertions, 36 deletions
diff --git a/Android.mk b/Android.mk
index c7e2dfde1..b4f2e69e1 100644
--- a/Android.mk
+++ b/Android.mk
@@ -24,7 +24,8 @@ include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13 \
- android-support-v7-recyclerview
+ android-support-v7-recyclerview \
+ guava
LOCAL_SRC_FILES := $(call all-java-files-under, src) \
$(call all-java-files-under, WallpaperPicker/src) \
diff --git a/src/com/android/launcher3/AppDrawerListAdapter.java b/src/com/android/launcher3/AppDrawerListAdapter.java
index ad6b35067..f0044212d 100644
--- a/src/com/android/launcher3/AppDrawerListAdapter.java
+++ b/src/com/android/launcher3/AppDrawerListAdapter.java
@@ -19,18 +19,21 @@ package com.android.launcher3;
import android.content.ComponentName;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.support.v7.widget.RecyclerView;
import android.widget.LinearLayout;
import android.widget.SectionIndexer;
+import com.android.launcher3.locale.LocaleSetManager;
+import com.android.launcher3.locale.LocaleUtils;
+import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
-import java.util.ListIterator;
/**
* AppDrawerListAdapter - list adapter for the vertical app drawer
@@ -38,7 +41,6 @@ import java.util.ListIterator;
public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdapter.ViewHolder>
implements View.OnLongClickListener, DragSource, SectionIndexer {
- private static final char NUMERIC_OR_SPECIAL_CHAR = '#';
private static final String NUMERIC_OR_SPECIAL_HEADER = "#";
private ArrayList<AppItemIndexedInfo> mHeaderList;
@@ -49,6 +51,7 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdap
private LinkedHashMap<String, Integer> mSectionHeaders;
private LinearLayout.LayoutParams mIconParams;
private Rect mIconRect;
+ private LocaleSetManager mLocaleSetManager;
public enum DrawerType {
Drawer(0),
@@ -88,6 +91,9 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdap
mHeaderList = new ArrayList<AppItemIndexedInfo>();
mDeviceProfile = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
mLayoutInflater = LayoutInflater.from(launcher);
+
+ mLocaleSetManager = new LocaleSetManager(mLauncher);
+ mLocaleSetManager.updateLocaleSet(mLocaleSetManager.getSystemLocaleSet());
initParams();
}
@@ -106,50 +112,51 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdap
*/
public void populateByCharacter(ArrayList<AppInfo> info) {
if (info == null || info.size() <= 0) {
+ Collections.sort(mHeaderList);
return;
}
// Create a clone of AppInfo ArrayList to preserve data
- ArrayList<AppInfo> tempInfo = new ArrayList<AppInfo>(info.size());
- for (AppInfo i : info) {
- tempInfo.add(i);
- }
+ ArrayList<AppInfo> tempInfo = (ArrayList<AppInfo>) info.clone();
- ListIterator<AppInfo> it = tempInfo.listIterator();
ArrayList<AppInfo> appInfos = new ArrayList<AppInfo>();
- appInfos.clear();
// get next app
- AppInfo app = it.next();
+ AppInfo app = tempInfo.get(0);
// get starting character
- boolean isSpecial = false;
- char startChar = app.title.toString().toUpperCase().charAt(0);
- if (!Character.isLetter(startChar)) {
- isSpecial = true;
+ LocaleUtils localeUtils = LocaleUtils.getInstance();
+ int bucketIndex = localeUtils.getBucketIndex(app.title.toString());
+ String startString
+ = localeUtils.getBucketLabel(bucketIndex);
+ if (TextUtils.isEmpty(startString)) {
+ startString = NUMERIC_OR_SPECIAL_HEADER;
+ bucketIndex = localeUtils.getBucketIndex(startString);
}
// now iterate through
for (AppInfo info1 : tempInfo) {
- char newChar = info1.title.toString().toUpperCase().charAt(0);
+ int newBucketIndex = localeUtils.getBucketIndex(info1.title.toString());
+
+ String newChar
+ = localeUtils.getBucketLabel(newBucketIndex);
+ if (TextUtils.isEmpty(newChar)) {
+ newChar = NUMERIC_OR_SPECIAL_HEADER;
+ }
// if same character
- if (newChar == startChar) {
+ if (newChar.equals(startString)) {
// add it
appInfos.add(info1);
- } else if (isSpecial && !Character.isLetter(newChar)) {
- appInfos.add(info1);
}
}
+ Collections.sort(appInfos, LauncherModel.getAppNameComparator());
+
for (int i = 0; i < appInfos.size(); i += mDeviceProfile.numColumnsBase) {
int endIndex = (int) Math.min(i + mDeviceProfile.numColumnsBase, appInfos.size());
ArrayList<AppInfo> subList = new ArrayList<AppInfo>(appInfos.subList(i, endIndex));
AppItemIndexedInfo indexInfo;
- if (isSpecial) {
- indexInfo = new AppItemIndexedInfo('#', subList, i != 0);
- } else {
- indexInfo = new AppItemIndexedInfo(startChar, subList, i != 0);
- }
+ indexInfo = new AppItemIndexedInfo(startString, bucketIndex, subList, i != 0);
mHeaderList.add(indexInfo);
}
@@ -163,7 +170,6 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdap
public void setApps(ArrayList<AppInfo> list) {
if (!LauncherAppState.isDisableAllApps()) {
mHeaderList.clear();
- Collections.sort(list, LauncherModel.getAppNameComparator());
populateByCharacter(list);
populateSectionHeaders();
mLauncher.updateScrubber();
@@ -179,7 +185,7 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdap
for (int i = 0; i < mHeaderList.size(); i++) {
AppItemIndexedInfo info = mHeaderList.get(i);
if (!mHeaderList.get(i).isChild) {
- mSectionHeaders.put(String.valueOf(mHeaderList.get(i).mChar), count);
+ mSectionHeaders.put(String.valueOf(mHeaderList.get(i).mStartString), count);
}
if (info.mInfo.size() < mDeviceProfile.numColumnsBase) {
count++;
@@ -227,14 +233,22 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdap
private void addAppsWithoutInvalidate(ArrayList<AppInfo> list) {
// We add it in place, in alphabetical order
+ LocaleUtils localeUtils = LocaleUtils.getInstance();
+
int count = list.size();
for (int i = 0; i < count; ++i) {
AppInfo info = list.get(i);
boolean found = false;
AppItemIndexedInfo lastInfoForSection = null;
+ int bucketIndex = localeUtils.getBucketIndex(info.title.toString());
+ String start = localeUtils.getBucketLabel(bucketIndex);
+ if (TextUtils.isEmpty(start)) {
+ start = NUMERIC_OR_SPECIAL_HEADER;
+ bucketIndex = localeUtils.getBucketIndex(start);
+ }
for (int j = 0; j < mHeaderList.size(); ++j) {
AppItemIndexedInfo indexedInfo = mHeaderList.get(j);
- if (info.title.charAt(0) == indexedInfo.mChar) {
+ if (start.equals(indexedInfo.mStartString)) {
Collections.sort(indexedInfo.mInfo, LauncherModel.getAppNameComparator());
int index =
Collections.binarySearch(indexedInfo.mInfo,
@@ -255,8 +269,9 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdap
ArrayList<AppInfo> newInfos = new ArrayList<AppInfo>();
newInfos.add(info);
AppItemIndexedInfo newInfo =
- new AppItemIndexedInfo(info.title.charAt(0), newInfos, false);
+ new AppItemIndexedInfo(start, bucketIndex, newInfos, false);
mHeaderList.add(newInfo);
+ Collections.sort(mHeaderList);
}
}
}
@@ -343,10 +358,10 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdap
AppItemIndexedInfo indexedInfo = mHeaderList.get(position);
holder.mTextView.setVisibility(indexedInfo.isChild ? View.INVISIBLE : View.VISIBLE);
if (!indexedInfo.isChild) {
- if (indexedInfo.mChar == NUMERIC_OR_SPECIAL_CHAR) {
+ if (indexedInfo.mStartString.equals(NUMERIC_OR_SPECIAL_HEADER)) {
holder.mTextView.setText(NUMERIC_OR_SPECIAL_HEADER);
} else {
- holder.mTextView.setText(String.valueOf(indexedInfo.mChar));
+ holder.mTextView.setText(String.valueOf(indexedInfo.mStartString));
}
}
final int size = indexedInfo.mInfo.size();
@@ -458,19 +473,35 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdap
// We just dismiss the drag when we fling, so cleanup here
}
- public class AppItemIndexedInfo {
+ public class AppItemIndexedInfo implements Comparable {
private boolean isChild;
- private char mChar;
+ private String mStartString;
+ private int mStringIndex;
private ArrayList<AppInfo> mInfo;
- private AppItemIndexedInfo(char startChar, ArrayList<AppInfo> info, boolean isChild) {
- this.mChar = startChar;
+ private AppItemIndexedInfo(String startString, int bucketIndex, ArrayList<AppInfo> info,
+ boolean isChild) {
+ this.mStartString = startString;
+ this.mStringIndex = bucketIndex;
this.mInfo = info;
this.isChild = isChild;
+
+ if (mStartString.equals(NUMERIC_OR_SPECIAL_HEADER)) {
+ this.mStringIndex = 0;
+ }
}
- public char getChar() {
- return mChar;
+ public String getString() {
+ return mStartString;
+ }
+
+ @Override
+ public int compareTo(Object o) {
+ if (o instanceof AppItemIndexedInfo) {
+ int otherBucketIndex = ((AppItemIndexedInfo) o).mStringIndex;
+ return Integer.compare(mStringIndex, otherBucketIndex);
+ }
+ return 0;
}
}
@@ -486,6 +517,6 @@ public class AppDrawerListAdapter extends RecyclerView.Adapter<AppDrawerListAdap
@Override
public int getSectionForPosition(int position) {
- return mSectionHeaders.get(mHeaderList.get(position).mChar);
+ return mSectionHeaders.get(mHeaderList.get(position).mStartString);
}
}
diff --git a/src/com/android/launcher3/locale/HanziToPinyin.java b/src/com/android/launcher3/locale/HanziToPinyin.java
new file mode 100644
index 000000000..9e398fac0
--- /dev/null
+++ b/src/com/android/launcher3/locale/HanziToPinyin.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2011 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.launcher3.locale;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+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 ICU Transliterator class. Polyphone is not supported in this
+ * implementation.
+ */
+public class HanziToPinyin {
+ private static final String TAG = "HanziToPinyin";
+
+ private static HanziToPinyin sInstance;
+ private Transliterator mPinyinTransliterator;
+ private Transliterator mAsciiTransliterator;
+
+ public static class Token {
+ /**
+ * Separator between target string for each source char
+ */
+ public static final String SEPARATOR = " ";
+
+ public static final int LATIN = 1;
+ public static final int PINYIN = 2;
+ public static final int UNKNOWN = 3;
+
+ public Token() {
+ }
+
+ public Token(int type, String source, String target) {
+ this.type = type;
+ this.source = source;
+ this.target = target;
+ }
+
+ /**
+ * Type of this token, ASCII, PINYIN or UNKNOWN.
+ */
+ public int type;
+ /**
+ * Original string before translation.
+ */
+ public String source;
+ /**
+ * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is
+ * original string in source.
+ */
+ public String target;
+ }
+
+ 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) {
+ sInstance = new HanziToPinyin();
+ }
+ return sInstance;
+ }
+ }
+
+ private void tokenize(char character, Token token) {
+ token.source = Character.toString(character);
+
+ // ASCII
+ if (character < 128) {
+ token.type = Token.LATIN;
+ 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;
+ 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;
+ }
+ }
+
+ 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> getTokens(final String input) {
+ ArrayList<Token> tokens = new ArrayList<Token>();
+ 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.isSpaceChar(character)) {
+ if (sb.length() > 0) {
+ addToken(sb, tokens, tokenType);
+ }
+ } else {
+ tokenize(character, token);
+ if (token.type == Token.PINYIN) {
+ if (sb.length() > 0) {
+ addToken(sb, tokens, tokenType);
+ }
+ tokens.add(token);
+ token = new Token();
+ } else {
+ if (tokenType != token.type && sb.length() > 0) {
+ addToken(sb, tokens, tokenType);
+ }
+ sb.append(token.target);
+ }
+ tokenType = token.type;
+ }
+ }
+ if (sb.length() > 0) {
+ addToken(sb, tokens, tokenType);
+ }
+ return tokens;
+ }
+
+ private void addToken(
+ final StringBuilder sb, final ArrayList<Token> tokens, final int tokenType) {
+ String str = sb.toString();
+ tokens.add(new Token(tokenType, str, str));
+ sb.setLength(0);
+ }
+}
diff --git a/src/com/android/launcher3/locale/LocaleSet.java b/src/com/android/launcher3/locale/LocaleSet.java
new file mode 100644
index 000000000..34634ab7e
--- /dev/null
+++ b/src/com/android/launcher3/locale/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.launcher3.locale;
+
+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/src/com/android/launcher3/locale/LocaleSetManager.java b/src/com/android/launcher3/locale/LocaleSetManager.java
new file mode 100644
index 000000000..b058718f3
--- /dev/null
+++ b/src/com/android/launcher3/locale/LocaleSetManager.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod Project
+ * Copyright (C) 2009 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.launcher3.locale;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.Locale;
+
+import libcore.icu.ICU;
+
+public class LocaleSetManager {
+ private static final String TAG = LocaleSetManager.class.getSimpleName();
+
+ private LocaleSet mCurrentLocales;
+ private final Context mContext;
+
+ public LocaleSetManager(final Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Sets up the locale set
+ * @param localeSet value to set it to
+ */
+ public void updateLocaleSet(LocaleSet localeSet) {
+ Log.d(TAG, "Locale Changed from: " + mCurrentLocales + " to " + localeSet);
+ mCurrentLocales = localeSet;
+ LocaleUtils.getInstance().setLocales(mCurrentLocales);
+ }
+
+ /**
+ * This takes an old and new locale set and creates a combined locale set. If they share a
+ * primary then the old one is returned
+ * @return the combined locale set
+ */
+ private static LocaleSet getCombinedLocaleSet(LocaleSet oldLocales, Locale newLocale) {
+ Locale prevLocale = null;
+
+ if (oldLocales != null) {
+ 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();
+ }
+
+ /**
+ * @return the system locale set
+ */
+ public LocaleSet getSystemLocaleSet() {
+ final Locale curLocale = getLocale();
+ return getCombinedLocaleSet(mCurrentLocales, curLocale);
+ }
+
+ @VisibleForTesting
+ protected Locale getLocale() {
+ return Locale.getDefault();
+ }
+}
diff --git a/src/com/android/launcher3/locale/LocaleUtils.java b/src/com/android/launcher3/locale/LocaleUtils.java
new file mode 100644
index 000000000..cc8277a6c
--- /dev/null
+++ b/src/com/android/launcher3/locale/LocaleUtils.java
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2010 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.launcher3.locale;
+
+import android.provider.ContactsContract.FullNameStyle;
+import android.provider.ContactsContract.PhoneticNameStyle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.launcher3.locale.HanziToPinyin.Token;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.lang.Character.UnicodeBlock;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Set;
+
+import libcore.icu.AlphabeticIndex;
+import libcore.icu.AlphabeticIndex.ImmutableIndex;
+import libcore.icu.Transliterator;
+
+/**
+ * This utility class provides specialized handling for locale specific
+ * information: labels, name lookup keys.
+ *
+ * This class has been modified from ContactLocaleUtils.java for now to rip out
+ * Chinese/Japanese specific Alphabetic Indexers because the MediaProvider's sort
+ * is using a Collator sort which can result in confusing behavior, so for now we will
+ * simplify and batch up those results until we later support our own internal databases
+ * An example of what This is, if we have songs "Able", "Xylophone" and "上" in
+ * simplified chinese language The media provider would give it to us in that order sorted,
+ * but the ICU lib would return "A", "X", "S". Unless we write our own db or do our own sort
+ * there is no good easy solution
+ */
+public class LocaleUtils {
+ public static final String TAG = "LauncherLocale";
+
+ 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");
+ // Serbian and Ukrainian labels are complementary supersets of Russian
+ public static final Locale LOCALE_SERBIAN = new Locale("sr");
+ public static final Locale LOCALE_UKRAINIAN = new Locale("uk");
+ public static final Locale LOCALE_THAI = new Locale("th");
+
+ /**
+ * 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], #, " "
+ */
+ private static class LocaleUtilsBase {
+ private static final String EMPTY_STRING = "";
+ private static final String NUMBER_STRING = "#";
+
+ protected final ImmutableIndex mAlphabeticIndex;
+ private final int mAlphabeticIndexBucketCount;
+ private final int mNumberBucketIndex;
+ private final boolean mEnableSecondaryLocalePinyin;
+
+ public LocaleUtilsBase(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
+ // 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 and Serbian are chosen for
+ // Cyrillic because their alphabets are complementary supersets
+ // of Russian.
+ 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)
+ .addLabels(LOCALE_ARABIC)
+ .addLabels(LOCALE_HEBREW)
+ .addLabels(LOCALE_GREEK)
+ .addLabels(LOCALE_UKRAINIAN)
+ .addLabels(LOCALE_SERBIAN)
+ .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;
+ }
+
+ /**
+ * 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;
+ }
+ 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, 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;
+ }
+ }
+
+ /**
+ * 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 static class JapaneseContactUtils extends LocaleUtilsBase {
+ // \u4ed6 is Japanese character 他 ("misc")
+ private static final String JAPANESE_MISC_LABEL = "\u4ed6";
+ private final int mMiscBucketIndex;
+
+ 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
+ // 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 int getBucketIndex(String name) {
+ final int bucketIndex = super.getBucketIndex(name);
+ if ((bucketIndex == mMiscBucketIndex &&
+ !isChineseOrJapanese(Character.codePointAt(name, 0))) ||
+ bucketIndex > mMiscBucketIndex) {
+ return bucketIndex + 1;
+ }
+ 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, 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 LocaleUtilsBase {
+ public SimplifiedChineseContactUtils(LocaleSet locales) {
+ super(locales);
+ }
+
+ @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().getTokens(name);
+ final int tokenCount = tokens.size();
+ 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 keyOriginal is used to
+ // build the lookup keys for itself.
+ 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));
+ } else if (Token.LATIN == token.type) {
+ // Avoid adding space at the end of String.
+ if (keyPinyin.length() > 0) {
+ keyPinyin.insert(0, ' ');
+ }
+ if (keyOriginal.length() > 0) {
+ keyOriginal.insert(0, ' ');
+ }
+ keyPinyin.insert(0, token.source);
+ keyInitial.insert(0, token.source.charAt(0));
+ }
+ keyOriginal.insert(0, token.source);
+ keys.add(keyOriginal.toString());
+ keys.add(keyPinyin.toString());
+ keys.add(keyInitial.toString());
+ }
+ return keys.iterator();
+ }
+ }
+
+ private static final String JAPANESE_LANGUAGE = Locale.JAPANESE.getLanguage().toLowerCase();
+ private static LocaleUtils sSingleton;
+
+ private final LocaleSet mLocales;
+ private final LocaleUtilsBase mUtils;
+
+ private LocaleUtils(LocaleSet locales) {
+ if (locales == null) {
+ mLocales = LocaleSet.getDefault();
+ } else {
+ mLocales = locales;
+ }
+ if (mLocales.isPrimaryLanguage(JAPANESE_LANGUAGE)) {
+ mUtils = new JapaneseContactUtils(mLocales);
+ } else if (mLocales.isPrimaryLocaleSimplifiedChinese()) {
+ mUtils = new SimplifiedChineseContactUtils(mLocales);
+ } else {
+ mUtils = new LocaleUtilsBase(mLocales);
+ }
+ Log.i(TAG, "AddressBook Labels [" + mLocales.toString() + "]: "
+ + getLabels().toString());
+ }
+
+ public boolean isLocale(LocaleSet locales) {
+ return mLocales.equals(locales);
+ }
+
+ public static synchronized LocaleUtils getInstance() {
+ if (sSingleton == null) {
+ sSingleton = new LocaleUtils(LocaleSet.getDefault());
+ }
+ return sSingleton;
+ }
+
+ @VisibleForTesting
+ public static synchronized void setLocale(Locale locale) {
+ setLocales(new LocaleSet(locale));
+ }
+
+ public static synchronized void setLocales(LocaleSet locales) {
+ if (sSingleton == null || !sSingleton.isLocale(locales)) {
+ sSingleton = new LocaleUtils(locales);
+ }
+ }
+
+ 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();
+ }
+}