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