| /* |
| * Copyright (C) 2010 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.calendar; |
| |
| import android.content.Context; |
| import android.content.SharedPreferences; |
| import android.content.res.Resources; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| import android.util.Log; |
| import android.widget.ArrayAdapter; |
| |
| import com.android.calendar.TimezoneAdapter.TimezoneRow; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| |
| /** |
| * {@link TimezoneAdapter} is a custom adapter implementation that allows you to |
| * easily display a list of timezones for users to choose from. In addition, it |
| * provides a two-stage behavior that initially only loads a small set of |
| * timezones (one user-provided, the device timezone, and two recent timezones), |
| * which can later be expanded into the full list with a call to |
| * {@link #showAllTimezones()}. |
| */ |
| public class TimezoneAdapter extends ArrayAdapter<TimezoneRow> { |
| private static final String TAG = "TimezoneAdapter"; |
| |
| /** |
| * {@link TimezoneRow} is an immutable class for representing a timezone. We |
| * don't use {@link TimeZone} directly, in order to provide a reasonable |
| * implementation of toString() and to control which display names we use. |
| */ |
| public class TimezoneRow implements Comparable<TimezoneRow> { |
| |
| /** The ID of this timezone, e.g. "America/Los_Angeles" */ |
| public final String mId; |
| |
| /** The display name of this timezone, e.g. "Pacific Time" */ |
| private final String mDisplayName; |
| |
| /** The actual offset of this timezone from GMT in milliseconds */ |
| private final int mOffset; |
| |
| /** Whether the TZ observes daylight saving time */ |
| private final boolean mUseDaylightTime; |
| |
| /** |
| * A one-line representation of this timezone, including both GMT offset |
| * and display name, e.g. "(GMT-7:00) Pacific Time" |
| */ |
| private String mGmtDisplayName; |
| |
| public TimezoneRow(String id, String displayName) { |
| mId = id; |
| mDisplayName = displayName; |
| TimeZone tz = TimeZone.getTimeZone(id); |
| mUseDaylightTime = tz.useDaylightTime(); |
| mOffset = tz.getOffset(TimezoneAdapter.this.mTime); |
| } |
| |
| @Override |
| public String toString() { |
| if (mGmtDisplayName == null) { |
| buildGmtDisplayName(); |
| } |
| |
| return mGmtDisplayName; |
| } |
| |
| /** |
| * |
| */ |
| public void buildGmtDisplayName() { |
| if (mGmtDisplayName != null) { |
| return; |
| } |
| |
| int p = Math.abs(mOffset); |
| StringBuilder name = new StringBuilder(); |
| name.append("GMT"); |
| |
| if (mOffset < 0) { |
| name.append('-'); |
| } else { |
| name.append('+'); |
| } |
| |
| name.append(p / (DateUtils.HOUR_IN_MILLIS)); |
| name.append(':'); |
| |
| int min = p / 60000; |
| min %= 60; |
| |
| if (min < 10) { |
| name.append('0'); |
| } |
| name.append(min); |
| name.insert(0, "("); |
| name.append(") "); |
| name.append(mDisplayName); |
| if (mUseDaylightTime) { |
| name.append(" \u2600"); // Sun symbol |
| } |
| mGmtDisplayName = name.toString(); |
| } |
| |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + ((mDisplayName == null) ? 0 : mDisplayName.hashCode()); |
| result = prime * result + ((mId == null) ? 0 : mId.hashCode()); |
| result = prime * result + mOffset; |
| return result; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj == null) { |
| return false; |
| } |
| if (getClass() != obj.getClass()) { |
| return false; |
| } |
| TimezoneRow other = (TimezoneRow) obj; |
| if (mDisplayName == null) { |
| if (other.mDisplayName != null) { |
| return false; |
| } |
| } else if (!mDisplayName.equals(other.mDisplayName)) { |
| return false; |
| } |
| if (mId == null) { |
| if (other.mId != null) { |
| return false; |
| } |
| } else if (!mId.equals(other.mId)) { |
| return false; |
| } |
| if (mOffset != other.mOffset) { |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| public int compareTo(TimezoneRow another) { |
| if (mOffset == another.mOffset) { |
| return 0; |
| } else { |
| return mOffset < another.mOffset ? -1 : 1; |
| } |
| } |
| |
| } |
| |
| 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 cache of all known timezones, mapped to their string IDs. This is |
| * lazily-loaded on the first call to {@link #loadFromResources(Resources)}. |
| * Loading is called in a synchronized block during initialization of this |
| * class and is based off the resources available to the calling context. |
| * This class should not be used outside of the initial context. |
| * LinkedHashMap is used to preserve ordering. |
| */ |
| private static LinkedHashMap<String, TimezoneRow> sTimezones; |
| |
| private Context mContext; |
| |
| private String mCurrentTimezone; |
| |
| private boolean mShowingAll = false; |
| |
| private long mTime; |
| |
| private Date mDateTime; |
| |
| /** |
| * Constructs a timezone adapter that contains an initial set of entries |
| * including the current timezone, the device timezone, and two recently |
| * used timezones. |
| * |
| * @param context |
| * @param currentTimezone |
| * @param time - needed to determine whether DLS is in effect |
| */ |
| public TimezoneAdapter(Context context, String currentTimezone, long time) { |
| super(context, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1); |
| mContext = context; |
| mCurrentTimezone = currentTimezone; |
| mTime = time; |
| mDateTime = new Date(mTime); |
| mShowingAll = false; |
| showInitialTimezones(); |
| } |
| |
| /** |
| * Given the ID of a timezone, returns the position of the timezone in this |
| * adapter, or -1 if not found. |
| * |
| * @param id the ID of the timezone to find |
| * @return the row position of the timezone, or -1 if not found |
| */ |
| public int getRowById(String id) { |
| TimezoneRow timezone = sTimezones.get(id); |
| if (timezone == null) { |
| return -1; |
| } else { |
| return getPosition(timezone); |
| } |
| } |
| |
| /** |
| * Populates the adapter with an initial list of timezones (one |
| * user-provided, the device timezone, and two recent timezones), which can |
| * later be expanded into the full list with a call to |
| * {@link #showAllTimezones()}. |
| * |
| * @param currentTimezone |
| */ |
| public void showInitialTimezones() { |
| |
| // we use a linked hash set to guarantee only unique IDs are added, and |
| // also to maintain the insertion order of the timezones |
| LinkedHashSet<String> ids = new LinkedHashSet<String>(); |
| |
| // add in the provided (event) timezone |
| if (!TextUtils.isEmpty(mCurrentTimezone)) { |
| ids.add(mCurrentTimezone); |
| } |
| |
| // add in the device timezone if it is different |
| ids.add(TimeZone.getDefault().getID()); |
| |
| // add in recent timezone selections |
| SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mContext); |
| String recentsString = prefs.getString(KEY_RECENT_TIMEZONES, null); |
| if (recentsString != null) { |
| String[] recents = recentsString.split(RECENT_TIMEZONES_DELIMITER); |
| for (String recent : recents) { |
| if (!TextUtils.isEmpty(recent)) { |
| ids.add(recent); |
| } |
| } |
| } |
| |
| clear(); |
| |
| synchronized (TimezoneAdapter.class) { |
| loadFromResources(mContext.getResources()); |
| TimeZone gmt = TimeZone.getTimeZone("GMT"); |
| for (String id : ids) { |
| if (!sTimezones.containsKey(id)) { |
| // a timezone we don't know about, so try to add it... |
| TimeZone newTz = TimeZone.getTimeZone(id); |
| // since TimeZone.getTimeZone actually returns a clone of GMT |
| // when it doesn't recognize the ID, this appears to be the only |
| // reliable way to check to see if the ID is a valid timezone |
| if (!newTz.equals(gmt)) { |
| final String tzDisplayName = newTz.getDisplayName( |
| newTz.inDaylightTime(mDateTime), TimeZone.LONG, |
| Locale.getDefault()); |
| sTimezones.put(id, new TimezoneRow(id, tzDisplayName)); |
| } else { |
| continue; |
| } |
| } |
| add(sTimezones.get(id)); |
| } |
| } |
| mShowingAll = false; |
| } |
| |
| /** |
| * Populates this adapter with all known timezones. |
| */ |
| public void showAllTimezones() { |
| List<TimezoneRow> timezones = new ArrayList<TimezoneRow>(sTimezones.values()); |
| Collections.sort(timezones); |
| clear(); |
| for (TimezoneRow timezone : timezones) { |
| timezone.buildGmtDisplayName(); |
| add(timezone); |
| } |
| mShowingAll = true; |
| } |
| |
| /** |
| * Sets the current timezone. If the adapter is currently displaying only a |
| * subset of views, reload that view since it may have changed. |
| * |
| * @param currentTimezone the current timezone |
| */ |
| public void setCurrentTimezone(String currentTimezone) { |
| if (currentTimezone != null && !currentTimezone.equals(mCurrentTimezone)) { |
| mCurrentTimezone = currentTimezone; |
| if (!mShowingAll) { |
| showInitialTimezones(); |
| } |
| } |
| } |
| |
| /** |
| * Set the time for the adapter and update the display string appropriate |
| * for the time of the year e.g. standard time vs daylight time |
| * |
| * @param time |
| */ |
| public void setTime(long time) { |
| if (time != mTime) { |
| mTime = time; |
| mDateTime.setTime(mTime); |
| sTimezones = null; |
| showInitialTimezones(); |
| } |
| } |
| |
| /** |
| * 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 = GeneralPreferences.getSharedPreferences(mContext); |
| String recentsString = prefs.getString(KEY_RECENT_TIMEZONES, null); |
| List<String> recents; |
| if (recentsString == null) { |
| recents = new ArrayList<String>(MAX_RECENT_TIMEZONES); |
| } else { |
| recents = new ArrayList<String>( |
| Arrays.asList(recentsString.split(RECENT_TIMEZONES_DELIMITER))); |
| } |
| |
| while (recents.size() >= MAX_RECENT_TIMEZONES) { |
| recents.remove(0); |
| } |
| recents.add(id); |
| recentsString = Utils.join(recents, RECENT_TIMEZONES_DELIMITER); |
| Utils.setSharedPreference(mContext, KEY_RECENT_TIMEZONES, recentsString); |
| } |
| |
| /** |
| * Returns an array of ids/time zones. This returns a double indexed array |
| * of ids and time zones for Calendar. It is an inefficient method and |
| * shouldn't be called often, but can be used for one time generation of |
| * this list. |
| * |
| * @return double array of tz ids and tz names |
| */ |
| public CharSequence[][] getAllTimezones() { |
| CharSequence[][] timeZones = new CharSequence[2][sTimezones.size()]; |
| List<String> ids = new ArrayList<String>(sTimezones.keySet()); |
| List<TimezoneRow> timezones = new ArrayList<TimezoneRow>(sTimezones.values()); |
| int i = 0; |
| for (TimezoneRow row : timezones) { |
| timeZones[0][i] = ids.get(i); |
| timeZones[1][i++] = row.toString(); |
| } |
| return timeZones; |
| } |
| |
| private void loadFromResources(Resources resources) { |
| if (sTimezones == null) { |
| String[] ids = resources.getStringArray(R.array.timezone_values); |
| String[] labels = resources.getStringArray(R.array.timezone_labels); |
| |
| int length = ids.length; |
| sTimezones = new LinkedHashMap<String, TimezoneRow>(length); |
| |
| if (ids.length != labels.length) { |
| Log.wtf(TAG, "ids length (" + ids.length + ") and labels length(" + labels.length + |
| ") should be equal but aren't."); |
| } |
| for (int i = 0; i < length; i++) { |
| sTimezones.put(ids[i], new TimezoneRow(ids[i], labels[i])); |
| } |
| } |
| } |
| } |