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