Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.internal.app;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.content.Context;
     22 import android.content.res.Configuration;
     23 import android.text.TextUtils;
     24 import android.view.LayoutInflater;
     25 import android.view.View;
     26 import android.view.ViewGroup;
     27 import android.widget.BaseAdapter;
     28 import android.widget.Filter;
     29 import android.widget.Filterable;
     30 import android.widget.TextView;
     31 
     32 import com.android.internal.R;
     33 
     34 import java.util.ArrayList;
     35 import java.util.Collections;
     36 import java.util.Locale;
     37 import java.util.Set;
     38 
     39 
     40 /**
     41  * This adapter wraps around a regular ListAdapter for LocaleInfo, and creates 2 sections.
     42  *
     43  * <p>The first section contains "suggested" languages (usually including a region),
     44  * the second section contains all the languages within the original adapter.
     45  * The "others" might still include languages that appear in the "suggested" section.</p>
     46  *
     47  * <p>Example: if we show "German Switzerland" as "suggested" (based on SIM, let's say),
     48  * then "German" will still show in the "others" section, clicking on it will only show the
     49  * countries for all the other German locales, but not Switzerland
     50  * (Austria, Belgium, Germany, Liechtenstein, Luxembourg)</p>
     51  */
     52 public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable {
     53     private static final int TYPE_HEADER_SUGGESTED = 0;
     54     private static final int TYPE_HEADER_ALL_OTHERS = 1;
     55     private static final int TYPE_LOCALE = 2;
     56     private static final int MIN_REGIONS_FOR_SUGGESTIONS = 6;
     57 
     58     private ArrayList<LocaleStore.LocaleInfo> mLocaleOptions;
     59     private ArrayList<LocaleStore.LocaleInfo> mOriginalLocaleOptions;
     60     private int mSuggestionCount;
     61     private final boolean mCountryMode;
     62     private LayoutInflater mInflater;
     63 
     64     private Locale mDisplayLocale = null;
     65     // used to potentially cache a modified Context that uses mDisplayLocale
     66     private Context mContextOverride = null;
     67 
     68     public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode) {
     69         mCountryMode = countryMode;
     70         mLocaleOptions = new ArrayList<>(localeOptions.size());
     71         for (LocaleStore.LocaleInfo li : localeOptions) {
     72             if (li.isSuggested()) {
     73                 mSuggestionCount++;
     74             }
     75             mLocaleOptions.add(li);
     76         }
     77     }
     78 
     79     @Override
     80     public boolean areAllItemsEnabled() {
     81         return false;
     82     }
     83 
     84     @Override
     85     public boolean isEnabled(int position) {
     86         return getItemViewType(position) == TYPE_LOCALE;
     87     }
     88 
     89     @Override
     90     public int getItemViewType(int position) {
     91         if (!showHeaders()) {
     92             return TYPE_LOCALE;
     93         } else {
     94             if (position == 0) {
     95                 return TYPE_HEADER_SUGGESTED;
     96             }
     97             if (position == mSuggestionCount + 1) {
     98                 return TYPE_HEADER_ALL_OTHERS;
     99             }
    100             return TYPE_LOCALE;
    101         }
    102     }
    103 
    104     @Override
    105     public int getViewTypeCount() {
    106         if (showHeaders()) {
    107             return 3; // Two headers in addition to the locales
    108         } else {
    109             return 1; // Locales items only
    110         }
    111     }
    112 
    113     @Override
    114     public int getCount() {
    115         if (showHeaders()) {
    116             return mLocaleOptions.size() + 2; // 2 extra for the headers
    117         } else {
    118             return mLocaleOptions.size();
    119         }
    120     }
    121 
    122     @Override
    123     public Object getItem(int position) {
    124         int offset = 0;
    125         if (showHeaders()) {
    126             offset = position > mSuggestionCount ? -2 : -1;
    127         }
    128 
    129         return mLocaleOptions.get(position + offset);
    130     }
    131 
    132     @Override
    133     public long getItemId(int position) {
    134         return position;
    135     }
    136 
    137     /**
    138      * Overrides the locale used to display localized labels. Setting the locale to null will reset
    139      * the Adapter to use the default locale for the labels.
    140      */
    141     public void setDisplayLocale(@NonNull Context context, @Nullable Locale locale) {
    142         if (locale == null) {
    143             mDisplayLocale = null;
    144             mContextOverride = null;
    145         } else if (!locale.equals(mDisplayLocale)) {
    146             mDisplayLocale = locale;
    147             final Configuration configOverride = new Configuration();
    148             configOverride.setLocale(locale);
    149             mContextOverride = context.createConfigurationContext(configOverride);
    150         }
    151     }
    152 
    153     private void setTextTo(@NonNull TextView textView, int resId) {
    154         if (mContextOverride == null) {
    155             textView.setText(resId);
    156         } else {
    157             textView.setText(mContextOverride.getText(resId));
    158             // If mContextOverride is not null, mDisplayLocale can't be null either.
    159         }
    160     }
    161 
    162     @Override
    163     public View getView(int position, View convertView, ViewGroup parent) {
    164         if (convertView == null && mInflater == null) {
    165             mInflater = LayoutInflater.from(parent.getContext());
    166         }
    167 
    168         int itemType = getItemViewType(position);
    169         switch (itemType) {
    170             case TYPE_HEADER_SUGGESTED: // intentional fallthrough
    171             case TYPE_HEADER_ALL_OTHERS:
    172                 // Covers both null, and "reusing" a wrong kind of view
    173                 if (!(convertView instanceof TextView)) {
    174                     convertView = mInflater.inflate(R.layout.language_picker_section_header,
    175                             parent, false);
    176                 }
    177                 TextView textView = (TextView) convertView;
    178                 if (itemType == TYPE_HEADER_SUGGESTED) {
    179                     setTextTo(textView, R.string.language_picker_section_suggested);
    180                 } else {
    181                     if (mCountryMode) {
    182                         setTextTo(textView, R.string.region_picker_section_all);
    183                     } else {
    184                         setTextTo(textView, R.string.language_picker_section_all);
    185                     }
    186                 }
    187                 textView.setTextLocale(
    188                         mDisplayLocale != null ? mDisplayLocale : Locale.getDefault());
    189                 break;
    190             default:
    191                 // Covers both null, and "reusing" a wrong kind of view
    192                 if (!(convertView instanceof ViewGroup)) {
    193                     convertView = mInflater.inflate(R.layout.language_picker_item, parent, false);
    194                 }
    195 
    196                 TextView text = (TextView) convertView.findViewById(R.id.locale);
    197                 LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position);
    198                 text.setText(item.getLabel(mCountryMode));
    199                 text.setTextLocale(item.getLocale());
    200                 text.setContentDescription(item.getContentDescription(mCountryMode));
    201                 if (mCountryMode) {
    202                     int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent());
    203                     //noinspection ResourceType
    204                     convertView.setLayoutDirection(layoutDir);
    205                     text.setTextDirection(layoutDir == View.LAYOUT_DIRECTION_RTL
    206                             ? View.TEXT_DIRECTION_RTL
    207                             : View.TEXT_DIRECTION_LTR);
    208                 }
    209         }
    210         return convertView;
    211     }
    212 
    213     private boolean showHeaders() {
    214         // We don't want to show suggestions for locales with very few regions
    215         // (e.g. Romanian, with 2 regions)
    216         // So we put a (somewhat) arbitrary limit.
    217         //
    218         // The initial idea was to make that limit dependent on the screen height.
    219         // But that would mean rotating the screen could make the suggestions disappear,
    220         // as the number of countries that fits on the screen would be different in portrait
    221         // and landscape mode.
    222         if (mCountryMode && mLocaleOptions.size() < MIN_REGIONS_FOR_SUGGESTIONS) {
    223             return false;
    224         }
    225         return mSuggestionCount != 0 && mSuggestionCount != mLocaleOptions.size();
    226     }
    227 
    228     /**
    229      * Sorts the items in the adapter using a locale-aware comparator.
    230      * @param comp The locale-aware comparator to use.
    231      */
    232     public void sort(LocaleHelper.LocaleInfoComparator comp) {
    233         Collections.sort(mLocaleOptions, comp);
    234     }
    235 
    236     class FilterByNativeAndUiNames extends Filter {
    237 
    238         @Override
    239         protected FilterResults performFiltering(CharSequence prefix) {
    240             FilterResults results = new FilterResults();
    241 
    242             if (mOriginalLocaleOptions == null) {
    243                 mOriginalLocaleOptions = new ArrayList<>(mLocaleOptions);
    244             }
    245 
    246             ArrayList<LocaleStore.LocaleInfo> values;
    247             values = new ArrayList<>(mOriginalLocaleOptions);
    248             if (prefix == null || prefix.length() == 0) {
    249                 results.values = values;
    250                 results.count = values.size();
    251             } else {
    252                 // TODO: decide if we should use the string's locale
    253                 Locale locale = Locale.getDefault();
    254                 String prefixString = LocaleHelper.normalizeForSearch(prefix.toString(), locale);
    255 
    256                 final int count = values.size();
    257                 final ArrayList<LocaleStore.LocaleInfo> newValues = new ArrayList<>();
    258 
    259                 for (int i = 0; i < count; i++) {
    260                     final LocaleStore.LocaleInfo value = values.get(i);
    261                     final String nameToCheck = LocaleHelper.normalizeForSearch(
    262                             value.getFullNameInUiLanguage(), locale);
    263                     final String nativeNameToCheck = LocaleHelper.normalizeForSearch(
    264                             value.getFullNameNative(), locale);
    265                     if (wordMatches(nativeNameToCheck, prefixString)
    266                             || wordMatches(nameToCheck, prefixString)) {
    267                         newValues.add(value);
    268                     }
    269                 }
    270 
    271                 results.values = newValues;
    272                 results.count = newValues.size();
    273             }
    274 
    275             return results;
    276         }
    277 
    278         // TODO: decide if this is enough, or we want to use a BreakIterator...
    279         boolean wordMatches(String valueText, String prefixString) {
    280             // First match against the whole, non-split value
    281             if (valueText.startsWith(prefixString)) {
    282                 return true;
    283             }
    284 
    285             final String[] words = valueText.split(" ");
    286             // Start at index 0, in case valueText starts with space(s)
    287             for (String word : words) {
    288                 if (word.startsWith(prefixString)) {
    289                     return true;
    290                 }
    291             }
    292 
    293             return false;
    294         }
    295 
    296         @Override
    297         protected void publishResults(CharSequence constraint, FilterResults results) {
    298             mLocaleOptions = (ArrayList<LocaleStore.LocaleInfo>) results.values;
    299 
    300             mSuggestionCount = 0;
    301             for (LocaleStore.LocaleInfo li : mLocaleOptions) {
    302                 if (li.isSuggested()) {
    303                     mSuggestionCount++;
    304                 }
    305             }
    306 
    307             if (results.count > 0) {
    308                 notifyDataSetChanged();
    309             } else {
    310                 notifyDataSetInvalidated();
    311             }
    312         }
    313     }
    314 
    315     @Override
    316     public Filter getFilter() {
    317         return new FilterByNativeAndUiNames();
    318     }
    319 }
    320