Re-implements the locales selection with suggestions and search.

This replaces the initial implementation of a two-step locale selection
with a more advanced version, which does suggestions, search, removes
locales that already exist in the user preferences.

Bug: 25800339
Bug: 26414919
Bug: 26278049
Bug: 26275094
Bug: 26266914
Bug: 26266743
Bug: 26266712
Bug: 26266605
Bug: 26266490
Bug: 26266021

Change-Id: I88944c86e4cae5eaa00b7ae4855887ab11989253
This commit is contained in:
Mihai Nita
2016-01-12 08:53:54 -08:00
parent 770e437d30
commit 1808ff7cde
10 changed files with 1087 additions and 188 deletions

View File

@@ -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).
*
* <p>There is no good API available for this, not even in ICU.
* We can revisit this if we get some ICU support later.</p>
*
* <p>There are currently several tickets requesting this feature:</p>
* <ul>
* <li>ICU needs to provide an easy way to titlecase only one first letter
* http://bugs.icu-project.org/trac/ticket/11729</li>
* <li>Add "initial case"
* http://bugs.icu-project.org/trac/ticket/8394</li>
* <li>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</li>
* <li>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</li>
* </ul>
*
* <p>A (clunky) option with the current ICU API is:</p>
* {{
* BreakIterator breakIterator = BreakIterator.getSentenceInstance(locale);
* String result = UCharacter.toTitleCase(locale,
* source, breakIterator, UCharacter.TITLECASE_NO_LOWERCASE);
* }}
*
* <p>That also means creating BreakIteratos for each locale. Expensive...</p>
*
* @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.
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* <p>Gives priority to suggested locales (to sort them at the top).</p>
*/
static final class LocaleInfoComparator implements Comparator<LocaleStore.LocaleInfo> {
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;
}
}
}
}

View File

@@ -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<LocalePicker.LocaleInfo> {
final private Map<String, LocalePicker.LocaleInfo> mLevelOne = new ArrayMap<>();
final private Map<String, HashSet<LocalePicker.LocaleInfo>> mLevelTwo = new ArrayMap<>();
final private LayoutInflater mInflater;
/**
* A two-step locale picker. It shows a language, then a country.
*
* <p>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.</p>
*/
public class LocalePickerWithRegion extends ListFragment implements SearchView.OnQueryTextListener {
final static class LocaleAwareComparator implements Comparator<LocalePicker.LocaleInfo> {
private final Collator mCollator;
private SuggestedLocaleAdapter mAdapter;
private LocaleSelectedListener mListener;
private Set<LocaleStore.LocaleInfo> 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.
*
* <p>This is the mechanism to "return" the result of the selection.</p>
*/
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<Locale> 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<Locale> 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.
*
* <p>Returns true if we need to show the list, false if not.</p>
*
* <p>Can return false because of an error, trying to show a list of countries,
* but no parent locale was provided.</p>
*
* <p>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.</p>
*/
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<String> 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<Locale> 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<LocalePicker.LocaleInfo> 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.
* <p/>
* 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;
}
}

View File

@@ -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<String, LocaleInfo> 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<String> getSimCountries(Context context) {
Set<String> 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<String> 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<String> 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<String> 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<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
LocaleInfo parent, boolean translatedOnly) {
fillCache(context);
String parentId = parent == null ? null : parent.getId();
HashSet<LocaleInfo> 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;
}
}

View File

@@ -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.
*
* <p>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.</p>
*
* <p>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)</p>
*/
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<LocaleStore.LocaleInfo> mLocaleOptions;
private ArrayList<LocaleStore.LocaleInfo> mOriginalLocaleOptions;
private int mSuggestionCount;
private final boolean mCountryMode;
private LayoutInflater mInflater;
SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> 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<LocaleStore.LocaleInfo> 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<LocaleStore.LocaleInfo> 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<LocaleStore.LocaleInfo>) 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();
}
}

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layoutDirection="locale"
android:textDirection="locale"
android:paddingBottom="8dp"
android:paddingTop="8dp">
<TextView
android:id="@+id/locale"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="France"
android:layout_weight="1"
android:padding="12dp"
android:paddingStart="18dp"
android:paddingEnd="18dp"
android:textAppearance="?android:attr/textAppearanceListItem"/>
<ImageView
android:id="@+id/l10nWarn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/stat_sys_warning"
android:focusableInTouchMode="false"
android:focusable="false"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:tint="?android:attr/colorAccent"/>
</LinearLayout>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="?android:attr/preferenceCategoryStyle"
android:layout_width="match_parent"
android:layout_height="36dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="?android:attr/colorAccent"
tools:text="@string/language_picker_section_all"/>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/locale_search_menu"
android:title="@string/locale_search_menu"
app:showAsAction="always"
app:actionViewClass="android.widget.SearchView" />
</menu>

View File

@@ -1,21 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
** Copyright 2015 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.
*/
<!-- Copyright (C) 2015 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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<resources>
<string-array translatable="false" name="supported_locales">
<item>af-NA</item> <!-- Afrikaans (Namibia) -->

View File

@@ -4146,4 +4146,22 @@
<string name="user_creation_account_exists"><b><xliff:g id="app" example="Gmail">%1$s</xliff:g></b> is trying to add a new user, but the account <b><xliff:g id="account" example="foobar">%2$s</xliff:g></b> already exists on this device. Proceed anyway?</string>
<!-- Message to user that app is trying to create user for a specified account. [CHAR LIMIT=none] -->
<string name="user_creation_adding"><b><xliff:g id="app" example="Gmail">%1$s</xliff:g></b> is trying to add a new user for the account <b><xliff:g id="account" example="foobar">%2$s</xliff:g></b>. Proceed?</string>
<!-- Locale picker strings -->
<!-- Title for the language selection screen [CHAR LIMIT=25] -->
<string name="language_selection_title">Language preference</string>
<!-- Title for the region selection screen [CHAR LIMIT=25] -->
<string name="country_selection_title">Region preference</string>
<!-- Hint text in a search edit box (used to filter long language / country lists) [CHAR LIMIT=20] -->
<string name="search_language_hint">Type language name</string>
<!-- List section subheader for the language picker, containing a list of suggested languages determined by the default region [CHAR LIMIT=30] -->
<string name="language_picker_section_suggested">Suggested</string>
<!-- List section subheader for the language picker, containing a list of all languages available [CHAR LIMIT=30] -->
<string name="language_picker_section_all">All languages</string>
<!-- Menu item in the locale menu [CHAR LIMIT=30] -->
<string name="locale_search_menu">Search</string>
</resources>

View File

@@ -2473,4 +2473,17 @@
<java-symbol type="string" name="status_bar_alarm_clock" />
<java-symbol type="string" name="status_bar_secure" />
<java-symbol type="string" name="status_bar_clock" />
<!-- Locale picker -->
<java-symbol type="id" name="l10nWarn" />
<java-symbol type="id" name="locale_search_menu" />
<java-symbol type="layout" name="language_picker_item" />
<java-symbol type="layout" name="language_picker_section_header" />
<java-symbol type="menu" name="language_selection_list" />
<java-symbol type="string" name="country_selection_title" />
<java-symbol type="string" name="language_picker_section_all" />
<java-symbol type="string" name="language_picker_section_suggested" />
<java-symbol type="string" name="language_selection_title" />
<java-symbol type="string" name="search_language_hint" />
</resources>