| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.timezonepicker; |
| |
| import android.content.Context; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup; |
| import android.widget.BaseAdapter; |
| import android.widget.Filter; |
| import android.widget.Filterable; |
| import android.widget.TextView; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| |
| public class TimeZoneFilterTypeAdapter extends BaseAdapter implements Filterable, OnClickListener { |
| public static final String TAG = "TimeZoneFilterTypeAdapter"; |
| |
| private static final boolean DEBUG = false; |
| |
| public static final int FILTER_TYPE_EMPTY = -1; |
| public static final int FILTER_TYPE_NONE = 0; |
| public static final int FILTER_TYPE_COUNTRY = 1; |
| public static final int FILTER_TYPE_STATE = 2; |
| public static final int FILTER_TYPE_GMT = 3; |
| |
| public interface OnSetFilterListener { |
| void onSetFilter(int filterType, String str, int time); |
| } |
| |
| static class ViewHolder { |
| int filterType; |
| String str; |
| int time; |
| TextView strTextView; |
| |
| static void setupViewHolder(View v) { |
| ViewHolder vh = new ViewHolder(); |
| vh.strTextView = (TextView) v.findViewById(R.id.value); |
| v.setTag(vh); |
| } |
| } |
| |
| class FilterTypeResult { |
| int type; |
| String constraint; |
| public int time; |
| |
| public FilterTypeResult(int type, String constraint, int time) { |
| this.type = type; |
| this.constraint = constraint; |
| this.time = time; |
| } |
| |
| @Override |
| public String toString() { |
| return constraint; |
| } |
| } |
| |
| private ArrayList<FilterTypeResult> mLiveResults = new ArrayList<FilterTypeResult>(); |
| private int mLiveResultsCount = 0; |
| |
| private ArrayFilter mFilter; |
| |
| private LayoutInflater mInflater; |
| |
| private TimeZoneData mTimeZoneData; |
| private OnSetFilterListener mListener; |
| |
| public TimeZoneFilterTypeAdapter(Context context, TimeZoneData tzd, OnSetFilterListener l) { |
| mTimeZoneData = tzd; |
| mListener = l; |
| mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| } |
| |
| @Override |
| public int getCount() { |
| return mLiveResultsCount; |
| } |
| |
| @Override |
| public FilterTypeResult getItem(int position) { |
| return mLiveResults.get(position); |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return position; |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| View v; |
| |
| if (convertView != null) { |
| v = convertView; |
| } else { |
| v = mInflater.inflate(R.layout.time_zone_filter_item, null); |
| ViewHolder.setupViewHolder(v); |
| } |
| |
| ViewHolder vh = (ViewHolder) v.getTag(); |
| |
| if (position >= mLiveResults.size()) { |
| Log.e(TAG, "getView: " + position + " of " + mLiveResults.size()); |
| } |
| |
| FilterTypeResult filter = mLiveResults.get(position); |
| |
| vh.filterType = filter.type; |
| vh.str = filter.constraint; |
| vh.time = filter.time; |
| vh.strTextView.setText(filter.constraint); |
| return v; |
| } |
| |
| OnClickListener mDummyListener = new OnClickListener() { |
| |
| @Override |
| public void onClick(View v) { |
| } |
| }; |
| |
| // Implements OnClickListener |
| |
| // This onClickListener is actually called from the AutoCompleteTextView's |
| // onItemClickListener. Trying to update the text in AutoCompleteTextView |
| // is causing an infinite loop. |
| @Override |
| public void onClick(View v) { |
| if (mListener != null && v != null) { |
| ViewHolder vh = (ViewHolder) v.getTag(); |
| mListener.onSetFilter(vh.filterType, vh.str, vh.time); |
| } |
| notifyDataSetInvalidated(); |
| } |
| |
| // Implements Filterable |
| @Override |
| public Filter getFilter() { |
| if (mFilter == null) { |
| mFilter = new ArrayFilter(); |
| } |
| return mFilter; |
| } |
| |
| private class ArrayFilter extends Filter { |
| @Override |
| protected FilterResults performFiltering(CharSequence prefix) { |
| if (DEBUG) { |
| Log.d(TAG, "performFiltering >>>> [" + prefix + "]"); |
| } |
| |
| FilterResults results = new FilterResults(); |
| String prefixString = null; |
| if (prefix != null) { |
| prefixString = prefix.toString().trim().toLowerCase(); |
| } |
| |
| if (TextUtils.isEmpty(prefixString)) { |
| results.values = null; |
| results.count = 0; |
| return results; |
| } |
| |
| // TODO Perf - we can loop through the filtered list if the new |
| // search string starts with the old search string |
| ArrayList<FilterTypeResult> filtered = new ArrayList<FilterTypeResult>(); |
| |
| // //////////////////////////////////////// |
| // Search by local time and GMT offset |
| // //////////////////////////////////////// |
| boolean gmtOnly = false; |
| int startParsePosition = 0; |
| if (prefixString.charAt(0) == '+' || prefixString.charAt(0) == '-') { |
| gmtOnly = true; |
| } |
| |
| if (prefixString.startsWith("gmt")) { |
| startParsePosition = 3; |
| gmtOnly = true; |
| } |
| |
| int num = parseNum(prefixString, startParsePosition); |
| if (num != Integer.MIN_VALUE) { |
| boolean positiveOnly = prefixString.length() > startParsePosition |
| && prefixString.charAt(startParsePosition) == '+'; |
| handleSearchByGmt(filtered, num, positiveOnly); |
| } |
| |
| // //////////////////////////////////////// |
| // Search by country |
| // //////////////////////////////////////// |
| ArrayList<String> countries = new ArrayList<String>(); |
| for (String country : mTimeZoneData.mTimeZonesByCountry.keySet()) { |
| // TODO Perf - cache toLowerCase()? |
| if (!TextUtils.isEmpty(country)) { |
| final String lowerCaseCountry = country.toLowerCase(); |
| boolean isMatch = false; |
| if (lowerCaseCountry.startsWith(prefixString) |
| || (lowerCaseCountry.charAt(0) == prefixString.charAt(0) && |
| isStartingInitialsFor(prefixString, lowerCaseCountry))) { |
| isMatch = true; |
| } else if (lowerCaseCountry.contains(" ")){ |
| // We should also search other words in the country name, so that |
| // searches like "Korea" yield "South Korea". |
| for (String word : lowerCaseCountry.split(" ")) { |
| if (word.startsWith(prefixString)) { |
| isMatch = true; |
| break; |
| } |
| } |
| } |
| if (isMatch) { |
| countries.add(country); |
| } |
| } |
| } |
| if (countries.size() > 0) { |
| // Sort countries alphabetically. |
| Collections.sort(countries); |
| for (String country : countries) { |
| filtered.add(new FilterTypeResult(FILTER_TYPE_COUNTRY, country, 0)); |
| } |
| } |
| |
| // //////////////////////////////////////// |
| // TODO Search by state |
| // //////////////////////////////////////// |
| if (DEBUG) { |
| Log.d(TAG, "performFiltering <<<< " + filtered.size() + "[" + prefix + "]"); |
| } |
| |
| results.values = filtered; |
| results.count = filtered.size(); |
| return results; |
| } |
| |
| /** |
| * Returns true if the prefixString is an initial for string. Note that |
| * this method will return true even if prefixString does not cover all |
| * the words. Words are separated by non-letters which includes spaces |
| * and symbols). |
| * |
| * For example: |
| * isStartingInitialsFor("UA", "United Arab Emirates") would return true |
| * isStartingInitialsFor("US", "U.S. Virgin Island") would return true |
| * |
| * @param prefixString |
| * @param string |
| * @return |
| */ |
| private boolean isStartingInitialsFor(String prefixString, String string) { |
| final int initialLen = prefixString.length(); |
| final int strLen = string.length(); |
| |
| int initialIdx = 0; |
| boolean wasWordBreak = true; |
| for (int i = 0; i < strLen; i++) { |
| if (!Character.isLetter(string.charAt(i))) { |
| wasWordBreak = true; |
| continue; |
| } |
| |
| if (wasWordBreak) { |
| if (prefixString.charAt(initialIdx++) != string.charAt(i)) { |
| return false; |
| } |
| if (initialIdx == initialLen) { |
| return true; |
| } |
| wasWordBreak = false; |
| } |
| } |
| |
| // Special case for "USA". Note that both strings have been turned to lowercase already. |
| if (prefixString.equals("usa") && string.equals("united states")) { |
| return true; |
| } |
| return false; |
| } |
| |
| private void handleSearchByGmt(ArrayList<FilterTypeResult> filtered, int num, |
| boolean positiveOnly) { |
| |
| FilterTypeResult r; |
| if (num >= 0) { |
| if (num == 1) { |
| for (int i = 19; i >= 10; i--) { |
| if (mTimeZoneData.hasTimeZonesInHrOffset(i)) { |
| r = new FilterTypeResult(FILTER_TYPE_GMT, "GMT+" + i, i); |
| filtered.add(r); |
| } |
| } |
| } |
| |
| if (mTimeZoneData.hasTimeZonesInHrOffset(num)) { |
| r = new FilterTypeResult(FILTER_TYPE_GMT, "GMT+" + num, num); |
| filtered.add(r); |
| } |
| num *= -1; |
| } |
| |
| if (!positiveOnly && num != 0) { |
| if (mTimeZoneData.hasTimeZonesInHrOffset(num)) { |
| r = new FilterTypeResult(FILTER_TYPE_GMT, "GMT" + num, num); |
| filtered.add(r); |
| } |
| |
| if (num == -1) { |
| for (int i = -10; i >= -19; i--) { |
| if (mTimeZoneData.hasTimeZonesInHrOffset(i)) { |
| r = new FilterTypeResult(FILTER_TYPE_GMT, "GMT" + i, i); |
| filtered.add(r); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Acceptable strings are in the following format: [+-]?[0-9]?[0-9] |
| * |
| * @param str |
| * @param startIndex |
| * @return Integer.MIN_VALUE as invalid |
| */ |
| public int parseNum(String str, int startIndex) { |
| int idx = startIndex; |
| int num = Integer.MIN_VALUE; |
| int negativeMultiplier = 1; |
| |
| // First char - check for + and - |
| char ch = str.charAt(idx++); |
| switch (ch) { |
| case '-': |
| negativeMultiplier = -1; |
| // fall through |
| case '+': |
| if (idx >= str.length()) { |
| // No more digits |
| return Integer.MIN_VALUE; |
| } |
| |
| ch = str.charAt(idx++); |
| break; |
| } |
| |
| if (!Character.isDigit(ch)) { |
| // No digit |
| return Integer.MIN_VALUE; |
| } |
| |
| // Got first digit |
| num = Character.digit(ch, 10); |
| |
| // Check next char |
| if (idx < str.length()) { |
| ch = str.charAt(idx++); |
| if (Character.isDigit(ch)) { |
| // Got second digit |
| num = 10 * num + Character.digit(ch, 10); |
| } else { |
| return Integer.MIN_VALUE; |
| } |
| } |
| |
| if (idx != str.length()) { |
| // Invalid |
| return Integer.MIN_VALUE; |
| } |
| |
| if (DEBUG) { |
| Log.d(TAG, "Parsing " + str + " -> " + negativeMultiplier * num); |
| } |
| return negativeMultiplier * num; |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| protected void publishResults(CharSequence constraint, FilterResults |
| results) { |
| if (results.values == null || results.count == 0) { |
| if (mListener != null) { |
| int filterType; |
| if (TextUtils.isEmpty(constraint)) { |
| filterType = FILTER_TYPE_NONE; |
| } else { |
| filterType = FILTER_TYPE_EMPTY; |
| } |
| mListener.onSetFilter(filterType, null, 0); |
| } |
| if (DEBUG) { |
| Log.d(TAG, "publishResults: " + results.count + " of null [" + constraint); |
| } |
| } else { |
| mLiveResults = (ArrayList<FilterTypeResult>) results.values; |
| if (DEBUG) { |
| Log.d(TAG, "publishResults: " + results.count + " of " + mLiveResults.size() |
| + " [" + constraint); |
| } |
| } |
| mLiveResultsCount = results.count; |
| |
| if (results.count > 0) { |
| notifyDataSetChanged(); |
| } else { |
| notifyDataSetInvalidated(); |
| } |
| } |
| } |
| } |