diff --git a/core/java/com/android/internal/app/LocaleHelper.java b/core/java/com/android/internal/app/LocaleHelper.java
new file mode 100644
index 0000000000000..5ac786a971dd4
--- /dev/null
+++ b/core/java/com/android/internal/app/LocaleHelper.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.icu.util.ULocale;
+import android.util.LocaleList;
+
+import java.text.Collator;
+import java.util.Comparator;
+import java.util.Locale;
+
+/**
+ * This class implements some handy methods to proces with locales.
+ */
+public class LocaleHelper {
+
+ /**
+ * Sentence-case (first character uppercased).
+ *
+ *
There is no good API available for this, not even in ICU.
+ * We can revisit this if we get some ICU support later.
+ *
+ * There are currently several tickets requesting this feature:
+ *
+ * - ICU needs to provide an easy way to titlecase only one first letter
+ * http://bugs.icu-project.org/trac/ticket/11729
+ * - Add "initial case"
+ * http://bugs.icu-project.org/trac/ticket/8394
+ * - Add code for initialCase, toTitlecase don't modify after Lt,
+ * avoid 49Ers, low-level language-specific casing
+ * http://bugs.icu-project.org/trac/ticket/10410
+ * - BreakIterator.getFirstInstance: Often you need to titlecase just the first
+ * word, and leave the rest of the string alone. (closed as duplicate)
+ * http://bugs.icu-project.org/trac/ticket/8946
+ *
+ *
+ * A (clunky) option with the current ICU API is:
+ * {{
+ * BreakIterator breakIterator = BreakIterator.getSentenceInstance(locale);
+ * String result = UCharacter.toTitleCase(locale,
+ * source, breakIterator, UCharacter.TITLECASE_NO_LOWERCASE);
+ * }}
+ *
+ * That also means creating BreakIteratos for each locale. Expensive...
+ *
+ * @param str the string to sentence-case.
+ * @param locale the locale used for the case conversion.
+ * @return the string converted to sentence-case.
+ */
+ public static String toSentenceCase(String str, Locale locale) {
+ if (str.isEmpty()) {
+ return str;
+ }
+ final int firstCodePointLen = str.offsetByCodePoints(0, 1);
+ return str.substring(0, firstCodePointLen).toUpperCase(locale)
+ + str.substring(firstCodePointLen);
+ }
+
+ /**
+ * Normalizes a string for locale name search. Does case conversion for now,
+ * but might do more in the future.
+ *
+ * Warning: it is only intended to be used in searches by the locale picker.
+ * Don't use it for other things, it is very limited.
+ *
+ * @param str the string to normalize
+ * @param locale the locale that might be used for certain operations (i.e. case conversion)
+ * @return the string normalized for search
+ */
+ public static String normalizeForSearch(String str, Locale locale) {
+ // TODO: tbd if it needs to be smarter (real normalization, remove accents, etc.)
+ // If needed we might use case folding and ICU/CLDR's collation-based loose searching.
+ // TODO: decide what should the locale be, the default locale, or the locale of the string.
+ // Uppercase is better than lowercase because of things like sharp S, Greek sigma, ...
+ return str.toUpperCase();
+ }
+
+ /**
+ * Returns the locale localized for display in the provided locale.
+ *
+ * @param locale the locale whose name is to be displayed.
+ * @param displayLocale the locale in which to display the name.
+ * @param sentenceCase true if the result should be sentence-cased
+ * @return the localized name of the locale.
+ */
+ public static String getDisplayName(Locale locale, Locale displayLocale, boolean sentenceCase) {
+ String result = ULocale.getDisplayName(locale.toLanguageTag(),
+ ULocale.forLocale(displayLocale));
+ return sentenceCase ? toSentenceCase(result, displayLocale) : result;
+ }
+
+ /**
+ * Returns the locale localized for display in the default locale.
+ *
+ * @param locale the locale whose name is to be displayed.
+ * @param sentenceCase true if the result should be sentence-cased
+ * @return the localized name of the locale.
+ */
+ public static String getDisplayName(Locale locale, boolean sentenceCase) {
+ String result = ULocale.getDisplayName(locale.toLanguageTag(), ULocale.getDefault());
+ return sentenceCase ? toSentenceCase(result, Locale.getDefault()) : result;
+ }
+
+ /**
+ * Returns a locale's country localized for display in the provided locale.
+ *
+ * @param locale the locale whose country will be displayed.
+ * @param displayLocale the locale in which to display the name.
+ * @return the localized country name.
+ */
+ public static String getDisplayCountry(Locale locale, Locale displayLocale) {
+ return ULocale.getDisplayCountry(locale.toLanguageTag(), ULocale.forLocale(displayLocale));
+ }
+
+ /**
+ * Returns a locale's country localized for display in the default locale.
+ *
+ * @param locale the locale whose country will be displayed.
+ * @return the localized country name.
+ */
+ public static String getDisplayCountry(Locale locale) {
+ return ULocale.getDisplayCountry(locale.toLanguageTag(), ULocale.getDefault());
+ }
+
+ /**
+ * Returns the locale list localized for display in the provided locale.
+ *
+ * @param locales the list of locales whose names is to be displayed.
+ * @param displayLocale the locale in which to display the names.
+ * If this is null, it will use the default locale.
+ * @return the locale aware list of locale names
+ */
+ public static String getDisplayLocaleList(LocaleList locales, Locale displayLocale) {
+ final StringBuilder result = new StringBuilder();
+
+ final Locale dispLocale = displayLocale == null ? Locale.getDefault() : displayLocale;
+ int localeCount = locales.size();
+ for (int i = 0; i < localeCount; i++) {
+ Locale locale = locales.get(i);
+ result.append(LocaleHelper.getDisplayName(locale, dispLocale, false));
+ // TODO: language aware list formatter. ICU has one.
+ if (i < localeCount - 1) {
+ result.append(", ");
+ }
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Adds the likely subtags for a provided locale ID.
+ *
+ * @param locale the locale to maximize.
+ * @return the maximized Locale instance.
+ */
+ public static Locale addLikelySubtags(Locale locale) {
+ return libcore.icu.ICU.addLikelySubtags(locale);
+ }
+
+ /**
+ * Locale-sensitive comparison for LocaleInfo.
+ *
+ * It uses the label, leaving the decision on what to put there to the LocaleInfo.
+ * For instance fr-CA can be shown as "français" as a generic label in the language selection,
+ * or "français (Canada)" if it is a suggestion, or "Canada" in the country selection.
+ *
+ * Gives priority to suggested locales (to sort them at the top).
+ */
+ static final class LocaleInfoComparator implements Comparator {
+ private final Collator mCollator;
+
+ /**
+ * Constructor.
+ *
+ * @param sortLocale the locale to be used for sorting.
+ */
+ public LocaleInfoComparator(Locale sortLocale) {
+ mCollator = Collator.getInstance(sortLocale);
+ }
+
+ /**
+ * Compares its two arguments for order.
+ *
+ * @param lhs the first object to be compared
+ * @param rhs the second object to be compared
+ * @return a negative integer, zero, or a positive integer as the first
+ * argument is less than, equal to, or greater than the second.
+ */
+ @Override
+ public int compare(LocaleStore.LocaleInfo lhs, LocaleStore.LocaleInfo rhs) {
+ // We don't care about the various suggestion types, just "suggested" (!= 0)
+ // and "all others" (== 0)
+ if (lhs.isSuggested() == rhs.isSuggested()) {
+ // They are in the same "bucket" (suggested / others), so we compare the text
+ return mCollator.compare(lhs.getLabel(), rhs.getLabel());
+ } else {
+ // One locale is suggested and one is not, so we put them in different "buckets"
+ return lhs.isSuggested() ? -1 : 1;
+ }
+ }
+ }
+}
diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java
index 3b8f865bec5df..9a17883735342 100644
--- a/core/java/com/android/internal/app/LocalePickerWithRegion.java
+++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 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.
@@ -16,189 +16,158 @@
package com.android.internal.app;
-import com.android.internal.R;
-
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.content.Context;
-import android.content.res.Resources;
import android.os.Bundle;
-import android.util.ArrayMap;
-import android.view.LayoutInflater;
+import android.util.LocaleList;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
import android.widget.ListView;
-import android.widget.TextView;
+import android.widget.SearchView;
+
+import com.android.internal.R;
-import java.text.Collator;
-import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashSet;
-import java.util.List;
import java.util.Locale;
-import java.util.Map;
+import java.util.Set;
-class LocaleAdapter extends ArrayAdapter {
- final private Map mLevelOne = new ArrayMap<>();
- final private Map> mLevelTwo = new ArrayMap<>();
- final private LayoutInflater mInflater;
+/**
+ * A two-step locale picker. It shows a language, then a country.
+ *
+ * It shows suggestions at the top, then the rest of the locales.
+ * Allows the user to search for locales using both their native name and their name in the
+ * default locale.
+ */
+public class LocalePickerWithRegion extends ListFragment implements SearchView.OnQueryTextListener {
- final static class LocaleAwareComparator implements Comparator {
- private final Collator mCollator;
+ private SuggestedLocaleAdapter mAdapter;
+ private LocaleSelectedListener mListener;
+ private Set mLocaleList;
+ private LocaleStore.LocaleInfo mParentLocale;
+ private boolean mTranslatedOnly = false;
+ private boolean mCountryMode = false;
- public LocaleAwareComparator(Locale sortLocale) {
- mCollator = Collator.getInstance(sortLocale);
- }
-
- @Override
- public int compare(LocalePicker.LocaleInfo lhs, LocalePicker.LocaleInfo rhs) {
- return mCollator.compare(lhs.getLabel(), rhs.getLabel());
- }
+ /**
+ * Other classes can register to be notified when a locale was selected.
+ *
+ * This is the mechanism to "return" the result of the selection.
+ */
+ public interface LocaleSelectedListener {
+ /**
+ * The classes that want to retrieve the locale picked should implement this method.
+ * @param locale the locale picked.
+ */
+ void onLocaleSelected(LocaleStore.LocaleInfo locale);
}
- static List getCuratedLocaleList(Context context) {
- final Resources resources = context.getResources();
- final String[] supportedLocaleCodes = resources.getStringArray(R.array.supported_locales);
+ private static LocalePickerWithRegion createCountryPicker(Context context,
+ LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
+ boolean translatedOnly) {
+ LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
+ boolean shouldShowTheList = localePicker.setListener(context, listener, parent,
+ true /* country mode */, translatedOnly);
+ return shouldShowTheList ? localePicker : null;
+ }
- final ArrayList result = new ArrayList<>(supportedLocaleCodes.length);
- for (String localeId : supportedLocaleCodes) {
- Locale locale = Locale.forLanguageTag(localeId);
- if (!locale.getCountry().isEmpty()) {
- result.add(Locale.forLanguageTag(localeId));
+ public static LocalePickerWithRegion createLanguagePicker(Context context,
+ LocaleSelectedListener listener, boolean translatedOnly) {
+ LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
+ localePicker.setListener(context, listener, null,
+ false /* language mode */, translatedOnly);
+ return localePicker;
+ }
+
+ /**
+ * Sets the listener and initializes the locale list.
+ *
+ * Returns true if we need to show the list, false if not.
+ *
+ * Can return false because of an error, trying to show a list of countries,
+ * but no parent locale was provided.
+ *
+ * It can also return false if the caller tries to show the list in country mode and
+ * there is only one country available (i.e. Japanese => Japan).
+ * In this case we don't even show the list, we call the listener with that locale,
+ * "pretending" it was selected, and return false.
+ */
+ private boolean setListener(Context context, LocaleSelectedListener listener,
+ LocaleStore.LocaleInfo parent, boolean countryMode, boolean translatedOnly) {
+ if (countryMode && (parent == null || parent.getLocale() == null)) {
+ // The list of countries is determined as all the countries where the parent language
+ // is used.
+ throw new IllegalArgumentException("The country selection list needs a parent.");
+ }
+
+ this.mCountryMode = countryMode;
+ this.mParentLocale = parent;
+ this.mListener = listener;
+ this.mTranslatedOnly = translatedOnly;
+ setRetainInstance(true);
+
+ final HashSet langTagsToIgnore = new HashSet<>();
+ if (!translatedOnly) {
+ final LocaleList userLocales = LocalePicker.getLocales();
+ final String[] langTags = userLocales.toLanguageTags().split(",");
+ Collections.addAll(langTagsToIgnore, langTags);
+ }
+
+ if (countryMode) {
+ mLocaleList = LocaleStore.getLevelLocales(context,
+ langTagsToIgnore, parent, translatedOnly);
+ if (mLocaleList.size() <= 1) {
+ if (listener != null && (mLocaleList.size() == 1)) {
+ listener.onLocaleSelected(mLocaleList.iterator().next());
+ }
+ return false;
}
+ } else {
+ mLocaleList = LocaleStore.getLevelLocales(context, langTagsToIgnore,
+ null /* no parent */, translatedOnly);
}
- return result;
+
+ return true;
}
- public LocaleAdapter(Context context) {
- this(context, getCuratedLocaleList(context));
- }
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
- static Locale getBaseLocale(Locale locale) {
- return new Locale.Builder()
- .setLocale(locale)
- .setRegion("")
- .build();
- }
-
- // There is no good API available for this, not even in ICU.
- // We can revisit this if we get some ICU support later
- //
- // There are currently several tickets requesting this feature:
- // * ICU needs to provide an easy way to titlecase only one first letter
- // http://bugs.icu-project.org/trac/ticket/11729
- // * Add "initial case"
- // http://bugs.icu-project.org/trac/ticket/8394
- // * Add code for initialCase, toTitlecase don't modify after Lt,
- // avoid 49Ers, low-level language-specific casing
- // http://bugs.icu-project.org/trac/ticket/10410
- // * BreakIterator.getFirstInstance: Often you need to titlecase just the first
- // word, and leave the rest of the string alone. (closed as duplicate)
- // http://bugs.icu-project.org/trac/ticket/8946
- //
- // A (clunky) option with the current ICU API is:
- // BreakIterator breakIterator = BreakIterator.getSentenceInstance(locale);
- // String result = UCharacter.toTitleCase(locale,
- // source, breakIterator, UCharacter.TITLECASE_NO_LOWERCASE);
- // That also means creating BreakIteratos for each locale. Expensive...
- private static String toTitleCase(String s, Locale locale) {
- if (s.length() == 0) {
- return s;
- }
- final int firstCodePointLen = s.offsetByCodePoints(0, 1);
- return s.substring(0, firstCodePointLen).toUpperCase(locale)
- + s.substring(firstCodePointLen);
- }
-
- public LocaleAdapter(Context context, List locales) {
- super(context, R.layout.locale_picker_item, R.id.locale);
- mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
- for (Locale locale : locales) {
- Locale baseLocale = getBaseLocale(locale);
- String language = baseLocale.toLanguageTag();
- if (!mLevelOne.containsKey(language)) {
- String label = toTitleCase(baseLocale.getDisplayName(baseLocale), baseLocale);
- mLevelOne.put(language, new LocalePicker.LocaleInfo(label, baseLocale));
- }
-
- final HashSet subLocales;
- if (mLevelTwo.containsKey(language)) {
- subLocales = mLevelTwo.get(language);
+ Locale sortingLocale;
+ if (mCountryMode) {
+ if (mParentLocale == null) {
+ sortingLocale = Locale.getDefault();
+ this.getActivity().setTitle(R.string.country_selection_title);
} else {
- subLocales = new HashSet<>();
- mLevelTwo.put(language, subLocales);
+ sortingLocale = mParentLocale.getLocale();
+ this.getActivity().setTitle(mParentLocale.getFullNameNative());
}
- String label = locale.getDisplayCountry(locale);
- subLocales.add(new LocalePicker.LocaleInfo(label, locale));
- }
-
- setAdapterLevel(null);
- }
-
- public void setAdapterLevel(String parentLocale) {
- this.clear();
-
- if (parentLocale == null) {
- this.addAll(mLevelOne.values());
} else {
- this.addAll(mLevelTwo.get(parentLocale));
+ sortingLocale = Locale.getDefault();
+ this.getActivity().setTitle(R.string.language_selection_title);
}
- Locale sortLocale = (parentLocale == null)
- ? Locale.getDefault()
- : Locale.forLanguageTag(parentLocale);
- LocaleAwareComparator comparator = new LocaleAwareComparator(sortLocale);
- this.sort(comparator);
-
- this.notifyDataSetChanged();
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- View view;
- TextView text;
- if (convertView == null) {
- view = mInflater.inflate(R.layout.locale_picker_item, parent, false);
- text = (TextView) view.findViewById(R.id.locale);
- view.setTag(text);
- } else {
- view = convertView;
- text = (TextView) view.getTag();
- }
- LocalePicker.LocaleInfo item = getItem(position);
- text.setText(item.getLabel());
- text.setTextLocale(item.getLocale());
- return view;
- }
-}
-
-public class LocalePickerWithRegion extends ListFragment {
- private static final int LIST_MODE_LANGUAGE = 0;
- private static final int LIST_MODE_COUNTRY = 1;
-
- private LocaleAdapter mAdapter;
- private int mDisplayMode = LIST_MODE_LANGUAGE;
-
- public static interface LocaleSelectionListener {
- // You can add any argument if you really need it...
- public void onLocaleSelected(Locale locale);
- }
-
- private LocaleSelectionListener mListener = null;
-
- @Override
- public void onActivityCreated(final Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- mAdapter = new LocaleAdapter(getContext());
- mAdapter.setAdapterLevel(null);
+ mAdapter = new SuggestedLocaleAdapter(mLocaleList, mCountryMode);
+ LocaleHelper.LocaleInfoComparator comp =
+ new LocaleHelper.LocaleInfoComparator(sortingLocale);
+ mAdapter.sort(comp);
setListAdapter(mAdapter);
}
- public void setLocaleSelectionListener(LocaleSelectionListener listener) {
- mListener = listener;
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ int id = menuItem.getItemId();
+ switch (id) {
+ case android.R.id.home:
+ getFragmentManager().popBackStack();
+ return true;
+ }
+ return super.onOptionsItemSelected(menuItem);
}
@Override
@@ -207,24 +176,56 @@ public class LocalePickerWithRegion extends ListFragment {
getListView().requestFocus();
}
- /**
- * Each listener needs to call {@link LocalePicker.updateLocale(Locale)} to actually
- * change the locale.
- *
- * We don't call {@link LocalePicker.updateLocale(Locale)} automatically, as it halts
- * the system for a moment and some callers won't want it.
- */
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
- final Locale locale = ((LocalePicker.LocaleInfo) getListAdapter().getItem(position)).locale;
- // TODO: handle the back buttons to return to the language list
- if (mDisplayMode == LIST_MODE_LANGUAGE) {
- mDisplayMode = LIST_MODE_COUNTRY;
- mAdapter.setAdapterLevel(locale.toLanguageTag());
- return;
- }
- if (mListener != null) {
- mListener.onLocaleSelected(locale);
+ final LocaleStore.LocaleInfo locale =
+ (LocaleStore.LocaleInfo) getListAdapter().getItem(position);
+
+ if (mCountryMode || locale.getParent() != null) {
+ if (mListener != null) {
+ mListener.onLocaleSelected(locale);
+ }
+ getFragmentManager().popBackStack("localeListEditor",
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ } else {
+ LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
+ getContext(), mListener, locale, mTranslatedOnly /* translate only */);
+ if (selector != null) {
+ getFragmentManager().beginTransaction()
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+ .replace(getId(), selector).addToBackStack(null)
+ .commit();
+ } else {
+ getFragmentManager().popBackStack("localeListEditor",
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
}
}
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (!mCountryMode) {
+ inflater.inflate(R.menu.language_selection_list, menu);
+
+ MenuItem mSearchMenuItem = menu.findItem(R.id.locale_search_menu);
+ SearchView mSearchView = (SearchView) mSearchMenuItem.getActionView();
+
+ mSearchView.setQueryHint(getText(R.string.search_language_hint));
+ mSearchView.setOnQueryTextListener(this);
+ mSearchView.setQuery("", false /* submit */);
+ }
+ }
+
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ if (mAdapter != null) {
+ mAdapter.getFilter().filter(newText);
+ }
+ return false;
+ }
}
diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java
new file mode 100644
index 0000000000000..2191c58a82e79
--- /dev/null
+++ b/core/java/com/android/internal/app/LocaleStore.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.content.Context;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IllformedLocaleException;
+import java.util.Locale;
+import java.util.Set;
+
+public class LocaleStore {
+ private static final HashMap sLocaleCache = new HashMap<>();
+ private static boolean sFullyInitialized = false;
+
+ public static class LocaleInfo {
+ private static final int SUGGESTION_TYPE_NONE = 0x00;
+ private static final int SUGGESTION_TYPE_SIM = 0x01;
+
+ private final Locale mLocale;
+ private final Locale mParent;
+ private final String mId;
+ private boolean mIsTranslated;
+ private boolean mIsPseudo;
+ private boolean mIsChecked; // Used by the LocaleListEditor to mark entries for deletion
+ // Combination of flags for various reasons to show a locale as a suggestion.
+ // Can be SIM, location, etc.
+ private int mSuggestionFlags;
+
+ private String mFullNameNative;
+ private String mFullCountryNameNative;
+ private String mLangScriptKey;
+
+ private LocaleInfo(Locale locale) {
+ this.mLocale = locale;
+ this.mId = locale.toLanguageTag();
+ this.mParent = getParent(locale);
+ this.mIsChecked = false;
+ this.mSuggestionFlags = SUGGESTION_TYPE_NONE;
+ this.mIsTranslated = false;
+ this.mIsPseudo = false;
+ }
+
+ private LocaleInfo(String localeId) {
+ this(Locale.forLanguageTag(localeId));
+ }
+
+ private static Locale getParent(Locale locale) {
+ if (locale.getCountry().isEmpty()) {
+ return null;
+ }
+ return new Locale.Builder()
+ .setLocale(locale).setRegion("")
+ .build();
+ }
+
+ @Override
+ public String toString() {
+ return mId;
+ }
+
+ public Locale getLocale() {
+ return mLocale;
+ }
+
+ public Locale getParent() {
+ return mParent;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public boolean isTranslated() {
+ return mIsTranslated;
+ }
+
+ public void setTranslated(boolean isTranslated) {
+ mIsTranslated = isTranslated;
+ }
+
+ /* package */ boolean isSuggested() {
+ if (!mIsTranslated) { // Never suggest an untranslated locale
+ return false;
+ }
+ return mSuggestionFlags != SUGGESTION_TYPE_NONE;
+ }
+
+ private boolean isSuggestionOfType(int suggestionMask) {
+ return (mSuggestionFlags & suggestionMask) == suggestionMask;
+ }
+
+ public String getFullNameNative() {
+ if (mFullNameNative == null) {
+ mFullNameNative =
+ LocaleHelper.getDisplayName(mLocale, mLocale, true /* sentence case */);
+ }
+ return mFullNameNative;
+ }
+
+ String getFullCountryNameNative() {
+ if (mFullCountryNameNative == null) {
+ mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale);
+ }
+ return mFullCountryNameNative;
+ }
+
+ /** Returns the name of the locale in the language of the UI.
+ * It is used for search, but never shown.
+ * For instance German will show as "Deutsch" in the list, but we will also search for
+ * "allemand" if the system UI is in French.
+ */
+ public String getFullNameInUiLanguage() {
+ return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
+ }
+
+ private String getLangScriptKey() {
+ if (mLangScriptKey == null) {
+ Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(mLocale));
+ mLangScriptKey =
+ (parentWithScript == null)
+ ? mLocale.toLanguageTag()
+ : parentWithScript.toLanguageTag();
+ }
+ return mLangScriptKey;
+ }
+
+ String getLabel() {
+ if (getParent() == null || this.isSuggestionOfType(SUGGESTION_TYPE_SIM)) {
+ return getFullNameNative();
+ } else {
+ return getFullCountryNameNative();
+ }
+ }
+
+ public boolean getChecked() {
+ return mIsChecked;
+ }
+
+ public void setChecked(boolean checked) {
+ mIsChecked = checked;
+ }
+ }
+
+ private static Set getSimCountries(Context context) {
+ Set result = new HashSet<>();
+
+ TelephonyManager tm = TelephonyManager.from(context);
+
+ if (tm != null) {
+ String iso = tm.getSimCountryIso().toUpperCase(Locale.US);
+ if (!iso.isEmpty()) {
+ result.add(iso);
+ }
+
+ iso = tm.getNetworkCountryIso().toUpperCase(Locale.US);
+ if (!iso.isEmpty()) {
+ result.add(iso);
+ }
+ }
+
+ return result;
+ }
+
+ public static void fillCache(Context context) {
+ if (sFullyInitialized) {
+ return;
+ }
+
+ Set simCountries = getSimCountries(context);
+
+ for (String localeId : LocalePicker.getSupportedLocales(context)) {
+ if (localeId.isEmpty()) {
+ throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
+ }
+ LocaleInfo li = new LocaleInfo(localeId);
+ if (simCountries.contains(li.getLocale().getCountry())) {
+ li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
+ }
+ sLocaleCache.put(li.getId(), li);
+ final Locale parent = li.getParent();
+ if (parent != null) {
+ String parentId = parent.toLanguageTag();
+ if (!sLocaleCache.containsKey(parentId)) {
+ sLocaleCache.put(parentId, new LocaleInfo(parent));
+ }
+ }
+ }
+
+ boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
+ for (String localeId : LocalePicker.getPseudoLocales()) {
+ LocaleInfo li = getLocaleInfo(Locale.forLanguageTag(localeId));
+ if (isInDeveloperMode) {
+ li.setTranslated(true);
+ li.mIsPseudo = true;
+ li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
+ } else {
+ sLocaleCache.remove(li.getId());
+ }
+ }
+
+ // TODO: See if we can reuse what LocaleList.matchScore does
+ final HashSet localizedLocales = new HashSet<>();
+ for (String localeId : LocalePicker.getSystemAssetLocales()) {
+ LocaleInfo li = new LocaleInfo(localeId);
+ localizedLocales.add(li.getLangScriptKey());
+ }
+
+ for (LocaleInfo li : sLocaleCache.values()) {
+ li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
+ }
+
+ sFullyInitialized = true;
+ }
+
+ private static int getLevel(Set ignorables, LocaleInfo li, boolean translatedOnly) {
+ if (ignorables.contains(li.getId())) return 0;
+ if (li.mIsPseudo) return 2;
+ if (translatedOnly && !li.isTranslated()) return 0;
+ if (li.getParent() != null) return 2;
+ return 0;
+ }
+
+ /**
+ * Returns a list of locales for language or region selection.
+ * If the parent is null, then it is the language list.
+ * If it is not null, then the list will contain all the locales that belong to that perent.
+ * Example: if the parent is "ar", then the region list will contain all Arabic locales.
+ * (this is not language based, but language-script, so that it works for zh-Hant and so on.
+ */
+ /* package */ static Set getLevelLocales(Context context, Set ignorables,
+ LocaleInfo parent, boolean translatedOnly) {
+ fillCache(context);
+ String parentId = parent == null ? null : parent.getId();
+
+ HashSet result = new HashSet<>();
+ for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
+ int level = getLevel(ignorables, li, translatedOnly);
+ if (level == 2) {
+ if (parent != null) { // region selection
+ if (parentId.equals(li.getParent().toLanguageTag())) {
+ if (!li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
+ result.add(li);
+ }
+ }
+ } else { // language selection
+ if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
+ result.add(li);
+ } else {
+ result.add(getLocaleInfo(li.getParent()));
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ public static LocaleInfo getLocaleInfo(Locale locale) {
+ String id = locale.toLanguageTag();
+ LocaleInfo result;
+ if (!sLocaleCache.containsKey(id)) {
+ result = new LocaleInfo(locale);
+ sLocaleCache.put(id, result);
+ } else {
+ result = sLocaleCache.get(id);
+ }
+ return result;
+ }
+}
diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
new file mode 100644
index 0000000000000..2f855c6cb42f3
--- /dev/null
+++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2016 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.internal.app;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Set;
+
+
+/**
+ * This adapter wraps around a regular ListAdapter for LocaleInfo, and creates 2 sections.
+ *
+ * The first section contains "suggested" languages (usually including a region),
+ * the second section contains all the languages within the original adapter.
+ * The "others" might still include languages that appear in the "suggested" section.
+ *
+ * Example: if we show "German Switzerland" as "suggested" (based on SIM, let's say),
+ * then "German" will still show in the "others" section, clicking on it will only show the
+ * countries for all the other German locales, but not Switzerland
+ * (Austria, Belgium, Germany, Liechtenstein, Luxembourg)
+ */
+class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
+ private static final int TYPE_HEADER_SUGGESTED = 0;
+ private static final int TYPE_HEADER_ALL_OTHERS = 1;
+ private static final int TYPE_LOCALE = 2;
+
+ private ArrayList mLocaleOptions;
+ private ArrayList mOriginalLocaleOptions;
+ private int mSuggestionCount;
+ private final boolean mCountryMode;
+ private LayoutInflater mInflater;
+
+ SuggestedLocaleAdapter(Set localeOptions, boolean countryMode) {
+ mCountryMode = countryMode;
+ mLocaleOptions = new ArrayList<>(localeOptions.size());
+ for (LocaleStore.LocaleInfo li : localeOptions) {
+ if (li.isSuggested()) {
+ mSuggestionCount++;
+ }
+ mLocaleOptions.add(li);
+ }
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return getItemViewType(position) == TYPE_LOCALE;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (!showHeaders()) {
+ return TYPE_LOCALE;
+ } else {
+ if (position == 0) {
+ return TYPE_HEADER_SUGGESTED;
+ }
+ if (position == mSuggestionCount + 1) {
+ return TYPE_HEADER_ALL_OTHERS;
+ }
+ return TYPE_LOCALE;
+ }
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ if (showHeaders()) {
+ return 3; // Two headers in addition to the locales
+ } else {
+ return 1; // Locales items only
+ }
+ }
+
+ @Override
+ public int getCount() {
+ if (showHeaders()) {
+ return mLocaleOptions.size() + 2; // 2 extra for the headers
+ } else {
+ return mLocaleOptions.size();
+ }
+ }
+
+ @Override
+ public Object getItem(int position) {
+ int offset = 0;
+ if (showHeaders()) {
+ offset = position > mSuggestionCount ? -2 : -1;
+ }
+
+ return mLocaleOptions.get(position + offset);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null && mInflater == null) {
+ mInflater = LayoutInflater.from(parent.getContext());
+ }
+
+ int itemType = getItemViewType(position);
+ switch (itemType) {
+ case TYPE_HEADER_SUGGESTED: // intentional fallthrough
+ case TYPE_HEADER_ALL_OTHERS:
+ // Covers both null, and "reusing" a wrong kind of view
+ if (!(convertView instanceof TextView)) {
+ convertView = mInflater.inflate(R.layout.language_picker_section_header,
+ parent, false);
+ }
+ TextView textView = (TextView) convertView;
+ if (itemType == TYPE_HEADER_SUGGESTED) {
+ textView.setText(R.string.language_picker_section_suggested);
+ } else {
+ textView.setText(R.string.language_picker_section_all);
+ }
+ textView.setTextLocale(Locale.getDefault());
+ break;
+ default:
+ // Covers both null, and "reusing" a wrong kind of view
+ if (!(convertView instanceof ViewGroup)) {
+ convertView = mInflater.inflate(R.layout.language_picker_item, parent, false);
+ }
+
+ TextView text = (TextView) convertView.findViewById(R.id.locale);
+ ImageView localized = (ImageView) convertView.findViewById(R.id.l10nWarn);
+ LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position);
+ text.setText(item.getLabel());
+ if (item.isTranslated() || mCountryMode) {
+ localized.setVisibility(View.GONE);
+ text.setTextLocale(item.getLocale());
+ } else {
+ localized.setVisibility(View.VISIBLE);
+ text.setTextLocale(Locale.getDefault());
+ }
+ }
+ return convertView;
+ }
+
+
+ private boolean showHeaders() {
+ return mSuggestionCount != 0 && mSuggestionCount != mLocaleOptions.size();
+ }
+
+ public void sort(LocaleHelper.LocaleInfoComparator comp) {
+ Collections.sort(mLocaleOptions, comp);
+ }
+
+ class FilterByNativeAndUiNames extends Filter {
+
+ @Override
+ protected FilterResults performFiltering(CharSequence prefix) {
+ FilterResults results = new FilterResults();
+
+ if (mOriginalLocaleOptions == null) {
+ mOriginalLocaleOptions = new ArrayList<>(mLocaleOptions);
+ }
+
+ ArrayList values;
+ values = new ArrayList<>(mOriginalLocaleOptions);
+ if (prefix == null || prefix.length() == 0) {
+ results.values = values;
+ results.count = values.size();
+ } else {
+ // TODO: decide if we should use the string's locale
+ Locale locale = Locale.getDefault();
+ String prefixString = LocaleHelper.normalizeForSearch(prefix.toString(), locale);
+
+ final int count = values.size();
+ final ArrayList newValues = new ArrayList<>();
+
+ for (int i = 0; i < count; i++) {
+ final LocaleStore.LocaleInfo value = values.get(i);
+ final String nameToCheck = LocaleHelper.normalizeForSearch(
+ value.getFullNameInUiLanguage(), locale);
+ final String nativeNameToCheck = LocaleHelper.normalizeForSearch(
+ value.getFullNameNative(), locale);
+ if (wordMatches(nativeNameToCheck, prefixString)
+ || wordMatches(nameToCheck, prefixString)) {
+ newValues.add(value);
+ }
+ }
+
+ results.values = newValues;
+ results.count = newValues.size();
+ }
+
+ return results;
+ }
+
+ // TODO: decide if this is enough, or we want to use a BreakIterator...
+ boolean wordMatches(String valueText, String prefixString) {
+ // First match against the whole, non-splitted value
+ if (valueText.startsWith(prefixString)) {
+ return true;
+ }
+
+ final String[] words = valueText.split(" ");
+ // Start at index 0, in case valueText starts with space(s)
+ for (String word : words) {
+ if (word.startsWith(prefixString)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ mLocaleOptions = (ArrayList) results.values;
+
+ mSuggestionCount = 0;
+ for (LocaleStore.LocaleInfo li : mLocaleOptions) {
+ if (li.isSuggested()) {
+ mSuggestionCount++;
+ }
+ }
+
+ if (results.count > 0) {
+ notifyDataSetChanged();
+ } else {
+ notifyDataSetInvalidated();
+ }
+ }
+ }
+
+ @Override
+ public Filter getFilter() {
+ return new FilterByNativeAndUiNames();
+ }
+}
diff --git a/core/res/res/layout/language_picker_item.xml b/core/res/res/layout/language_picker_item.xml
new file mode 100644
index 0000000000000..22cb514b10b9f
--- /dev/null
+++ b/core/res/res/layout/language_picker_item.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/core/res/res/layout/language_picker_section_header.xml b/core/res/res/layout/language_picker_section_header.xml
new file mode 100644
index 0000000000000..c4d3069c19188
--- /dev/null
+++ b/core/res/res/layout/language_picker_section_header.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/core/res/res/menu/language_selection_list.xml b/core/res/res/menu/language_selection_list.xml
new file mode 100644
index 0000000000000..63b962740e75a
--- /dev/null
+++ b/core/res/res/menu/language_selection_list.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/core/res/res/values/locale_config.xml b/core/res/res/values/locale_config.xml
index 3cfd9f4be0cef..f07fe7086693d 100644
--- a/core/res/res/values/locale_config.xml
+++ b/core/res/res/values/locale_config.xml
@@ -1,21 +1,20 @@
-
-
+
+
- af-NA
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index f780df3dfcf02..258c5d9466507 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4146,4 +4146,22 @@
%1$s is trying to add a new user, but the account %2$s already exists on this device. Proceed anyway?
%1$s is trying to add a new user for the account %2$s. Proceed?
+
+
+
+
+ Language preference
+
+ Region preference
+
+ Type language name
+
+
+ Suggested
+
+ All languages
+
+
+ Search
+
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ee334be9835ba..9023dd420e62d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2473,4 +2473,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+