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