summaryrefslogtreecommitdiffstats
path: root/src/com/android
diff options
context:
space:
mode:
authorMichael Chan <mchan@android.com>2013-03-07 09:33:49 -0800
committerMichael Chan <mchan@android.com>2013-04-03 06:05:10 -0700
commitb1b7080deea42aa533c3757b585cf765c6b76732 (patch)
treec777b8eb7182209f16f1213bea0c8deea38fcd7b /src/com/android
parent0e791fedeb7517d315ed9f7524fa49f9476382e8 (diff)
downloadandroid_frameworks_opt_timezonepicker-b1b7080deea42aa533c3757b585cf765c6b76732.tar.gz
android_frameworks_opt_timezonepicker-b1b7080deea42aa533c3757b585cf765c6b76732.tar.bz2
android_frameworks_opt_timezonepicker-b1b7080deea42aa533c3757b585cf765c6b76732.zip
First Draft for Timezone picker
Change-Id: I23ce51e9962aced60468b441a12611c75ff8f0d4
Diffstat (limited to 'src/com/android')
-rw-r--r--src/com/android/timezonepicker/TimeZoneData.java442
-rw-r--r--src/com/android/timezonepicker/TimeZoneFilterTypeAdapter.java470
-rw-r--r--src/com/android/timezonepicker/TimeZoneInfo.java352
-rw-r--r--src/com/android/timezonepicker/TimeZonePickerDialog.java85
-rw-r--r--src/com/android/timezonepicker/TimeZonePickerView.java86
-rw-r--r--src/com/android/timezonepicker/TimeZoneResultAdapter.java283
6 files changed, 1718 insertions, 0 deletions
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<TimeZoneInfo> mTimeZones;
+ LinkedHashMap<String, ArrayList<Integer>> mTimeZonesByCountry;
+ HashSet<String> mTimeZoneNames = new HashSet<String>();
+
+ private long mTimeMillis;
+ private HashMap<String, String> mCountryCodeToNameMap = new HashMap<String, String>();
+
+ 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<TimeZoneInfo>();
+ HashSet<String> 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<String, ArrayList<Integer>>();
+ mTimeZonesByOffsets = new SparseArray<ArrayList<Integer>>(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<Integer> group = mTimeZonesByCountry.get(tz.mCountry);
+ if (group == null) {
+ group = new ArrayList<Integer>();
+ 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<ArrayList<Integer>> 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<Integer> group = mTimeZonesByOffsets.get(index);
+ if (group == null) {
+ group = new ArrayList<Integer>();
+ mTimeZonesByOffsets.put(index, group);
+ }
+ group.add(idx);
+ }
+
+ public ArrayList<Integer> getTimeZonesByOffset(int offsetHr) {
+ int index = OFFSET_ARRAY_OFFSET + offsetHr;
+ if (index >= mHasTimeZonesInHrOffset.length || index < 0) {
+ return null;
+ }
+ return mTimeZonesByOffsets.get(index);
+ }
+
+ private HashSet<String> loadTzsInZoneTab(Context context) {
+ HashSet<String> processedTimeZones = new HashSet<String>();
+ 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<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;
+
+ 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<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 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<FilterTypeResult> 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<FilterTypeResult>) 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<TimeZoneInfo> {
+ 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<String> mLocalTimeCache = new SparseArray<String>();
+ long mLocalTimeCacheReferenceTime = 0;
+ static private long mGmtDisplayNameUpdateTime;
+ static private SparseArray<String> mGmtDisplayNameCache = new SparseArray<String>();
+
+ 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<Integer> 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<Integer> 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<String> recents = new ArrayList<String>(
+ Arrays.asList(recentsString.split(RECENT_TIMEZONES_DELIMITER)));
+ Iterator<String> 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);
+ }
+ }
+
+}