TimeZoneFilterTypeAdapter.java revision b1b7080deea42aa533c3757b585cf765c6b76732
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_TIME = 1; 39 public static final int FILTER_TYPE_TIME_ZONE = 2; 40 public static final int FILTER_TYPE_COUNTRY = 3; 41 public static final int FILTER_TYPE_STATE = 4; 42 public static final int FILTER_TYPE_GMT = 5; 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 53 TextView typeTextView; 54 TextView strTextView; 55 56 static void setupViewHolder(View v) { 57 ViewHolder vh = new ViewHolder(); 58 vh.typeTextView = (TextView) v.findViewById(R.id.type); 59 vh.strTextView = (TextView) v.findViewById(R.id.value); 60 v.setTag(vh); 61 } 62 } 63 64 class FilterTypeResult { 65 boolean showLabel; 66 int type; 67 String constraint; 68 public int time; 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 90 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 91 } 92 93 @Override 94 public int getCount() { 95 return mLiveResultsCount; 96 } 97 98 @Override 99 public FilterTypeResult getItem(int position) { 100 return mLiveResults.get(position); 101 } 102 103 @Override 104 public long getItemId(int position) { 105 return position; 106 } 107 108 @Override 109 public View getView(int position, View convertView, ViewGroup parent) { 110 View v; 111 112 if (convertView != null) { 113 v = convertView; 114 } else { 115 v = mInflater.inflate(R.layout.time_zone_filter_item, null); 116 ViewHolder.setupViewHolder(v); 117 } 118 119 ViewHolder vh = (ViewHolder) v.getTag(); 120 121 if (position >= mLiveResults.size()) { 122 Log.e(TAG, "getView: " + position + " of " + mLiveResults.size()); 123 } 124 125 FilterTypeResult filter = mLiveResults.get(position); 126 127 vh.filterType = filter.type; 128 vh.str = filter.constraint; 129 vh.time = filter.time; 130 131 if (filter.showLabel) { 132 int resId; 133 switch (filter.type) { 134 case FILTER_TYPE_GMT: 135 resId = R.string.gmt_offset; 136 break; 137 case FILTER_TYPE_TIME: 138 resId = R.string.local_time; 139 break; 140 case FILTER_TYPE_TIME_ZONE: 141 resId = R.string.time_zone; 142 break; 143 case FILTER_TYPE_COUNTRY: 144 resId = R.string.country; 145 break; 146 default: 147 throw new IllegalArgumentException(); 148 } 149 vh.typeTextView.setText(resId); 150 vh.typeTextView.setVisibility(View.VISIBLE); 151 vh.strTextView.setVisibility(View.GONE); 152 } else { 153 vh.typeTextView.setVisibility(View.GONE); 154 vh.strTextView.setVisibility(View.VISIBLE); 155 } 156 vh.strTextView.setText(filter.constraint); 157 return v; 158 } 159 160 OnClickListener mDummyListener = new OnClickListener() { 161 162 @Override 163 public void onClick(View v) { 164 } 165 }; 166 167 // Implements OnClickListener 168 169 // This onClickListener is actually called from the AutoCompleteTextView's 170 // onItemClickListener. Trying to update the text in AutoCompleteTextView 171 // is causing an infinite loop. 172 @Override 173 public void onClick(View v) { 174 if (mListener != null && v != null) { 175 ViewHolder vh = (ViewHolder) v.getTag(); 176 mListener.onSetFilter(vh.filterType, vh.str, vh.time); 177 } 178 notifyDataSetInvalidated(); 179 } 180 181 // Implements Filterable 182 @Override 183 public Filter getFilter() { 184 if (mFilter == null) { 185 mFilter = new ArrayFilter(); 186 } 187 return mFilter; 188 } 189 190 private class ArrayFilter extends Filter { 191 @Override 192 protected FilterResults performFiltering(CharSequence prefix) { 193 Log.e(TAG, "performFiltering >>>> [" + prefix + "]"); 194 195 FilterResults results = new FilterResults(); 196 String prefixString = null; 197 if (prefix != null) { 198 prefixString = prefix.toString().trim().toLowerCase(); 199 } 200 201 if (TextUtils.isEmpty(prefixString)) { 202 results.values = null; 203 results.count = 0; 204 return results; 205 } 206 207 // TODO Perf - we can loop through the filtered list if the new 208 // search string starts with the old search string 209 ArrayList<FilterTypeResult> filtered = new ArrayList<FilterTypeResult>(); 210 211 // //////////////////////////////////////// 212 // Search by local time and GMT offset 213 // //////////////////////////////////////// 214 boolean gmtOnly = false; 215 int startParsePosition = 0; 216 if (prefixString.charAt(0) == '+' || prefixString.charAt(0) == '-') { 217 gmtOnly = true; 218 } 219 220 if (prefixString.startsWith("gmt")) { 221 startParsePosition = 3; 222 gmtOnly = true; 223 } 224 225 int num = parseNum(prefixString, startParsePosition); 226 if (num != Integer.MIN_VALUE) { 227 boolean positiveOnly = prefixString.length() > startParsePosition 228 && prefixString.charAt(startParsePosition) == '+'; 229 handleSearchByGmt(filtered, num, positiveOnly); 230 231 // Search by time 232// if (!gmtOnly) { 233// for(TimeZoneInfo tzi : mTimeZoneData.mTimeZones) { 234// tzi.getLocalHr(referenceTime) 235// } 236// } 237 238 } 239 240 // //////////////////////////////////////// 241 // Search by country 242 // //////////////////////////////////////// 243 boolean first = true; 244 for (String country : mTimeZoneData.mTimeZonesByCountry.keySet()) { 245 // TODO Perf - cache toLowerCase()? 246 if (country != null && country.toLowerCase().startsWith(prefixString)) { 247 FilterTypeResult r; 248 if (first) { 249 r = new FilterTypeResult(); 250 filtered.add(r); 251 r.type = FILTER_TYPE_COUNTRY; 252 r.constraint = null; 253 r.showLabel = true; 254 first = false; 255 } 256 r = new FilterTypeResult(); 257 filtered.add(r); 258 r.type = FILTER_TYPE_COUNTRY; 259 r.constraint = country; 260 r.showLabel = false; 261 } 262 } 263 264 // //////////////////////////////////////// 265 // Search by time zone name 266 // //////////////////////////////////////// 267 first = true; 268 for (String timeZoneName : mTimeZoneData.mTimeZoneNames) { 269 // TODO Perf - cache toLowerCase()? 270 if (timeZoneName.toLowerCase().startsWith(prefixString)) { 271 FilterTypeResult r; 272 if (first) { 273 r = new FilterTypeResult(); 274 filtered.add(r); 275 r.type = FILTER_TYPE_TIME_ZONE; 276 r.constraint = null; 277 r.showLabel = true; 278 first = false; 279 } 280 r = new FilterTypeResult(); 281 filtered.add(r); 282 r.type = FILTER_TYPE_TIME_ZONE; 283 r.constraint = timeZoneName; 284 r.showLabel = false; 285 } 286 } 287 288 // //////////////////////////////////////// 289 // TODO Search by state 290 // //////////////////////////////////////// 291 Log.e(TAG, "performFiltering <<<< " + filtered.size() + "[" + prefix + "]"); 292 293 results.values = filtered; 294 results.count = filtered.size(); 295 return results; 296 } 297 298 private void handleSearchByGmt(ArrayList<FilterTypeResult> filtered, int num, 299 boolean positiveOnly) { 300 FilterTypeResult r; 301 int originalResultCount = filtered.size(); 302 303 // Separator 304 r = new FilterTypeResult(); 305 filtered.add(r); 306 r.type = FILTER_TYPE_GMT; 307 r.showLabel = true; 308 309 if (num >= 0) { 310 if (num == 1) { 311 for (int i = 19; i >= 10; i--) { 312 if (mTimeZoneData.hasTimeZonesInHrOffset(i)) { 313 r = new FilterTypeResult(); 314 filtered.add(r); 315 r.type = FILTER_TYPE_GMT; 316 r.time = i; 317 r.constraint = "GMT+" + r.time; 318 r.showLabel = false; 319 } 320 } 321 } 322 323 if (mTimeZoneData.hasTimeZonesInHrOffset(num)) { 324 r = new FilterTypeResult(); 325 filtered.add(r); 326 r.type = FILTER_TYPE_GMT; 327 r.time = num; 328 r.constraint = "GMT+" + r.time; 329 r.showLabel = false; 330 } 331 num *= -1; 332 } 333 334 if (!positiveOnly && num != 0) { 335 if (mTimeZoneData.hasTimeZonesInHrOffset(num)) { 336 r = new FilterTypeResult(); 337 filtered.add(r); 338 r.type = FILTER_TYPE_GMT; 339 r.time = num; 340 r.constraint = "GMT" + r.time; 341 r.showLabel = false; 342 } 343 344 if (num == -1) { 345 for (int i = -10; i >= -19; i--) { 346 if (mTimeZoneData.hasTimeZonesInHrOffset(i)) { 347 r = new FilterTypeResult(); 348 filtered.add(r); 349 r.type = FILTER_TYPE_GMT; 350 r.time = i; 351 r.constraint = "GMT" + r.time; 352 r.showLabel = false; 353 } 354 } 355 } 356 } 357 358 // Nothing was added except for the separator. Let's remove it. 359 if (filtered.size() == originalResultCount + 1) { 360 filtered.remove(originalResultCount); 361 } 362 return; 363 } 364 365 // 366 // int start = Integer.MAX_VALUE; 367 // int end = Integer.MIN_VALUE; 368 // switch(num) { 369 // case 2: 370 // if (TimeZoneData.is24HourFormat) { 371 // start = 23; 372 // end = 20; 373 // } 374 // break; 375 // case 1: 376 // if (TimeZoneData.is24HourFormat) { 377 // start = 19; 378 // } else { 379 // start = 12; 380 // } 381 // end = 10; 382 // break; 383 // } 384 385 /** 386 * Acceptable strings are in the following format: [+-]?[0-9]?[0-9] 387 * 388 * @param str 389 * @param startIndex 390 * @return Integer.MIN_VALUE as invalid 391 */ 392 public int parseNum(String str, int startIndex) { 393 int idx = startIndex; 394 int num = Integer.MIN_VALUE; 395 int negativeMultiplier = 1; 396 397 // First char - check for + and - 398 char ch = str.charAt(idx++); 399 switch (ch) { 400 case '-': 401 negativeMultiplier = -1; 402 // fall through 403 case '+': 404 if (idx >= str.length()) { 405 // No more digits 406 return Integer.MIN_VALUE; 407 } 408 409 ch = str.charAt(idx++); 410 break; 411 } 412 413 if (!Character.isDigit(ch)) { 414 // No digit 415 return Integer.MIN_VALUE; 416 } 417 418 // Got first digit 419 num = Character.digit(ch, 10); 420 421 // Check next char 422 if (idx < str.length()) { 423 ch = str.charAt(idx++); 424 if (Character.isDigit(ch)) { 425 // Got second digit 426 num = 10 * num + Character.digit(ch, 10); 427 } else { 428 return Integer.MIN_VALUE; 429 } 430 } 431 432 if (idx != str.length()) { 433 // Invalid 434 return Integer.MIN_VALUE; 435 } 436 437 Log.e(TAG, "Parsing " + str + " -> " + negativeMultiplier * num); 438 return negativeMultiplier * num; 439 } 440 441 @SuppressWarnings("unchecked") 442 @Override 443 protected void publishResults(CharSequence constraint, FilterResults 444 results) { 445 if (results.values == null || results.count == 0) { 446 if (mListener != null) { 447 int filterType; 448 if (TextUtils.isEmpty(constraint)) { 449 filterType = FILTER_TYPE_NONE; 450 } else { 451 filterType = FILTER_TYPE_EMPTY; 452 } 453 mListener.onSetFilter(filterType, null, 0); 454 } 455 Log.e(TAG, "publishResults: " + results.count + " of null [" + constraint); 456 } else { 457 mLiveResults = (ArrayList<FilterTypeResult>) results.values; 458 Log.e(TAG, "publishResults: " + results.count + " of " + mLiveResults.size() + " [" 459 + constraint); 460 } 461 mLiveResultsCount = results.count; 462 463 if (results.count > 0) { 464 notifyDataSetChanged(); 465 } else { 466 notifyDataSetInvalidated(); 467 } 468 } 469 } 470} 471