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