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 17package com.android.internal.app; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.content.Context; 22import android.content.res.Configuration; 23import android.text.TextUtils; 24import android.view.LayoutInflater; 25import android.view.View; 26import android.view.ViewGroup; 27import android.widget.BaseAdapter; 28import android.widget.Filter; 29import android.widget.Filterable; 30import android.widget.TextView; 31 32import com.android.internal.R; 33 34import java.util.ArrayList; 35import java.util.Collections; 36import java.util.Locale; 37import 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 */ 52public 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