/* * 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.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.Configuration; import android.text.TextUtils; 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.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)

*/ public 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 static final int MIN_REGIONS_FOR_SUGGESTIONS = 6; private ArrayList mLocaleOptions; private ArrayList mOriginalLocaleOptions; private int mSuggestionCount; private final boolean mCountryMode; private LayoutInflater mInflater; private Locale mDisplayLocale = null; // used to potentially cache a modified Context that uses mDisplayLocale private Context mContextOverride = null; public 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; } /** * Overrides the locale used to display localized labels. Setting the locale to null will reset * the Adapter to use the default locale for the labels. */ public void setDisplayLocale(@NonNull Context context, @Nullable Locale locale) { if (locale == null) { mDisplayLocale = null; mContextOverride = null; } else if (!locale.equals(mDisplayLocale)) { mDisplayLocale = locale; final Configuration configOverride = new Configuration(); configOverride.setLocale(locale); mContextOverride = context.createConfigurationContext(configOverride); } } private void setTextTo(@NonNull TextView textView, int resId) { if (mContextOverride == null) { textView.setText(resId); } else { textView.setText(mContextOverride.getText(resId)); // If mContextOverride is not null, mDisplayLocale can't be null either. } } @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) { setTextTo(textView, R.string.language_picker_section_suggested); } else { if (mCountryMode) { setTextTo(textView, R.string.region_picker_section_all); } else { setTextTo(textView, R.string.language_picker_section_all); } } textView.setTextLocale( mDisplayLocale != null ? mDisplayLocale : 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); LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position); text.setText(item.getLabel(mCountryMode)); text.setTextLocale(item.getLocale()); text.setContentDescription(item.getContentDescription(mCountryMode)); if (mCountryMode) { int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent()); //noinspection ResourceType convertView.setLayoutDirection(layoutDir); text.setTextDirection(layoutDir == View.LAYOUT_DIRECTION_RTL ? View.TEXT_DIRECTION_RTL : View.TEXT_DIRECTION_LTR); } } return convertView; } private boolean showHeaders() { // We don't want to show suggestions for locales with very few regions // (e.g. Romanian, with 2 regions) // So we put a (somewhat) arbitrary limit. // // The initial idea was to make that limit dependent on the screen height. // But that would mean rotating the screen could make the suggestions disappear, // as the number of countries that fits on the screen would be different in portrait // and landscape mode. if (mCountryMode && mLocaleOptions.size() < MIN_REGIONS_FOR_SUGGESTIONS) { return false; } return mSuggestionCount != 0 && mSuggestionCount != mLocaleOptions.size(); } /** * Sorts the items in the adapter using a locale-aware comparator. * @param comp The locale-aware comparator to use. */ 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-split 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 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(); } }