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.LinearLayout; 31 import android.widget.TextView; 32 33 import com.android.internal.R; 34 35 import java.util.ArrayList; 36 import java.util.Collections; 37 import java.util.Locale; 38 import java.util.Set; 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 protected static final int TYPE_HEADER_SUGGESTED = 0; 54 protected static final int TYPE_HEADER_ALL_OTHERS = 1; 55 protected static final int TYPE_LOCALE = 2; 56 protected static final int TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER = 3; 57 protected static final int TYPE_CURRENT_LOCALE = 4; 58 protected static final int MIN_REGIONS_FOR_SUGGESTIONS = 6; 59 protected static final int APP_LANGUAGE_PICKER_TYPE_COUNT = 5; 60 protected static final int SYSTEM_LANGUAGE_TYPE_COUNT = 3; 61 protected static final int SYSTEM_LANGUAGE_WITHOUT_HEADER_TYPE_COUNT = 1; 62 63 protected ArrayList<LocaleStore.LocaleInfo> mLocaleOptions; 64 protected ArrayList<LocaleStore.LocaleInfo> mOriginalLocaleOptions; 65 protected int mSuggestionCount; 66 protected final boolean mCountryMode; 67 protected boolean mIsNumberingMode; 68 protected LayoutInflater mInflater; 69 70 protected Locale mDisplayLocale = null; 71 // used to potentially cache a modified Context that uses mDisplayLocale 72 protected Context mContextOverride = null; 73 private boolean mHasSpecificAppPackageName; 74 SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode)75 public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode) { 76 this(localeOptions, countryMode, false); 77 } 78 SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode, boolean hasSpecificAppPackageName)79 public SuggestedLocaleAdapter(Set<LocaleStore.LocaleInfo> localeOptions, boolean countryMode, 80 boolean hasSpecificAppPackageName) { 81 mCountryMode = countryMode; 82 mLocaleOptions = new ArrayList<>(localeOptions.size()); 83 mHasSpecificAppPackageName = hasSpecificAppPackageName; 84 85 for (LocaleStore.LocaleInfo li : localeOptions) { 86 if (li.isSuggested()) { 87 mSuggestionCount++; 88 } 89 mLocaleOptions.add(li); 90 } 91 } 92 setNumberingSystemMode(boolean isNumberSystemMode)93 public void setNumberingSystemMode(boolean isNumberSystemMode) { 94 mIsNumberingMode = isNumberSystemMode; 95 } 96 getIsForNumberingSystem()97 public boolean getIsForNumberingSystem() { 98 return mIsNumberingMode; 99 } 100 101 @Override areAllItemsEnabled()102 public boolean areAllItemsEnabled() { 103 return false; 104 } 105 106 @Override isEnabled(int position)107 public boolean isEnabled(int position) { 108 return getItemViewType(position) == TYPE_LOCALE 109 || getItemViewType(position) == TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER 110 || getItemViewType(position) == TYPE_CURRENT_LOCALE; 111 } 112 113 @Override getItemViewType(int position)114 public int getItemViewType(int position) { 115 if (!showHeaders()) { 116 LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position); 117 if (item.isSystemLocale()) { 118 return TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER; 119 } 120 if (item.isAppCurrentLocale()) { 121 return TYPE_CURRENT_LOCALE; 122 } 123 return TYPE_LOCALE; 124 } else { 125 if (position == 0) { 126 return TYPE_HEADER_SUGGESTED; 127 } 128 if (position == mSuggestionCount + 1) { 129 return TYPE_HEADER_ALL_OTHERS; 130 } 131 132 LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position); 133 if (item == null) { 134 throw new NullPointerException("Non header locale cannot be null"); 135 } 136 if (item.isSystemLocale()) { 137 return TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER; 138 } 139 if (item.isAppCurrentLocale()) { 140 return TYPE_CURRENT_LOCALE; 141 } 142 return TYPE_LOCALE; 143 } 144 } 145 146 @Override getViewTypeCount()147 public int getViewTypeCount() { 148 if (mHasSpecificAppPackageName && showHeaders()) { 149 // Two headers, 1 "System language", 1 current locale 150 return APP_LANGUAGE_PICKER_TYPE_COUNT; 151 } else if (showHeaders()) { 152 // Two headers in addition to the locales 153 return SYSTEM_LANGUAGE_TYPE_COUNT; 154 } else { 155 return SYSTEM_LANGUAGE_WITHOUT_HEADER_TYPE_COUNT; // Locales items only 156 } 157 } 158 159 @Override getCount()160 public int getCount() { 161 if (showHeaders()) { 162 return mLocaleOptions.size() + 2; // 2 extra for the headers 163 } else { 164 return mLocaleOptions.size(); 165 } 166 } 167 168 @Override getItem(int position)169 public Object getItem(int position) { 170 if (isHeaderPosition(position)) { 171 return null; 172 } 173 174 int offset = 0; 175 if (showHeaders()) { 176 offset = position > mSuggestionCount ? -2 : -1; 177 } 178 179 return mLocaleOptions.get(position + offset); 180 } 181 isHeaderPosition(int position)182 private boolean isHeaderPosition(int position) { 183 return showHeaders() && (position == 0 || position == mSuggestionCount + 1); 184 } 185 186 @Override getItemId(int position)187 public long getItemId(int position) { 188 return position; 189 } 190 191 /** 192 * Overrides the locale used to display localized labels. Setting the locale to null will reset 193 * the Adapter to use the default locale for the labels. 194 */ setDisplayLocale(@onNull Context context, @Nullable Locale locale)195 public void setDisplayLocale(@NonNull Context context, @Nullable Locale locale) { 196 if (locale == null) { 197 mDisplayLocale = null; 198 mContextOverride = null; 199 } else if (!locale.equals(mDisplayLocale)) { 200 mDisplayLocale = locale; 201 final Configuration configOverride = new Configuration(); 202 configOverride.setLocale(locale); 203 mContextOverride = context.createConfigurationContext(configOverride); 204 } 205 } 206 setTextTo(@onNull TextView textView, int resId)207 protected void setTextTo(@NonNull TextView textView, int resId) { 208 if (mContextOverride == null) { 209 textView.setText(resId); 210 } else { 211 textView.setText(mContextOverride.getText(resId)); 212 // If mContextOverride is not null, mDisplayLocale can't be null either. 213 } 214 } 215 216 @Override getView(int position, View convertView, ViewGroup parent)217 public View getView(int position, View convertView, ViewGroup parent) { 218 if (convertView == null && mInflater == null) { 219 mInflater = LayoutInflater.from(parent.getContext()); 220 } 221 int itemType = getItemViewType(position); 222 View itemView = getNewViewIfNeeded(convertView, parent, itemType, position); 223 switch (itemType) { 224 case TYPE_HEADER_SUGGESTED: // intentional fallthrough 225 case TYPE_HEADER_ALL_OTHERS: 226 TextView textView = (TextView) itemView; 227 if (itemType == TYPE_HEADER_SUGGESTED) { 228 if (mCountryMode && !mIsNumberingMode) { 229 setTextTo(textView, R.string.language_picker_regions_section_suggested); 230 } else { 231 setTextTo(textView, R.string.language_picker_section_suggested); 232 } 233 } else { 234 if (mCountryMode && !mIsNumberingMode) { 235 setTextTo(textView, R.string.region_picker_section_all); 236 } else { 237 setTextTo(textView, R.string.language_picker_section_all); 238 } 239 } 240 textView.setTextLocale( 241 mDisplayLocale != null ? mDisplayLocale : Locale.getDefault()); 242 break; 243 case TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER: 244 TextView title; 245 LocaleStore.LocaleInfo info = (LocaleStore.LocaleInfo) getItem(position); 246 if (info == null) { 247 throw new NullPointerException("Non header locale cannot be null."); 248 } 249 if (info.isAppCurrentLocale()) { 250 title = itemView.findViewById(R.id.language_picker_item); 251 } else { 252 title = itemView.findViewById(R.id.locale); 253 } 254 title.setText(R.string.system_locale_title); 255 break; 256 case TYPE_CURRENT_LOCALE: 257 updateTextView(itemView, 258 itemView.findViewById(R.id.language_picker_item), position); 259 break; 260 default: 261 updateTextView(itemView, itemView.findViewById(R.id.locale), position); 262 break; 263 } 264 return itemView; 265 } 266 267 /** Check if the old view can be reused, otherwise create a new one. */ getNewViewIfNeeded( View convertView, ViewGroup parent, int itemType, int position)268 private View getNewViewIfNeeded( 269 View convertView, ViewGroup parent, int itemType, int position) { 270 View updatedView = convertView; 271 boolean shouldReuseView; 272 switch (itemType) { 273 case TYPE_HEADER_SUGGESTED: // intentional fallthrough 274 case TYPE_HEADER_ALL_OTHERS: 275 shouldReuseView = convertView instanceof TextView 276 && convertView.findViewById(R.id.language_picker_header) != null; 277 if (!shouldReuseView) { 278 updatedView = mInflater.inflate( 279 R.layout.language_picker_section_header, parent, false); 280 } 281 break; 282 case TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER: 283 if (((LocaleStore.LocaleInfo) getItem(position)).isAppCurrentLocale()) { 284 shouldReuseView = convertView instanceof LinearLayout 285 && convertView.findViewById(R.id.language_picker_item) != null; 286 if (!shouldReuseView) { 287 updatedView = mInflater.inflate( 288 R.layout.app_language_picker_current_locale_item, 289 parent, false); 290 } 291 } else { 292 shouldReuseView = convertView instanceof TextView 293 && convertView.findViewById(R.id.locale) != null; 294 if (!shouldReuseView) { 295 updatedView = mInflater.inflate( 296 R.layout.language_picker_item, parent, false); 297 } 298 } 299 break; 300 case TYPE_CURRENT_LOCALE: 301 shouldReuseView = convertView instanceof LinearLayout 302 && convertView.findViewById(R.id.language_picker_item) != null; 303 if (!shouldReuseView) { 304 updatedView = mInflater.inflate( 305 R.layout.app_language_picker_current_locale_item, parent, false); 306 } 307 break; 308 default: 309 shouldReuseView = convertView instanceof TextView 310 && convertView.findViewById(R.id.locale) != null; 311 if (!shouldReuseView) { 312 updatedView = mInflater.inflate(R.layout.language_picker_item, parent, false); 313 } 314 break; 315 } 316 return updatedView; 317 } 318 showHeaders()319 protected boolean showHeaders() { 320 // We don't want to show suggestions for locales with very few regions 321 // (e.g. Romanian, with 2 regions) 322 // So we put a (somewhat) arbitrary limit. 323 // 324 // The initial idea was to make that limit dependent on the screen height. 325 // But that would mean rotating the screen could make the suggestions disappear, 326 // as the number of countries that fits on the screen would be different in portrait 327 // and landscape mode. 328 if (mCountryMode && mLocaleOptions.size() < MIN_REGIONS_FOR_SUGGESTIONS) { 329 return false; 330 } 331 return mSuggestionCount != 0 && mSuggestionCount != mLocaleOptions.size(); 332 } 333 334 /** 335 * Sorts the items in the adapter using a locale-aware comparator. 336 * @param comp The locale-aware comparator to use. 337 */ sort(LocaleHelper.LocaleInfoComparator comp)338 public void sort(LocaleHelper.LocaleInfoComparator comp) { 339 Collections.sort(mLocaleOptions, comp); 340 } 341 342 class FilterByNativeAndUiNames extends Filter { 343 344 @Override performFiltering(CharSequence prefix)345 protected FilterResults performFiltering(CharSequence prefix) { 346 FilterResults results = new FilterResults(); 347 348 if (mOriginalLocaleOptions == null) { 349 mOriginalLocaleOptions = new ArrayList<>(mLocaleOptions); 350 } 351 352 ArrayList<LocaleStore.LocaleInfo> values; 353 values = new ArrayList<>(mOriginalLocaleOptions); 354 if (prefix == null || prefix.length() == 0) { 355 results.values = values; 356 results.count = values.size(); 357 } else { 358 // TODO: decide if we should use the string's locale 359 Locale locale = Locale.getDefault(); 360 String prefixString = LocaleHelper.normalizeForSearch(prefix.toString(), locale); 361 362 final int count = values.size(); 363 final ArrayList<LocaleStore.LocaleInfo> newValues = new ArrayList<>(); 364 365 for (int i = 0; i < count; i++) { 366 final LocaleStore.LocaleInfo value = values.get(i); 367 final String nameToCheck = LocaleHelper.normalizeForSearch( 368 value.getFullNameInUiLanguage(), locale); 369 final String nativeNameToCheck = LocaleHelper.normalizeForSearch( 370 value.getFullNameNative(), locale); 371 if (wordMatches(nativeNameToCheck, prefixString) 372 || wordMatches(nameToCheck, prefixString)) { 373 newValues.add(value); 374 } 375 } 376 377 results.values = newValues; 378 results.count = newValues.size(); 379 } 380 381 return results; 382 } 383 384 // TODO: decide if this is enough, or we want to use a BreakIterator... wordMatches(String valueText, String prefixString)385 boolean wordMatches(String valueText, String prefixString) { 386 // First match against the whole, non-split value 387 if (valueText.startsWith(prefixString)) { 388 return true; 389 } 390 391 final String[] words = valueText.split(" "); 392 // Start at index 0, in case valueText starts with space(s) 393 for (String word : words) { 394 if (word.startsWith(prefixString)) { 395 return true; 396 } 397 } 398 399 return false; 400 } 401 402 @Override publishResults(CharSequence constraint, FilterResults results)403 protected void publishResults(CharSequence constraint, FilterResults results) { 404 mLocaleOptions = (ArrayList<LocaleStore.LocaleInfo>) results.values; 405 406 mSuggestionCount = 0; 407 for (LocaleStore.LocaleInfo li : mLocaleOptions) { 408 if (li.isSuggested()) { 409 mSuggestionCount++; 410 } 411 } 412 413 if (results.count > 0) { 414 notifyDataSetChanged(); 415 } else { 416 notifyDataSetInvalidated(); 417 } 418 } 419 } 420 421 @Override getFilter()422 public Filter getFilter() { 423 return new FilterByNativeAndUiNames(); 424 } 425 updateTextView(View convertView, TextView text, int position)426 private void updateTextView(View convertView, TextView text, int position) { 427 LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position); 428 text.setText(mIsNumberingMode 429 ? item.getNumberingSystem() : item.getLabel(mCountryMode)); 430 text.setTextLocale(item.getLocale()); 431 text.setContentDescription(mIsNumberingMode 432 ? item.getNumberingSystem() : item.getContentDescription(mCountryMode)); 433 if (mCountryMode) { 434 int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent()); 435 //noinspection ResourceType 436 convertView.setLayoutDirection(layoutDir); 437 text.setTextDirection(layoutDir == View.LAYOUT_DIRECTION_RTL 438 ? View.TEXT_DIRECTION_RTL 439 : View.TEXT_DIRECTION_LTR); 440 } 441 } 442 } 443