| /* |
| * 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. |
| * |
| * <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> |
| */ |
| 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<LocaleStore.LocaleInfo> mLocaleOptions; |
| private ArrayList<LocaleStore.LocaleInfo> 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<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; |
| } |
| |
| /** |
| * 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<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-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<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(); |
| } |
| } |