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