From b1b7080deea42aa533c3757b585cf765c6b76732 Mon Sep 17 00:00:00 2001 From: Michael Chan Date: Thu, 7 Mar 2013 09:33:49 -0800 Subject: First Draft for Timezone picker Change-Id: I23ce51e9962aced60468b441a12611c75ff8f0d4 --- src/com/android/timezonepicker/TimeZoneData.java | 442 +++++++++++++++++++ .../timezonepicker/TimeZoneFilterTypeAdapter.java | 470 +++++++++++++++++++++ src/com/android/timezonepicker/TimeZoneInfo.java | 352 +++++++++++++++ .../timezonepicker/TimeZonePickerDialog.java | 85 ++++ .../android/timezonepicker/TimeZonePickerView.java | 86 ++++ .../timezonepicker/TimeZoneResultAdapter.java | 283 +++++++++++++ 6 files changed, 1718 insertions(+) create mode 100644 src/com/android/timezonepicker/TimeZoneData.java create mode 100644 src/com/android/timezonepicker/TimeZoneFilterTypeAdapter.java create mode 100644 src/com/android/timezonepicker/TimeZoneInfo.java create mode 100644 src/com/android/timezonepicker/TimeZonePickerDialog.java create mode 100644 src/com/android/timezonepicker/TimeZonePickerView.java create mode 100644 src/com/android/timezonepicker/TimeZoneResultAdapter.java (limited to 'src/com/android') diff --git a/src/com/android/timezonepicker/TimeZoneData.java b/src/com/android/timezonepicker/TimeZoneData.java new file mode 100644 index 0000000..115a8b8 --- /dev/null +++ b/src/com/android/timezonepicker/TimeZoneData.java @@ -0,0 +1,442 @@ +/* + * 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.content.res.AssetManager; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.SparseArray; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.TimeZone; + +public class TimeZoneData { + private static final String TAG = "TimeZoneData"; + private static final boolean DEBUG = false; + private static final int OFFSET_ARRAY_OFFSET = 20; + + ArrayList mTimeZones; + LinkedHashMap> mTimeZonesByCountry; + HashSet mTimeZoneNames = new HashSet(); + + private long mTimeMillis; + private HashMap mCountryCodeToNameMap = new HashMap(); + + public String mDefaultTimeZoneId; + public static boolean is24HourFormat; + private TimeZoneInfo mDefaultTimeZoneInfo; + private String mAlternateDefaultTimeZoneId; + private String mDefaultTimeZoneCountry; + + public TimeZoneData(Context context, String defaultTimeZoneId, long timeMillis) { + is24HourFormat = TimeZoneInfo.is24HourFormat = DateFormat.is24HourFormat(context); + mDefaultTimeZoneId = mAlternateDefaultTimeZoneId = defaultTimeZoneId; + long now = System.currentTimeMillis(); + + if (timeMillis == 0) { + mTimeMillis = now; + } else { + mTimeMillis = timeMillis; + } + loadTzs(context); + Log.i(TAG, "Time to load time zones (ms): " + (System.currentTimeMillis() - now)); + + // now = System.currentTimeMillis(); + // printTz(); + // Log.i(TAG, "Time to print time zones (ms): " + + // (System.currentTimeMillis() - now)); + } + + public void setTime(long timeMillis) { + mTimeMillis = timeMillis; + } + + public TimeZoneInfo get(int position) { + return mTimeZones.get(position); + } + + public int size() { + return mTimeZones.size(); + } + + public int getDefaultTimeZoneIndex() { + return mTimeZones.indexOf(mDefaultTimeZoneInfo); + } + + // TODO speed this up + public int findIndexByTimeZoneIdSlow(String timeZoneId) { + int idx = 0; + for (TimeZoneInfo tzi : mTimeZones) { + if (timeZoneId.equals(tzi.mTzId)) { + return idx; + } + idx++; + } + return -1; + } + + void loadTzs(Context context) { + mTimeZones = new ArrayList(); + HashSet processedTimeZones = loadTzsInZoneTab(context); + String[] tzIds = TimeZone.getAvailableIDs(); + + if (DEBUG) { + Log.e(TAG, "Available time zones: " + tzIds.length); + } + + for (String tzId : tzIds) { + if (processedTimeZones.contains(tzId)) { + continue; + } + + final TimeZone tz = TimeZone.getTimeZone(tzId); + if (tz == null) { + Log.e(TAG, "Timezone not found: " + tzId); + continue; + } + + TimeZoneInfo tzInfo = new TimeZoneInfo(tz, null); + + if (getIdenticalTimeZoneInTheCountry(tzInfo) == -1) { + if (DEBUG) { + Log.e(TAG, "# Adding time zone from getAvailId: " + tzInfo.toString()); + } + mTimeZones.add(tzInfo); + } else { + if (DEBUG) { + Log.e(TAG, + "# Dropping identical time zone from getAvailId: " + tzInfo.toString()); + } + continue; + } + // + // TODO check for dups + // checkForNameDups(tz, tzInfo.mCountry, false /* dls */, + // TimeZone.SHORT, groupIdx, !found); + // checkForNameDups(tz, tzInfo.mCountry, false /* dls */, + // TimeZone.LONG, groupIdx, !found); + // if (tz.useDaylightTime()) { + // checkForNameDups(tz, tzInfo.mCountry, true /* dls */, + // TimeZone.SHORT, groupIdx, + // !found); + // checkForNameDups(tz, tzInfo.mCountry, true /* dls */, + // TimeZone.LONG, groupIdx, + // !found); + // } + } + + // Don't change the order of mTimeZones after this sort + Collections.sort(mTimeZones); + + mTimeZonesByCountry = new LinkedHashMap>(); + mTimeZonesByOffsets = new SparseArray>(mHasTimeZonesInHrOffset.length); + + Date date = new Date(mTimeMillis); + Locale defaultLocal = Locale.getDefault(); + + int idx = 0; + for (TimeZoneInfo tz : mTimeZones) { + tz.mDisplayName = tz.mTz.getDisplayName(tz.mTz.inDaylightTime(date), + TimeZone.LONG, defaultLocal); + + // ///////////////////// + // Grouping tz's by country for search by country + ArrayList group = mTimeZonesByCountry.get(tz.mCountry); + if (group == null) { + group = new ArrayList(); + mTimeZonesByCountry.put(tz.mCountry, group); + } + + group.add(idx); + + // ///////////////////// + // Grouping tz's by GMT offsets + indexByOffsets(idx, tz); + + // Skip all the GMT+xx:xx style display names from search + if (!tz.mDisplayName.endsWith(":00")) { + mTimeZoneNames.add(tz.mDisplayName); + } else if (DEBUG) { + Log.e(TAG, "# Hiding from pretty name search: " + + tz.mDisplayName); + } + + idx++; + } + } + + private boolean[] mHasTimeZonesInHrOffset = new boolean[40]; + SparseArray> mTimeZonesByOffsets; + + public boolean hasTimeZonesInHrOffset(int offsetHr) { + int index = OFFSET_ARRAY_OFFSET + offsetHr; + if (index >= mHasTimeZonesInHrOffset.length || index < 0) { + return false; + } + return mHasTimeZonesInHrOffset[index]; + } + + private void indexByOffsets(int idx, TimeZoneInfo tzi) { + int offsetMillis = tzi.getNowOffsetMillis(); + int index = OFFSET_ARRAY_OFFSET + (int) (offsetMillis / DateUtils.HOUR_IN_MILLIS); + mHasTimeZonesInHrOffset[index] = true; + + ArrayList group = mTimeZonesByOffsets.get(index); + if (group == null) { + group = new ArrayList(); + mTimeZonesByOffsets.put(index, group); + } + group.add(idx); + } + + public ArrayList getTimeZonesByOffset(int offsetHr) { + int index = OFFSET_ARRAY_OFFSET + offsetHr; + if (index >= mHasTimeZonesInHrOffset.length || index < 0) { + return null; + } + return mTimeZonesByOffsets.get(index); + } + + private HashSet loadTzsInZoneTab(Context context) { + HashSet processedTimeZones = new HashSet(); + AssetManager am = context.getAssets(); + InputStream is = null; + + /* + * The 'backward' file contain mappings between new and old time zone + * ids. We will explicitly ignore the old ones. + */ + try { + is = am.open("backward"); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line; + + while ((line = reader.readLine()) != null) { + // Skip comment lines + if (!line.startsWith("#") && line.length() > 0) { + // 0: "Link" + // 1: New tz id + // Last: Old tz id + String[] fields = line.split("\t+"); + String newTzId = fields[1]; + String oldTzId = fields[fields.length - 1]; + + final TimeZone tz = TimeZone.getTimeZone(newTzId); + if (tz == null) { + Log.e(TAG, "Timezone not found: " + newTzId); + continue; + } + + processedTimeZones.add(oldTzId); + + if (DEBUG) { + Log.e(TAG, "# Dropping identical time zone from backward: " + oldTzId); + } + + // Remember the cooler/newer time zone id + if (mDefaultTimeZoneId != null && mDefaultTimeZoneId.equals(oldTzId)) { + mAlternateDefaultTimeZoneId = newTzId; + } + } + } + } catch (IOException ex) { + Log.e(TAG, "Failed to read 'backward' file."); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException ignored) { + } + } + + /* + * zone.tab contains a list of time zones and country code. They are + * "sorted first by country, then an order within the country that (1) + * makes some geographical sense, and (2) puts the most populous zones + * first, where that does not contradict (1)." + */ + try { + String lang = Locale.getDefault().getLanguage(); + is = am.open("zone.tab"); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line; + while ((line = reader.readLine()) != null) { + if (!line.startsWith("#")) { // Skip comment lines + // 0: country code + // 1: coordinates + // 2: time zone id + // 3: comments + final String[] fields = line.split("\t"); + final String timeZoneId = fields[2]; + final String countryCode = fields[0]; + final TimeZone tz = TimeZone.getTimeZone(timeZoneId); + if (tz == null) { + Log.e(TAG, "Timezone not found: " + timeZoneId); + continue; + } + + // Remember the mapping between the country code and display + // name + String country = mCountryCodeToNameMap.get(fields[0]); + if (country == null) { + country = new Locale(lang, countryCode) + .getDisplayCountry(Locale.getDefault()); + mCountryCodeToNameMap.put(countryCode, country); + } + + // TODO Don't like this here but need to get the country of + // the default tz. + + // Find the country of the default tz + if (mDefaultTimeZoneId != null && mDefaultTimeZoneCountry == null + && timeZoneId.equals(mAlternateDefaultTimeZoneId)) { + mDefaultTimeZoneCountry = country; + TimeZone defaultTz = TimeZone.getTimeZone(mDefaultTimeZoneId); + if (defaultTz != null) { + mDefaultTimeZoneInfo = new TimeZoneInfo(defaultTz, country); + + int tzToOverride = getIdenticalTimeZoneInTheCountry(mDefaultTimeZoneInfo); + if (tzToOverride == -1) { + if (DEBUG) { + Log.e(TAG, "Adding default time zone: " + + mDefaultTimeZoneInfo.toString()); + } + mTimeZones.add(mDefaultTimeZoneInfo); + } else { + mTimeZones.add(tzToOverride, mDefaultTimeZoneInfo); + if (DEBUG) { + TimeZoneInfo tzInfoToOverride = mTimeZones.get(tzToOverride); + String tzIdToOverride = tzInfoToOverride.mTzId; + Log.e(TAG, "Replaced by default tz: " + + tzInfoToOverride.toString()); + Log.e(TAG, "Adding default time zone: " + + mDefaultTimeZoneInfo.toString()); + } + } + } + } + + // Add to the list of time zones if the time zone is unique + // in the given country. + TimeZoneInfo timeZoneInfo = new TimeZoneInfo(tz, country); + int identicalTzIdx = getIdenticalTimeZoneInTheCountry(timeZoneInfo); + if (identicalTzIdx == -1) { + if (DEBUG) { + Log.e(TAG, "# Adding time zone: " + timeZoneId + " ## " + + tz.getDisplayName()); + } + mTimeZones.add(timeZoneInfo); + } else { + if (DEBUG) { + Log.e(TAG, "# Dropping identical time zone: " + timeZoneId + " ## " + + tz.getDisplayName()); + } + } + processedTimeZones.add(timeZoneId); + } + } + + } catch (IOException ex) { + Log.e(TAG, "Failed to read 'zone.tab'."); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException ignored) { + } + } + + return processedTimeZones; + } + + private int getIdenticalTimeZoneInTheCountry(TimeZoneInfo timeZoneInfo) { + int idx = 0; + for (TimeZoneInfo tzi : mTimeZones) { + if (tzi.hasSameRules(timeZoneInfo)) { + if (tzi.mCountry == null) { + if (timeZoneInfo.mCountry == null) { + return idx; + } + } else if (tzi.mCountry.equals(timeZoneInfo.mCountry)) { + return idx; + } + } + ++idx; + } + return -1; + } + + private void printTz() { + for (TimeZoneInfo tz : mTimeZones) { + Log.e(TAG, "" + tz.toString()); + } + + Log.e(TAG, "Total number of tz's = " + mTimeZones.size()); + } + + // void checkForNameDups(TimeZone tz, String country, boolean dls, int + // style, int idx, + // boolean print) { + // if (country == null) { + // return; + // } + // String displayName = tz.getDisplayName(dls, style); + // + // if (print) { + // Log.e(TAG, "" + idx + " " + tz.getID() + " " + country + " ## " + + // displayName); + // } + // + // if (tz.useDaylightTime()) { + // if (displayName.matches("GMT[+-][0-9][0-9]:[0-9][0-9]")) { + // return; + // } + // + // if (displayName.length() == 3 && displayName.charAt(2) == 'T' && + // (displayName.charAt(1) == 'S' || displayName.charAt(1) == 'D')) { + // displayName = "" + displayName.charAt(0) + 'T'; + // } else { + // displayName = displayName.replace(" Daylight ", + // " ").replace(" Standard ", " "); + // } + // } + // + // String tzNameWithCountry = country + " ## " + displayName; + // Integer groupId = mCountryPlusTzName2Tzs.get(tzNameWithCountry); + // if (groupId == null) { + // mCountryPlusTzName2Tzs.put(tzNameWithCountry, idx); + // } else if (groupId != idx) { + // Log.e(TAG, "Yikes: " + tzNameWithCountry + " matches " + groupId + + // " and " + idx); + // } + // } + +} diff --git a/src/com/android/timezonepicker/TimeZoneFilterTypeAdapter.java b/src/com/android/timezonepicker/TimeZoneFilterTypeAdapter.java new file mode 100644 index 0000000..82d65bf --- /dev/null +++ b/src/com/android/timezonepicker/TimeZoneFilterTypeAdapter.java @@ -0,0 +1,470 @@ +/* + * 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; + +public class TimeZoneFilterTypeAdapter extends BaseAdapter implements Filterable, OnClickListener { + public static final String TAG = "TimeZoneFilterTypeAdapter"; + + public static final int FILTER_TYPE_EMPTY = -1; + public static final int FILTER_TYPE_NONE = 0; + public static final int FILTER_TYPE_TIME = 1; + public static final int FILTER_TYPE_TIME_ZONE = 2; + public static final int FILTER_TYPE_COUNTRY = 3; + public static final int FILTER_TYPE_STATE = 4; + public static final int FILTER_TYPE_GMT = 5; + + public interface OnSetFilterListener { + void onSetFilter(int filterType, String str, int time); + } + + static class ViewHolder { + int filterType; + String str; + int time; + + TextView typeTextView; + TextView strTextView; + + static void setupViewHolder(View v) { + ViewHolder vh = new ViewHolder(); + vh.typeTextView = (TextView) v.findViewById(R.id.type); + vh.strTextView = (TextView) v.findViewById(R.id.value); + v.setTag(vh); + } + } + + class FilterTypeResult { + boolean showLabel; + int type; + String constraint; + public int time; + + @Override + public String toString() { + return constraint; + } + } + + private ArrayList mLiveResults = new ArrayList(); + 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; + + if (filter.showLabel) { + int resId; + switch (filter.type) { + case FILTER_TYPE_GMT: + resId = R.string.gmt_offset; + break; + case FILTER_TYPE_TIME: + resId = R.string.local_time; + break; + case FILTER_TYPE_TIME_ZONE: + resId = R.string.time_zone; + break; + case FILTER_TYPE_COUNTRY: + resId = R.string.country; + break; + default: + throw new IllegalArgumentException(); + } + vh.typeTextView.setText(resId); + vh.typeTextView.setVisibility(View.VISIBLE); + vh.strTextView.setVisibility(View.GONE); + } else { + vh.typeTextView.setVisibility(View.GONE); + vh.strTextView.setVisibility(View.VISIBLE); + } + 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) { + Log.e(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 filtered = new ArrayList(); + + // //////////////////////////////////////// + // 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 time +// if (!gmtOnly) { +// for(TimeZoneInfo tzi : mTimeZoneData.mTimeZones) { +// tzi.getLocalHr(referenceTime) +// } +// } + + } + + // //////////////////////////////////////// + // Search by country + // //////////////////////////////////////// + boolean first = true; + for (String country : mTimeZoneData.mTimeZonesByCountry.keySet()) { + // TODO Perf - cache toLowerCase()? + if (country != null && country.toLowerCase().startsWith(prefixString)) { + FilterTypeResult r; + if (first) { + r = new FilterTypeResult(); + filtered.add(r); + r.type = FILTER_TYPE_COUNTRY; + r.constraint = null; + r.showLabel = true; + first = false; + } + r = new FilterTypeResult(); + filtered.add(r); + r.type = FILTER_TYPE_COUNTRY; + r.constraint = country; + r.showLabel = false; + } + } + + // //////////////////////////////////////// + // Search by time zone name + // //////////////////////////////////////// + first = true; + for (String timeZoneName : mTimeZoneData.mTimeZoneNames) { + // TODO Perf - cache toLowerCase()? + if (timeZoneName.toLowerCase().startsWith(prefixString)) { + FilterTypeResult r; + if (first) { + r = new FilterTypeResult(); + filtered.add(r); + r.type = FILTER_TYPE_TIME_ZONE; + r.constraint = null; + r.showLabel = true; + first = false; + } + r = new FilterTypeResult(); + filtered.add(r); + r.type = FILTER_TYPE_TIME_ZONE; + r.constraint = timeZoneName; + r.showLabel = false; + } + } + + // //////////////////////////////////////// + // TODO Search by state + // //////////////////////////////////////// + Log.e(TAG, "performFiltering <<<< " + filtered.size() + "[" + prefix + "]"); + + results.values = filtered; + results.count = filtered.size(); + return results; + } + + private void handleSearchByGmt(ArrayList filtered, int num, + boolean positiveOnly) { + FilterTypeResult r; + int originalResultCount = filtered.size(); + + // Separator + r = new FilterTypeResult(); + filtered.add(r); + r.type = FILTER_TYPE_GMT; + r.showLabel = true; + + if (num >= 0) { + if (num == 1) { + for (int i = 19; i >= 10; i--) { + if (mTimeZoneData.hasTimeZonesInHrOffset(i)) { + r = new FilterTypeResult(); + filtered.add(r); + r.type = FILTER_TYPE_GMT; + r.time = i; + r.constraint = "GMT+" + r.time; + r.showLabel = false; + } + } + } + + if (mTimeZoneData.hasTimeZonesInHrOffset(num)) { + r = new FilterTypeResult(); + filtered.add(r); + r.type = FILTER_TYPE_GMT; + r.time = num; + r.constraint = "GMT+" + r.time; + r.showLabel = false; + } + num *= -1; + } + + if (!positiveOnly && num != 0) { + if (mTimeZoneData.hasTimeZonesInHrOffset(num)) { + r = new FilterTypeResult(); + filtered.add(r); + r.type = FILTER_TYPE_GMT; + r.time = num; + r.constraint = "GMT" + r.time; + r.showLabel = false; + } + + if (num == -1) { + for (int i = -10; i >= -19; i--) { + if (mTimeZoneData.hasTimeZonesInHrOffset(i)) { + r = new FilterTypeResult(); + filtered.add(r); + r.type = FILTER_TYPE_GMT; + r.time = i; + r.constraint = "GMT" + r.time; + r.showLabel = false; + } + } + } + } + + // Nothing was added except for the separator. Let's remove it. + if (filtered.size() == originalResultCount + 1) { + filtered.remove(originalResultCount); + } + return; + } + + // + // int start = Integer.MAX_VALUE; + // int end = Integer.MIN_VALUE; + // switch(num) { + // case 2: + // if (TimeZoneData.is24HourFormat) { + // start = 23; + // end = 20; + // } + // break; + // case 1: + // if (TimeZoneData.is24HourFormat) { + // start = 19; + // } else { + // start = 12; + // } + // end = 10; + // break; + // } + + /** + * 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; + } + + Log.e(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); + } + Log.e(TAG, "publishResults: " + results.count + " of null [" + constraint); + } else { + mLiveResults = (ArrayList) results.values; + Log.e(TAG, "publishResults: " + results.count + " of " + mLiveResults.size() + " [" + + constraint); + } + mLiveResultsCount = results.count; + + if (results.count > 0) { + notifyDataSetChanged(); + } else { + notifyDataSetInvalidated(); + } + } + } +} diff --git a/src/com/android/timezonepicker/TimeZoneInfo.java b/src/com/android/timezonepicker/TimeZoneInfo.java new file mode 100644 index 0000000..6c526fd --- /dev/null +++ b/src/com/android/timezonepicker/TimeZoneInfo.java @@ -0,0 +1,352 @@ +/* + * 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.format.DateUtils; +import android.text.format.Time; +import android.util.Log; +import android.util.SparseArray; + +import java.lang.reflect.Field; +import java.text.DateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.Formatter; +import java.util.Locale; +import java.util.TimeZone; + +public class TimeZoneInfo implements Comparable { + private static final char SEPARATOR = ','; + private static final String TAG = null; + public static int NUM_OF_TRANSITIONS = 6; + public static long time = System.currentTimeMillis() / 1000; + public static boolean is24HourFormat; + + TimeZone mTz; + public String mTzId; + int mRawoffset; + public int[] mTransitions; // may have trailing 0's. + public String mCountry; + public int groupId; + private boolean hasDst; + public String mDisplayName; + private Time recycledTime = new Time(); + private static StringBuilder mSB = new StringBuilder(50); + private static Formatter mFormatter = new Formatter(mSB, Locale.getDefault()); + + public TimeZoneInfo(TimeZone tz, String country) { + mTz = tz; + mTzId = tz.getID(); + mCountry = country; + mRawoffset = tz.getRawOffset(); + hasDst = tz.useDaylightTime(); + + try { + mTransitions = getTransitions(tz, time); + } catch (NoSuchFieldException ignored) { + } catch (IllegalAccessException ignored) { + ignored.printStackTrace(); + } + } + + SparseArray mLocalTimeCache = new SparseArray(); + long mLocalTimeCacheReferenceTime = 0; + static private long mGmtDisplayNameUpdateTime; + static private SparseArray mGmtDisplayNameCache = new SparseArray(); + + public String getLocalTime(long referenceTime) { + recycledTime.timezone = TimeZone.getDefault().getID(); + recycledTime.set(referenceTime); + + int currYearDay = recycledTime.year * 366 + recycledTime.yearDay; + + recycledTime.timezone = mTzId; + recycledTime.set(referenceTime); + + String localTimeStr = null; + + int hourMinute = recycledTime.hour * 60 + + recycledTime.minute; + + if (mLocalTimeCacheReferenceTime != referenceTime) { + mLocalTimeCacheReferenceTime = referenceTime; + mLocalTimeCache.clear(); + } else { + localTimeStr = mLocalTimeCache.get(hourMinute); + } + + if (localTimeStr == null) { + String format = "%I:%M %p"; + if (currYearDay != (recycledTime.year * 366 + recycledTime.yearDay)) { + if (is24HourFormat) { + format = "%b %d %H:%M"; + } else { + format = "%b %d %I:%M %p"; + } + } else if (is24HourFormat) { + format = "%H:%M"; + } + + // format = "%Y-%m-%d %H:%M"; + localTimeStr = recycledTime.format(format); + mLocalTimeCache.put(hourMinute, localTimeStr); + } + + return localTimeStr; + } + + public int getLocalHr(long referenceTime) { + recycledTime.timezone = TimeZone.getDefault().getID(); + recycledTime.set(referenceTime); + return recycledTime.hour; + } + + public int getNowOffsetMillis() { + return mTz.getOffset(System.currentTimeMillis()); + } + + /* + * The method is synchronized because there's one mSB, which is used by + * mFormatter, per instance. If there are multiple callers for + * getGmtDisplayName, the output may be mangled. + */ + public synchronized String getGmtDisplayName(Context context) { + // TODO Note: The local time is shown in current time (current GMT + // offset) which may be different from the time specified by + // mTimeMillis + + final long nowMinute = System.currentTimeMillis() / DateUtils.MINUTE_IN_MILLIS; + final long now = nowMinute * DateUtils.MINUTE_IN_MILLIS; + final int gmtOffset = mTz.getOffset(now); + int cacheKey; + + boolean hasFutureDST = mTz.useDaylightTime(); + if (hasFutureDST) { + cacheKey = (int) (gmtOffset + 36 * DateUtils.HOUR_IN_MILLIS); + } else { + cacheKey = (int) (gmtOffset - 36 * DateUtils.HOUR_IN_MILLIS); + } + + String displayName = null; + if (mGmtDisplayNameUpdateTime != nowMinute) { + mGmtDisplayNameUpdateTime = nowMinute; + mGmtDisplayNameCache.clear(); + } else { + displayName = mGmtDisplayNameCache.get(cacheKey); + } + + if (displayName == null) { + mSB.setLength(0); + int flags = DateUtils.FORMAT_ABBREV_ALL; + flags |= DateUtils.FORMAT_SHOW_TIME; + if (TimeZoneInfo.is24HourFormat) { + flags |= DateUtils.FORMAT_24HOUR; + } + + // mFormatter writes to mSB + DateUtils.formatDateRange(context, mFormatter, now, now, flags, mTzId); + mSB.append(" (GMT"); + + if (gmtOffset < 0) { + mSB.append('-'); + } else { + mSB.append('+'); + } + + final int p = Math.abs(gmtOffset); + mSB.append(p / DateUtils.HOUR_IN_MILLIS); // Hour + + final int min = (p / 60000) % 60; + if (min != 0) { // Show minutes if non-zero + mSB.append(':'); + if (min < 10) { + mSB.append('0'); + } + mSB.append(min); + } + mSB.append(')'); + + if (hasFutureDST) { + mSB.append(" \u2600"); // Sun symbol + } + + displayName = mSB.toString(); + mGmtDisplayNameCache.put(cacheKey, displayName); + } + return displayName; + } + + private static int[] getTransitions(TimeZone tz, long time) + throws IllegalAccessException, NoSuchFieldException { + Class zoneInfoClass = tz.getClass(); + Field mTransitionsField = zoneInfoClass.getDeclaredField("mTransitions"); + mTransitionsField.setAccessible(true); + int[] objTransitions = (int[]) mTransitionsField.get(tz); + int[] transitions = null; + if (objTransitions.length != 0) { + transitions = new int[NUM_OF_TRANSITIONS]; + int numOfTransitions = 0; + for (int i = 0; i < objTransitions.length; ++i) { + if (objTransitions[i] < time) { + continue; + } + transitions[numOfTransitions++] = objTransitions[i]; + if (numOfTransitions == NUM_OF_TRANSITIONS) { + break; + } + } + } + return transitions; + } + + public boolean hasSameRules(TimeZoneInfo tzi) { + // this.mTz.hasSameRules(tzi.mTz) + + return this.mRawoffset == tzi.mRawoffset + && Arrays.equals(this.mTransitions, tzi.mTransitions); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + final String country = this.mCountry; + final TimeZone tz = this.mTz; + + sb.append(mTzId); + sb.append(SEPARATOR); + sb.append(tz.getDisplayName(false /* daylightTime */, TimeZone.LONG)); + sb.append(SEPARATOR); + sb.append(tz.getDisplayName(false /* daylightTime */, TimeZone.SHORT)); + sb.append(SEPARATOR); + if (tz.useDaylightTime()) { + sb.append(tz.getDisplayName(true, TimeZone.LONG)); + sb.append(SEPARATOR); + sb.append(tz.getDisplayName(true, TimeZone.SHORT)); + } else { + sb.append(SEPARATOR); + } + sb.append(SEPARATOR); + sb.append(tz.getRawOffset() / 3600000f); + sb.append(SEPARATOR); + sb.append(tz.getDSTSavings() / 3600000f); + sb.append(SEPARATOR); + sb.append(country); + sb.append(SEPARATOR); + + // 1-1-2013 noon GMT + sb.append(getLocalTime(1357041600000L)); + sb.append(SEPARATOR); + + // 3-15-2013 noon GMT + sb.append(getLocalTime(1363348800000L)); + sb.append(SEPARATOR); + + // 7-1-2013 noon GMT + sb.append(getLocalTime(1372680000000L)); + sb.append(SEPARATOR); + + // 11-01-2013 noon GMT + sb.append(getLocalTime(1383307200000L)); + sb.append(SEPARATOR); + + // if (this.mTransitions != null && this.mTransitions.length != 0) { + // sb.append('"'); + // DateFormat df = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss Z", + // Locale.US); + // df.setTimeZone(tz); + // DateFormat weekdayFormat = new SimpleDateFormat("EEEE", Locale.US); + // weekdayFormat.setTimeZone(tz); + // Formatter f = new Formatter(sb); + // for (int i = 0; i < this.mTransitions.length; ++i) { + // if (this.mTransitions[i] < time) { + // continue; + // } + // + // String fromTime = formatTime(df, this.mTransitions[i] - 1); + // String toTime = formatTime(df, this.mTransitions[i]); + // f.format("%s -> %s (%d)", fromTime, toTime, this.mTransitions[i]); + // + // String weekday = weekdayFormat.format(new Date(1000L * + // this.mTransitions[i])); + // if (!weekday.equals("Sunday")) { + // f.format(" -- %s", weekday); + // } + // sb.append("##"); + // } + // sb.append('"'); + // } + // sb.append(SEPARATOR); + sb.append('\n'); + return sb.toString(); + } + + private static String formatTime(DateFormat df, int s) { + long ms = s * 1000L; + return df.format(new Date(ms)); + } + + /* + * Returns a negative integer if this instance is less than the other; a + * positive integer if this instance is greater than the other; 0 if this + * instance has the same order as the other. + */ + @Override + public int compareTo(TimeZoneInfo other) { + + // TODO !!! Should compare the clock time instead of raw offset + + // Higher raw offset comes before i.e. if the offset is bigger, return + // positive number. + if (this.mRawoffset != other.mRawoffset) { + return other.mRawoffset - this.mRawoffset; + } + + // TZ with DST comes first because the offset is bigger during DST + // compared to a tz without DST + if (this.hasDst != other.hasDst) { + return this.hasDst ? -1 : 1; + } + + // By country + if (this.mCountry == null) { + if (other.mCountry != null) { + return 1; + } + } + + if (other.mCountry == null) { + return -1; + } else { + int diff = this.mCountry.compareTo(other.mCountry); + + if (diff != 0) { + return diff; + } + } + + if (Arrays.equals(this.mTransitions, other.mTransitions)) { + Log.e(TAG, "Not expected to be comparing tz with the same country, same offset," + + " same dst, same transitions:\n" + this.toString() + "\n" + other.toString()); + } + + // Finally diff by display name + return this.mTz.getDisplayName(Locale.getDefault()).compareTo( + other.mTz.getDisplayName(Locale.getDefault())); + } +} diff --git a/src/com/android/timezonepicker/TimeZonePickerDialog.java b/src/com/android/timezonepicker/TimeZonePickerDialog.java new file mode 100644 index 0000000..e1d70a7 --- /dev/null +++ b/src/com/android/timezonepicker/TimeZonePickerDialog.java @@ -0,0 +1,85 @@ +/* + * 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.app.Dialog; +import android.app.DialogFragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +public class TimeZonePickerDialog extends DialogFragment implements + TimeZonePickerView.OnTimeZoneSetListener { + public static final String BUNDLE_START_TIME_MILLIS = "bundle_event_start_time"; + public static final String BUNDLE_TIME_ZONE = "bundle_event_time_zone"; + + private OnTimeZoneSetListener mTimeZoneSetListener; + + public interface OnTimeZoneSetListener { + void onTimeZoneSet(TimeZoneInfo tzi); + } + + public void setOnTimeZoneSetListener(OnTimeZoneSetListener l) { + mTimeZoneSetListener = l; + } + + public TimeZonePickerDialog() { + super(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + long timeMillis = 0; + String timeZone = null; + if (savedInstanceState != null) { + // TODO + } else { + Bundle b = getArguments(); + if (b != null) { + timeMillis = b.getLong(BUNDLE_START_TIME_MILLIS); + timeZone = b.getString(BUNDLE_TIME_ZONE); + } + } + return new TimeZonePickerView(getActivity(), null, timeZone, timeMillis, this); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + + Window w = dialog.getWindow(); + WindowManager.LayoutParams a = w.getAttributes(); + a.softInputMode |= WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; + a.softInputMode |= WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN; + w.setAttributes(a); + + return dialog; + } + + @Override + public void onTimeZoneSet(TimeZoneInfo tzi) { + if (mTimeZoneSetListener != null) { + mTimeZoneSetListener.onTimeZoneSet(tzi); + } + dismiss(); + } +} diff --git a/src/com/android/timezonepicker/TimeZonePickerView.java b/src/com/android/timezonepicker/TimeZonePickerView.java new file mode 100644 index 0000000..fc7e5e0 --- /dev/null +++ b/src/com/android/timezonepicker/TimeZonePickerView.java @@ -0,0 +1,86 @@ +/* + * 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.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AutoCompleteTextView; +import android.widget.LinearLayout; +import android.widget.ListView; + +public class TimeZonePickerView extends LinearLayout implements TextWatcher, OnItemClickListener { + private static final String TAG = "TimeZonePickerView"; + + private Context mContext; + private AutoCompleteTextView mAutoCompleteTextView; + private TimeZoneFilterTypeAdapter mFilterAdapter; + TimeZoneResultAdapter mResultAdapter; + + public interface OnTimeZoneSetListener { + void onTimeZoneSet(TimeZoneInfo tzi); + } + + public TimeZonePickerView(Context context, AttributeSet attrs, + String timeZone, long timeMillis, OnTimeZoneSetListener l) { + super(context, attrs); + mContext = context; + LayoutInflater inflater = (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.timezonepickerview, this, true); + + TimeZoneData tzd = new TimeZoneData(mContext, timeZone, timeMillis); + + mResultAdapter = new TimeZoneResultAdapter(mContext, tzd, l); + ListView timeZoneList = (ListView) findViewById(R.id.timezonelist); + timeZoneList.setAdapter(mResultAdapter); + + mAutoCompleteTextView = (AutoCompleteTextView) findViewById(R.id.searchBox); + mFilterAdapter = new TimeZoneFilterTypeAdapter(mContext, tzd, mResultAdapter); + mAutoCompleteTextView.setAdapter(mFilterAdapter); + mAutoCompleteTextView.addTextChangedListener(this); + mAutoCompleteTextView.setOnItemClickListener(this); + } + + // Implementation of TextWatcher + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + // Implementation of TextWatcher + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mFilterAdapter.getFilter().filter(s.toString()); + } + + // Implementation of TextWatcher + @Override + public void afterTextChanged(Editable s) { + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + // An onClickListener for the view item because I haven't figured out a + // way to update the AutoCompleteTextView without causing an infinite loop. + mFilterAdapter.onClick(view); + } +} diff --git a/src/com/android/timezonepicker/TimeZoneResultAdapter.java b/src/com/android/timezonepicker/TimeZoneResultAdapter.java new file mode 100644 index 0000000..7d8b10f --- /dev/null +++ b/src/com/android/timezonepicker/TimeZoneResultAdapter.java @@ -0,0 +1,283 @@ +/* + * 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.content.SharedPreferences; +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.TextView; + +import com.android.timezonepicker.TimeZoneFilterTypeAdapter.OnSetFilterListener; +import com.android.timezonepicker.TimeZonePickerView.OnTimeZoneSetListener; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +public class TimeZoneResultAdapter extends BaseAdapter implements OnClickListener, + OnSetFilterListener { + private static final String TAG = "TimeZoneResultAdapter"; + private static final int VIEW_TAG_TIME_ZONE = R.id.time_zone; + + /** SharedPref name and key for recent time zones */ + private static final String SHARED_PREFS_NAME = "com.android.calendar_preferences"; + private static final String KEY_RECENT_TIMEZONES = "preferences_recent_timezones"; + + /** + * The delimiter we use when serializing recent timezones to shared + * preferences + */ + private static final String RECENT_TIMEZONES_DELIMITER = ","; + + /** The maximum number of recent timezones to save */ + private static final int MAX_RECENT_TIMEZONES = 3; + + static class ViewHolder { + TextView timeZone; + TextView timeOffset; + TextView location; + + static void setupViewHolder(View v) { + ViewHolder vh = new ViewHolder(); + vh.timeZone = (TextView) v.findViewById(R.id.time_zone); + vh.timeOffset = (TextView) v.findViewById(R.id.time_offset); + vh.location = (TextView) v.findViewById(R.id.location); + v.setTag(vh); + } + } + + private Context mContext; + private LayoutInflater mInflater; + + private OnTimeZoneSetListener mTimeZoneSetListener; + private TimeZoneData mTimeZoneData; + + private int[] mFilteredTimeZoneIndices; + private int mFilteredTimeZoneLength = 0; + private int mFilterType; + + public TimeZoneResultAdapter(Context context, TimeZoneData tzd, + com.android.timezonepicker.TimeZonePickerView.OnTimeZoneSetListener l) { + super(); + + mContext = context; + mTimeZoneData = tzd; + mTimeZoneSetListener = l; + + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + mFilteredTimeZoneIndices = new int[mTimeZoneData.size()]; + + onSetFilter(TimeZoneFilterTypeAdapter.FILTER_TYPE_NONE, null, 0); + } + + // Implements OnSetFilterListener + @Override + public void onSetFilter(int filterType, String str, int time) { + Log.d(TAG, "onSetFilter: " + filterType + " [" + str + "] " + time); + + mFilterType = filterType; + mFilteredTimeZoneLength = 0; + int idx = 0; + + switch (filterType) { + case TimeZoneFilterTypeAdapter.FILTER_TYPE_EMPTY: + break; + case TimeZoneFilterTypeAdapter.FILTER_TYPE_NONE: + // Show the default/current value first + int defaultTzIndex = mTimeZoneData.getDefaultTimeZoneIndex(); + if (defaultTzIndex != -1) { + mFilteredTimeZoneIndices[mFilteredTimeZoneLength++] = defaultTzIndex; + } + + // Show the recent selections + SharedPreferences prefs = mContext.getSharedPreferences(SHARED_PREFS_NAME, + Context.MODE_PRIVATE); + String recentsString = prefs.getString(KEY_RECENT_TIMEZONES, null); + if (!TextUtils.isEmpty(recentsString)) { + String[] recents = recentsString.split(RECENT_TIMEZONES_DELIMITER); + for (int i = recents.length - 1; i >= 0; i--) { + if (!TextUtils.isEmpty(recents[i]) + && !recents[i].equals(mTimeZoneData.mDefaultTimeZoneId)) { + int index = mTimeZoneData.findIndexByTimeZoneIdSlow(recents[i]); + if (index != -1) { + mFilteredTimeZoneIndices[mFilteredTimeZoneLength++] = index; + } + } + } + } + + break; + case TimeZoneFilterTypeAdapter.FILTER_TYPE_GMT: + ArrayList indices = mTimeZoneData.getTimeZonesByOffset(time); + if (indices != null) { + for (Integer i : indices) { + mFilteredTimeZoneIndices[mFilteredTimeZoneLength++] = i; + } + } + break; + case TimeZoneFilterTypeAdapter.FILTER_TYPE_TIME: + // TODO Filter by time properly + for (TimeZoneInfo tzi : mTimeZoneData.mTimeZones) { + if (str.equalsIgnoreCase(tzi.getGmtDisplayName(mContext))) { + mFilteredTimeZoneIndices[mFilteredTimeZoneLength++] = idx; + } + idx++; + } + break; + case TimeZoneFilterTypeAdapter.FILTER_TYPE_TIME_ZONE: + for (TimeZoneInfo tzi : mTimeZoneData.mTimeZones) { + if (str.equalsIgnoreCase(tzi.mDisplayName)) { + mFilteredTimeZoneIndices[mFilteredTimeZoneLength++] = idx; + } + idx++; + } + break; + case TimeZoneFilterTypeAdapter.FILTER_TYPE_COUNTRY: + ArrayList tzIds = mTimeZoneData.mTimeZonesByCountry.get(str); + if (tzIds != null) { + for (Integer tzi : tzIds) { + mFilteredTimeZoneIndices[mFilteredTimeZoneLength++] = tzi; + } + } + break; + case TimeZoneFilterTypeAdapter.FILTER_TYPE_STATE: + // TODO Filter by state + break; + default: + throw new IllegalArgumentException(); + } + notifyDataSetChanged(); + } + + /** + * Saves the given timezone ID as a recent timezone under shared + * preferences. If there are already the maximum number of recent timezones + * saved, it will remove the oldest and append this one. + * + * @param id the ID of the timezone to save + * @see {@link #MAX_RECENT_TIMEZONES} + */ + public void saveRecentTimezone(String id) { + SharedPreferences prefs = mContext.getSharedPreferences(SHARED_PREFS_NAME, + Context.MODE_PRIVATE); + String recentsString = prefs.getString(KEY_RECENT_TIMEZONES, null); + if (recentsString == null) { + recentsString = id; + } else { + List recents = new ArrayList( + Arrays.asList(recentsString.split(RECENT_TIMEZONES_DELIMITER))); + Iterator it = recents.iterator(); + while(it.hasNext()) { + String tz = it.next(); + if (id.equals(tz)) { + it.remove(); + } + } + + while (recents.size() >= MAX_RECENT_TIMEZONES) { + recents.remove(0); + } + recents.add(id); + + StringBuilder builder = new StringBuilder(); + boolean first = true; + for (String recent : recents) { + if (first) { + first = false; + } else { + builder.append(RECENT_TIMEZONES_DELIMITER); + } + builder.append(recent); + } + recentsString = builder.toString(); + } + + prefs.edit().putString(KEY_RECENT_TIMEZONES, recentsString).apply(); + } + + @Override + public int getCount() { + return mFilteredTimeZoneLength; + } + + @Override + public TimeZoneInfo getItem(int position) { + if (position < 0 || position >= mFilteredTimeZoneLength) { + return null; + } + + return mTimeZoneData.get(mFilteredTimeZoneIndices[position]); + } + + @Override + public long getItemId(int position) { + return mFilteredTimeZoneIndices[position]; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View v = convertView; + + if (v == null) { + v = mInflater.inflate(R.layout.time_zone_item, null); + v.setOnClickListener(this); + ViewHolder.setupViewHolder(v); + } + + TimeZoneInfo tzi = mTimeZoneData.get(mFilteredTimeZoneIndices[position]); + v.setTag(VIEW_TAG_TIME_ZONE, tzi); + + ViewHolder vh = (ViewHolder) v.getTag(); + vh.timeOffset.setText(tzi.getGmtDisplayName(mContext)); + + vh.timeZone.setText(tzi.mDisplayName); + + String location = tzi.mCountry; + if (location == null) { + vh.location.setVisibility(View.INVISIBLE); + } else { + vh.location.setText(location); + vh.location.setVisibility(View.VISIBLE); + } + + return v; + } + + @Override + public boolean hasStableIds() { + return true; + } + + // Implements OnClickListener + @Override + public void onClick(View v) { + if (mTimeZoneSetListener != null) { + TimeZoneInfo tzi = (TimeZoneInfo) v.getTag(VIEW_TAG_TIME_ZONE); + mTimeZoneSetListener.onTimeZoneSet(tzi); + saveRecentTimezone(tzi.mTzId); + } + } + +} -- cgit v1.2.3