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.text.TextUtils; 20import android.view.LayoutInflater; 21import android.view.View; 22import android.view.ViewGroup; 23import android.widget.BaseAdapter; 24import android.widget.Filter; 25import android.widget.Filterable; 26import android.widget.TextView; 27 28import com.android.internal.R; 29 30import java.util.ArrayList; 31import java.util.Collections; 32import java.util.Locale; 33import 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 */ 48public 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 textView.setText(R.string.language_picker_section_all); 149 } 150 textView.setTextLocale(Locale.getDefault()); 151 break; 152 default: 153 // Covers both null, and "reusing" a wrong kind of view 154 if (!(convertView instanceof ViewGroup)) { 155 convertView = mInflater.inflate(R.layout.language_picker_item, parent, false); 156 } 157 158 TextView text = (TextView) convertView.findViewById(R.id.locale); 159 LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position); 160 text.setText(item.getLabel(mCountryMode)); 161 text.setTextLocale(item.getLocale()); 162 text.setContentDescription(item.getContentDescription(mCountryMode)); 163 if (mCountryMode) { 164 int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent()); 165 //noinspection ResourceType 166 convertView.setLayoutDirection(layoutDir); 167 text.setTextDirection(layoutDir == View.LAYOUT_DIRECTION_RTL 168 ? View.TEXT_DIRECTION_RTL 169 : View.TEXT_DIRECTION_LTR); 170 } 171 } 172 return convertView; 173 } 174 175 private boolean showHeaders() { 176 // We don't want to show suggestions for locales with very few regions 177 // (e.g. Romanian, with 2 regions) 178 // So we put a (somewhat) arbitrary limit. 179 // 180 // The initial idea was to make that limit dependent on the screen height. 181 // But that would mean rotating the screen could make the suggestions disappear, 182 // as the number of countries that fits on the screen would be different in portrait 183 // and landscape mode. 184 if (mCountryMode && mLocaleOptions.size() < MIN_REGIONS_FOR_SUGGESTIONS) { 185 return false; 186 } 187 return mSuggestionCount != 0 && mSuggestionCount != mLocaleOptions.size(); 188 } 189 190 /** 191 * Sorts the items in the adapter using a locale-aware comparator. 192 * @param comp The locale-aware comparator to use. 193 */ 194 public void sort(LocaleHelper.LocaleInfoComparator comp) { 195 Collections.sort(mLocaleOptions, comp); 196 } 197 198 class FilterByNativeAndUiNames extends Filter { 199 200 @Override 201 protected FilterResults performFiltering(CharSequence prefix) { 202 FilterResults results = new FilterResults(); 203 204 if (mOriginalLocaleOptions == null) { 205 mOriginalLocaleOptions = new ArrayList<>(mLocaleOptions); 206 } 207 208 ArrayList<LocaleStore.LocaleInfo> values; 209 values = new ArrayList<>(mOriginalLocaleOptions); 210 if (prefix == null || prefix.length() == 0) { 211 results.values = values; 212 results.count = values.size(); 213 } else { 214 // TODO: decide if we should use the string's locale 215 Locale locale = Locale.getDefault(); 216 String prefixString = LocaleHelper.normalizeForSearch(prefix.toString(), locale); 217 218 final int count = values.size(); 219 final ArrayList<LocaleStore.LocaleInfo> newValues = new ArrayList<>(); 220 221 for (int i = 0; i < count; i++) { 222 final LocaleStore.LocaleInfo value = values.get(i); 223 final String nameToCheck = LocaleHelper.normalizeForSearch( 224 value.getFullNameInUiLanguage(), locale); 225 final String nativeNameToCheck = LocaleHelper.normalizeForSearch( 226 value.getFullNameNative(), locale); 227 if (wordMatches(nativeNameToCheck, prefixString) 228 || wordMatches(nameToCheck, prefixString)) { 229 newValues.add(value); 230 } 231 } 232 233 results.values = newValues; 234 results.count = newValues.size(); 235 } 236 237 return results; 238 } 239 240 // TODO: decide if this is enough, or we want to use a BreakIterator... 241 boolean wordMatches(String valueText, String prefixString) { 242 // First match against the whole, non-split value 243 if (valueText.startsWith(prefixString)) { 244 return true; 245 } 246 247 final String[] words = valueText.split(" "); 248 // Start at index 0, in case valueText starts with space(s) 249 for (String word : words) { 250 if (word.startsWith(prefixString)) { 251 return true; 252 } 253 } 254 255 return false; 256 } 257 258 @Override 259 protected void publishResults(CharSequence constraint, FilterResults results) { 260 mLocaleOptions = (ArrayList<LocaleStore.LocaleInfo>) results.values; 261 262 mSuggestionCount = 0; 263 for (LocaleStore.LocaleInfo li : mLocaleOptions) { 264 if (li.isSuggested()) { 265 mSuggestionCount++; 266 } 267 } 268 269 if (results.count > 0) { 270 notifyDataSetChanged(); 271 } else { 272 notifyDataSetInvalidated(); 273 } 274 } 275 } 276 277 @Override 278 public Filter getFilter() { 279 return new FilterByNativeAndUiNames(); 280 } 281} 282