TimeZoneFilterTypeAdapter.java revision 020b0c4329f432946df13a43a2831be0ee3b32cd
1/* 2 * Copyright (C) 2013 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.timezonepicker; 18 19import android.content.Context; 20import android.text.TextUtils; 21import android.util.Log; 22import android.view.LayoutInflater; 23import android.view.View; 24import android.view.View.OnClickListener; 25import android.view.ViewGroup; 26import android.widget.BaseAdapter; 27import android.widget.Filter; 28import android.widget.Filterable; 29import android.widget.TextView; 30 31import java.util.ArrayList; 32 33public class TimeZoneFilterTypeAdapter extends BaseAdapter implements Filterable, OnClickListener { 34 public static final String TAG = "TimeZoneFilterTypeAdapter"; 35 36 public static final int FILTER_TYPE_EMPTY = -1; 37 public static final int FILTER_TYPE_NONE = 0; 38 public static final int FILTER_TYPE_COUNTRY = 1; 39 public static final int FILTER_TYPE_STATE = 2; 40 public static final int FILTER_TYPE_GMT = 3; 41 42 public interface OnSetFilterListener { 43 void onSetFilter(int filterType, String str, int time); 44 } 45 46 static class ViewHolder { 47 int filterType; 48 String str; 49 int time; 50 TextView strTextView; 51 52 static void setupViewHolder(View v) { 53 ViewHolder vh = new ViewHolder(); 54 vh.strTextView = (TextView) v.findViewById(R.id.value); 55 v.setTag(vh); 56 } 57 } 58 59 class FilterTypeResult { 60 int type; 61 String constraint; 62 public int time; 63 64 public FilterTypeResult(int type, String constraint, int time) { 65 this.type = type; 66 this.constraint = constraint; 67 this.time = time; 68 } 69 70 @Override 71 public String toString() { 72 return constraint; 73 } 74 } 75 76 private ArrayList<FilterTypeResult> mLiveResults = new ArrayList<FilterTypeResult>(); 77 private int mLiveResultsCount = 0; 78 79 private ArrayFilter mFilter; 80 81 private LayoutInflater mInflater; 82 83 private TimeZoneData mTimeZoneData; 84 private OnSetFilterListener mListener; 85 86 public TimeZoneFilterTypeAdapter(Context context, TimeZoneData tzd, OnSetFilterListener l) { 87 mTimeZoneData = tzd; 88 mListener = l; 89 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 90 } 91 92 @Override 93 public int getCount() { 94 return mLiveResultsCount; 95 } 96 97 @Override 98 public FilterTypeResult getItem(int position) { 99 return mLiveResults.get(position); 100 } 101 102 @Override 103 public long getItemId(int position) { 104 return position; 105 } 106 107 @Override 108 public View getView(int position, View convertView, ViewGroup parent) { 109 View v; 110 111 if (convertView != null) { 112 v = convertView; 113 } else { 114 v = mInflater.inflate(R.layout.time_zone_filter_item, null); 115 ViewHolder.setupViewHolder(v); 116 } 117 118 ViewHolder vh = (ViewHolder) v.getTag(); 119 120 if (position >= mLiveResults.size()) { 121 Log.e(TAG, "getView: " + position + " of " + mLiveResults.size()); 122 } 123 124 FilterTypeResult filter = mLiveResults.get(position); 125 126 vh.filterType = filter.type; 127 vh.str = filter.constraint; 128 vh.time = filter.time; 129 vh.strTextView.setText(filter.constraint); 130 return v; 131 } 132 133 OnClickListener mDummyListener = new OnClickListener() { 134 135 @Override 136 public void onClick(View v) { 137 } 138 }; 139 140 // Implements OnClickListener 141 142 // This onClickListener is actually called from the AutoCompleteTextView's 143 // onItemClickListener. Trying to update the text in AutoCompleteTextView 144 // is causing an infinite loop. 145 @Override 146 public void onClick(View v) { 147 if (mListener != null && v != null) { 148 ViewHolder vh = (ViewHolder) v.getTag(); 149 mListener.onSetFilter(vh.filterType, vh.str, vh.time); 150 } 151 notifyDataSetInvalidated(); 152 } 153 154 // Implements Filterable 155 @Override 156 public Filter getFilter() { 157 if (mFilter == null) { 158 mFilter = new ArrayFilter(); 159 } 160 return mFilter; 161 } 162 163 private class ArrayFilter extends Filter { 164 @Override 165 protected FilterResults performFiltering(CharSequence prefix) { 166 Log.e(TAG, "performFiltering >>>> [" + prefix + "]"); 167 168 FilterResults results = new FilterResults(); 169 String prefixString = null; 170 if (prefix != null) { 171 prefixString = prefix.toString().trim().toLowerCase(); 172 } 173 174 if (TextUtils.isEmpty(prefixString)) { 175 results.values = null; 176 results.count = 0; 177 return results; 178 } 179 180 // TODO Perf - we can loop through the filtered list if the new 181 // search string starts with the old search string 182 ArrayList<FilterTypeResult> filtered = new ArrayList<FilterTypeResult>(); 183 184 // //////////////////////////////////////// 185 // Search by local time and GMT offset 186 // //////////////////////////////////////// 187 boolean gmtOnly = false; 188 int startParsePosition = 0; 189 if (prefixString.charAt(0) == '+' || prefixString.charAt(0) == '-') { 190 gmtOnly = true; 191 } 192 193 if (prefixString.startsWith("gmt")) { 194 startParsePosition = 3; 195 gmtOnly = true; 196 } 197 198 int num = parseNum(prefixString, startParsePosition); 199 if (num != Integer.MIN_VALUE) { 200 boolean positiveOnly = prefixString.length() > startParsePosition 201 && prefixString.charAt(startParsePosition) == '+'; 202 handleSearchByGmt(filtered, num, positiveOnly); 203 } 204 205 // //////////////////////////////////////// 206 // Search by country 207 // //////////////////////////////////////// 208 for (String country : mTimeZoneData.mTimeZonesByCountry.keySet()) { 209 // TODO Perf - cache toLowerCase()? 210 if (!TextUtils.isEmpty(country)) { 211 final String lowerCaseCountry = country.toLowerCase(); 212 boolean isMatch = false; 213 if (lowerCaseCountry.startsWith(prefixString) 214 || (lowerCaseCountry.charAt(0) == prefixString.charAt(0) && 215 isStartingInitialsFor(prefixString, lowerCaseCountry))) { 216 isMatch = true; 217 } else if (lowerCaseCountry.contains(" ")){ 218 // We should also search other words in the country name, so that 219 // searches like "Korea" yield "South Korea". 220 for (String word : lowerCaseCountry.split(" ")) { 221 if (word.startsWith(prefixString)) { 222 isMatch = true; 223 break; 224 } 225 } 226 } 227 if (isMatch) { 228 filtered.add(new FilterTypeResult(FILTER_TYPE_COUNTRY, country, 0)); 229 } 230 } 231 } 232 233 // //////////////////////////////////////// 234 // TODO Search by state 235 // //////////////////////////////////////// 236 Log.e(TAG, "performFiltering <<<< " + filtered.size() + "[" + prefix + "]"); 237 238 results.values = filtered; 239 results.count = filtered.size(); 240 return results; 241 } 242 243 /** 244 * Returns true if the prefixString is an initial for string. Note that 245 * this method will return true even if prefixString does not cover all 246 * the words. Words are separated by non-letters which includes spaces 247 * and symbols). 248 * 249 * For example: 250 * isStartingInitialsFor("UA", "United Arb Emirates") would return true 251 * isStartingInitialsFor("US", "U.S. Virgin Island") would return true 252 253 * @param prefixString 254 * @param string 255 * @return 256 */ 257 private boolean isStartingInitialsFor(String prefixString, String string) { 258 final int initialLen = prefixString.length(); 259 final int strLen = string.length(); 260 261 int initialIdx = 0; 262 boolean wasWordBreak = true; 263 for (int i = 0; i < strLen; i++) { 264 if (!Character.isLetter(string.charAt(i))) { 265 wasWordBreak = true; 266 continue; 267 } 268 269 if (wasWordBreak) { 270 if (prefixString.charAt(initialIdx++) != string.charAt(i)) { 271 return false; 272 } 273 if (initialIdx == initialLen) { 274 return true; 275 } 276 wasWordBreak = false; 277 } 278 } 279 return false; 280 } 281 282 private void handleSearchByGmt(ArrayList<FilterTypeResult> filtered, int num, 283 boolean positiveOnly) { 284 285 FilterTypeResult r; 286 if (num >= 0) { 287 if (num == 1) { 288 for (int i = 19; i >= 10; i--) { 289 if (mTimeZoneData.hasTimeZonesInHrOffset(i)) { 290 r = new FilterTypeResult(FILTER_TYPE_GMT, "GMT+" + i, i); 291 filtered.add(r); 292 } 293 } 294 } 295 296 if (mTimeZoneData.hasTimeZonesInHrOffset(num)) { 297 r = new FilterTypeResult(FILTER_TYPE_GMT, "GMT+" + num, num); 298 filtered.add(r); 299 } 300 num *= -1; 301 } 302 303 if (!positiveOnly && num != 0) { 304 if (mTimeZoneData.hasTimeZonesInHrOffset(num)) { 305 r = new FilterTypeResult(FILTER_TYPE_GMT, "GMT" + num, num); 306 filtered.add(r); 307 } 308 309 if (num == -1) { 310 for (int i = -10; i >= -19; i--) { 311 if (mTimeZoneData.hasTimeZonesInHrOffset(i)) { 312 r = new FilterTypeResult(FILTER_TYPE_GMT, "GMT" + i, i); 313 filtered.add(r); 314 } 315 } 316 } 317 } 318 } 319 320 /** 321 * Acceptable strings are in the following format: [+-]?[0-9]?[0-9] 322 * 323 * @param str 324 * @param startIndex 325 * @return Integer.MIN_VALUE as invalid 326 */ 327 public int parseNum(String str, int startIndex) { 328 int idx = startIndex; 329 int num = Integer.MIN_VALUE; 330 int negativeMultiplier = 1; 331 332 // First char - check for + and - 333 char ch = str.charAt(idx++); 334 switch (ch) { 335 case '-': 336 negativeMultiplier = -1; 337 // fall through 338 case '+': 339 if (idx >= str.length()) { 340 // No more digits 341 return Integer.MIN_VALUE; 342 } 343 344 ch = str.charAt(idx++); 345 break; 346 } 347 348 if (!Character.isDigit(ch)) { 349 // No digit 350 return Integer.MIN_VALUE; 351 } 352 353 // Got first digit 354 num = Character.digit(ch, 10); 355 356 // Check next char 357 if (idx < str.length()) { 358 ch = str.charAt(idx++); 359 if (Character.isDigit(ch)) { 360 // Got second digit 361 num = 10 * num + Character.digit(ch, 10); 362 } else { 363 return Integer.MIN_VALUE; 364 } 365 } 366 367 if (idx != str.length()) { 368 // Invalid 369 return Integer.MIN_VALUE; 370 } 371 372 Log.e(TAG, "Parsing " + str + " -> " + negativeMultiplier * num); 373 return negativeMultiplier * num; 374 } 375 376 @SuppressWarnings("unchecked") 377 @Override 378 protected void publishResults(CharSequence constraint, FilterResults 379 results) { 380 if (results.values == null || results.count == 0) { 381 if (mListener != null) { 382 int filterType; 383 if (TextUtils.isEmpty(constraint)) { 384 filterType = FILTER_TYPE_NONE; 385 } else { 386 filterType = FILTER_TYPE_EMPTY; 387 } 388 mListener.onSetFilter(filterType, null, 0); 389 } 390 Log.e(TAG, "publishResults: " + results.count + " of null [" + constraint); 391 } else { 392 mLiveResults = (ArrayList<FilterTypeResult>) results.values; 393 Log.e(TAG, "publishResults: " + results.count + " of " + mLiveResults.size() + " [" 394 + constraint); 395 } 396 mLiveResultsCount = results.count; 397 398 if (results.count > 0) { 399 notifyDataSetChanged(); 400 } else { 401 notifyDataSetInvalidated(); 402 } 403 } 404 } 405} 406