blob: c012263a09c941307af9d69b717bcb4ccc845d5b [file] [log] [blame]
/*
* 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.localepicker;
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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.localepicker.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>
*
* <p>This class implements {@link Filterable}; apps using this class can use
* {@code getFilter().filter(...)} to filter on the available list of locales.</p>
*/
public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
@VisibleForTesting static final int TYPE_HEADER_SUGGESTED = 0;
@VisibleForTesting static final int TYPE_HEADER_ALL_OTHERS = 1;
@VisibleForTesting static final int TYPE_LOCALE = 2;
@VisibleForTesting 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 LocaleStore.LocaleInfo getItem(int position) {
if (getItemViewType(position) != TYPE_LOCALE) {
return null;
}
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();
}
}