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