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