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