Home | History | Annotate | Download | only in calendar
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.calendar;
     18 
     19 import android.content.Context;
     20 import android.content.SharedPreferences;
     21 import android.content.res.Resources;
     22 import android.text.TextUtils;
     23 import android.text.format.DateUtils;
     24 import android.util.Log;
     25 import android.widget.ArrayAdapter;
     26 
     27 import com.android.calendar.TimezoneAdapter.TimezoneRow;
     28 
     29 import java.util.ArrayList;
     30 import java.util.Arrays;
     31 import java.util.Collections;
     32 import java.util.Date;
     33 import java.util.LinkedHashMap;
     34 import java.util.LinkedHashSet;
     35 import java.util.List;
     36 import java.util.Locale;
     37 import java.util.TimeZone;
     38 
     39 /**
     40  * {@link TimezoneAdapter} is a custom adapter implementation that allows you to
     41  * easily display a list of timezones for users to choose from. In addition, it
     42  * provides a two-stage behavior that initially only loads a small set of
     43  * timezones (one user-provided, the device timezone, and two recent timezones),
     44  * which can later be expanded into the full list with a call to
     45  * {@link #showAllTimezones()}.
     46  */
     47 public class TimezoneAdapter extends ArrayAdapter<TimezoneRow> {
     48     private static final String TAG = "TimezoneAdapter";
     49 
     50     /**
     51      * {@link TimezoneRow} is an immutable class for representing a timezone. We
     52      * don't use {@link TimeZone} directly, in order to provide a reasonable
     53      * implementation of toString() and to control which display names we use.
     54      */
     55     public class TimezoneRow implements Comparable<TimezoneRow> {
     56 
     57         /** The ID of this timezone, e.g. "America/Los_Angeles" */
     58         public final String mId;
     59 
     60         /** The display name of this timezone, e.g. "Pacific Time" */
     61         private final String mDisplayName;
     62 
     63         /** The actual offset of this timezone from GMT in milliseconds */
     64         private final int mOffset;
     65 
     66         /** Whether the TZ observes daylight saving time */
     67         private final boolean mUseDaylightTime;
     68 
     69         /**
     70          * A one-line representation of this timezone, including both GMT offset
     71          * and display name, e.g. "(GMT-7:00) Pacific Time"
     72          */
     73         private String mGmtDisplayName;
     74 
     75         public TimezoneRow(String id, String displayName) {
     76             mId = id;
     77             mDisplayName = displayName;
     78             TimeZone tz = TimeZone.getTimeZone(id);
     79             mUseDaylightTime = tz.useDaylightTime();
     80             mOffset = tz.getOffset(TimezoneAdapter.this.mTime);
     81         }
     82 
     83         @Override
     84         public String toString() {
     85             if (mGmtDisplayName == null) {
     86                 buildGmtDisplayName();
     87             }
     88 
     89             return mGmtDisplayName;
     90         }
     91 
     92         /**
     93          *
     94          */
     95         public void buildGmtDisplayName() {
     96             if (mGmtDisplayName != null) {
     97                 return;
     98             }
     99 
    100             int p = Math.abs(mOffset);
    101             StringBuilder name = new StringBuilder();
    102             name.append("GMT");
    103 
    104             if (mOffset < 0) {
    105                 name.append('-');
    106             } else {
    107                 name.append('+');
    108             }
    109 
    110             name.append(p / (DateUtils.HOUR_IN_MILLIS));
    111             name.append(':');
    112 
    113             int min = p / 60000;
    114             min %= 60;
    115 
    116             if (min < 10) {
    117                 name.append('0');
    118             }
    119             name.append(min);
    120             name.insert(0, "(");
    121             name.append(") ");
    122             name.append(mDisplayName);
    123             if (mUseDaylightTime) {
    124                 name.append(" \u2600"); // Sun symbol
    125             }
    126             mGmtDisplayName = name.toString();
    127         }
    128 
    129         @Override
    130         public int hashCode() {
    131             final int prime = 31;
    132             int result = 1;
    133             result = prime * result + ((mDisplayName == null) ? 0 : mDisplayName.hashCode());
    134             result = prime * result + ((mId == null) ? 0 : mId.hashCode());
    135             result = prime * result + mOffset;
    136             return result;
    137         }
    138 
    139         @Override
    140         public boolean equals(Object obj) {
    141             if (this == obj) {
    142                 return true;
    143             }
    144             if (obj == null) {
    145                 return false;
    146             }
    147             if (getClass() != obj.getClass()) {
    148                 return false;
    149             }
    150             TimezoneRow other = (TimezoneRow) obj;
    151             if (mDisplayName == null) {
    152                 if (other.mDisplayName != null) {
    153                     return false;
    154                 }
    155             } else if (!mDisplayName.equals(other.mDisplayName)) {
    156                 return false;
    157             }
    158             if (mId == null) {
    159                 if (other.mId != null) {
    160                     return false;
    161                 }
    162             } else if (!mId.equals(other.mId)) {
    163                 return false;
    164             }
    165             if (mOffset != other.mOffset) {
    166                 return false;
    167             }
    168             return true;
    169         }
    170 
    171         @Override
    172         public int compareTo(TimezoneRow another) {
    173             if (mOffset == another.mOffset) {
    174                 return 0;
    175             } else {
    176                 return mOffset < another.mOffset ? -1 : 1;
    177             }
    178         }
    179 
    180     }
    181 
    182     private static final String KEY_RECENT_TIMEZONES = "preferences_recent_timezones";
    183 
    184     /** The delimiter we use when serializing recent timezones to shared preferences */
    185     private static final String RECENT_TIMEZONES_DELIMITER = ",";
    186 
    187     /** The maximum number of recent timezones to save */
    188     private static final int MAX_RECENT_TIMEZONES = 3;
    189 
    190     /**
    191      * Static cache of all known timezones, mapped to their string IDs. This is
    192      * lazily-loaded on the first call to {@link #loadFromResources(Resources)}.
    193      * Loading is called in a synchronized block during initialization of this
    194      * class and is based off the resources available to the calling context.
    195      * This class should not be used outside of the initial context.
    196      * LinkedHashMap is used to preserve ordering.
    197      */
    198     private static LinkedHashMap<String, TimezoneRow> sTimezones;
    199 
    200     private Context mContext;
    201 
    202     private String mCurrentTimezone;
    203 
    204     private boolean mShowingAll = false;
    205 
    206     private long mTime;
    207 
    208     private Date mDateTime;
    209 
    210     /**
    211      * Constructs a timezone adapter that contains an initial set of entries
    212      * including the current timezone, the device timezone, and two recently
    213      * used timezones.
    214      *
    215      * @param context
    216      * @param currentTimezone
    217      * @param time - needed to determine whether DLS is in effect
    218      */
    219     public TimezoneAdapter(Context context, String currentTimezone, long time) {
    220         super(context, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1);
    221         mContext = context;
    222         mCurrentTimezone = currentTimezone;
    223         mTime = time;
    224         mDateTime = new Date(mTime);
    225         mShowingAll = false;
    226         showInitialTimezones();
    227     }
    228 
    229     /**
    230      * Given the ID of a timezone, returns the position of the timezone in this
    231      * adapter, or -1 if not found.
    232      *
    233      * @param id the ID of the timezone to find
    234      * @return the row position of the timezone, or -1 if not found
    235      */
    236     public int getRowById(String id) {
    237         TimezoneRow timezone = sTimezones.get(id);
    238         if (timezone == null) {
    239             return -1;
    240         } else {
    241             return getPosition(timezone);
    242         }
    243     }
    244 
    245     /**
    246      * Populates the adapter with an initial list of timezones (one
    247      * user-provided, the device timezone, and two recent timezones), which can
    248      * later be expanded into the full list with a call to
    249      * {@link #showAllTimezones()}.
    250      *
    251      * @param currentTimezone
    252      */
    253     public void showInitialTimezones() {
    254 
    255         // we use a linked hash set to guarantee only unique IDs are added, and
    256         // also to maintain the insertion order of the timezones
    257         LinkedHashSet<String> ids = new LinkedHashSet<String>();
    258 
    259         // add in the provided (event) timezone
    260         if (!TextUtils.isEmpty(mCurrentTimezone)) {
    261             ids.add(mCurrentTimezone);
    262         }
    263 
    264         // add in the device timezone if it is different
    265         ids.add(TimeZone.getDefault().getID());
    266 
    267         // add in recent timezone selections
    268         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mContext);
    269         String recentsString = prefs.getString(KEY_RECENT_TIMEZONES, null);
    270         if (recentsString != null) {
    271             String[] recents = recentsString.split(RECENT_TIMEZONES_DELIMITER);
    272             for (String recent : recents) {
    273                 if (!TextUtils.isEmpty(recent)) {
    274                     ids.add(recent);
    275                 }
    276             }
    277         }
    278 
    279         clear();
    280 
    281         synchronized (TimezoneAdapter.class) {
    282             loadFromResources(mContext.getResources());
    283             TimeZone gmt = TimeZone.getTimeZone("GMT");
    284             for (String id : ids) {
    285                 if (!sTimezones.containsKey(id)) {
    286                     // a timezone we don't know about, so try to add it...
    287                     TimeZone newTz = TimeZone.getTimeZone(id);
    288                     // since TimeZone.getTimeZone actually returns a clone of GMT
    289                     // when it doesn't recognize the ID, this appears to be the only
    290                     // reliable way to check to see if the ID is a valid timezone
    291                     if (!newTz.equals(gmt)) {
    292                         final String tzDisplayName = newTz.getDisplayName(
    293                                 newTz.inDaylightTime(mDateTime), TimeZone.LONG,
    294                                 Locale.getDefault());
    295                         sTimezones.put(id, new TimezoneRow(id, tzDisplayName));
    296                     } else {
    297                         continue;
    298                     }
    299                 }
    300                 add(sTimezones.get(id));
    301             }
    302         }
    303         mShowingAll = false;
    304     }
    305 
    306     /**
    307      * Populates this adapter with all known timezones.
    308      */
    309     public void showAllTimezones() {
    310         List<TimezoneRow> timezones = new ArrayList<TimezoneRow>(sTimezones.values());
    311         Collections.sort(timezones);
    312         clear();
    313         for (TimezoneRow timezone : timezones) {
    314             timezone.buildGmtDisplayName();
    315             add(timezone);
    316         }
    317         mShowingAll = true;
    318     }
    319 
    320     /**
    321      * Sets the current timezone. If the adapter is currently displaying only a
    322      * subset of views, reload that view since it may have changed.
    323      *
    324      * @param currentTimezone the current timezone
    325      */
    326     public void setCurrentTimezone(String currentTimezone) {
    327         if (currentTimezone != null && !currentTimezone.equals(mCurrentTimezone)) {
    328             mCurrentTimezone = currentTimezone;
    329             if (!mShowingAll) {
    330                 showInitialTimezones();
    331             }
    332         }
    333     }
    334 
    335     /**
    336      * Set the time for the adapter and update the display string appropriate
    337      * for the time of the year e.g. standard time vs daylight time
    338      *
    339      * @param time
    340      */
    341     public void setTime(long time) {
    342         if (time != mTime) {
    343             mTime = time;
    344             mDateTime.setTime(mTime);
    345             sTimezones = null;
    346             showInitialTimezones();
    347         }
    348     }
    349 
    350     /**
    351      * Saves the given timezone ID as a recent timezone under shared
    352      * preferences. If there are already the maximum number of recent timezones
    353      * saved, it will remove the oldest and append this one.
    354      *
    355      * @param id the ID of the timezone to save
    356      * @see {@link #MAX_RECENT_TIMEZONES}
    357      */
    358     public void saveRecentTimezone(String id) {
    359         SharedPreferences prefs = GeneralPreferences.getSharedPreferences(mContext);
    360         String recentsString = prefs.getString(KEY_RECENT_TIMEZONES, null);
    361         List<String> recents;
    362         if (recentsString == null) {
    363             recents = new ArrayList<String>(MAX_RECENT_TIMEZONES);
    364         } else {
    365             recents = new ArrayList<String>(
    366                 Arrays.asList(recentsString.split(RECENT_TIMEZONES_DELIMITER)));
    367         }
    368 
    369         while (recents.size() >= MAX_RECENT_TIMEZONES) {
    370             recents.remove(0);
    371         }
    372         recents.add(id);
    373         recentsString = Utils.join(recents, RECENT_TIMEZONES_DELIMITER);
    374         Utils.setSharedPreference(mContext, KEY_RECENT_TIMEZONES, recentsString);
    375     }
    376 
    377     /**
    378      * Returns an array of ids/time zones. This returns a double indexed array
    379      * of ids and time zones for Calendar. It is an inefficient method and
    380      * shouldn't be called often, but can be used for one time generation of
    381      * this list.
    382      *
    383      * @return double array of tz ids and tz names
    384      */
    385     public CharSequence[][] getAllTimezones() {
    386         CharSequence[][] timeZones = new CharSequence[2][sTimezones.size()];
    387         List<String> ids = new ArrayList<String>(sTimezones.keySet());
    388         List<TimezoneRow> timezones = new ArrayList<TimezoneRow>(sTimezones.values());
    389         int i = 0;
    390         for (TimezoneRow row : timezones) {
    391             timeZones[0][i] = ids.get(i);
    392             timeZones[1][i++] = row.toString();
    393         }
    394         return timeZones;
    395     }
    396 
    397     private void loadFromResources(Resources resources) {
    398         if (sTimezones == null) {
    399             String[] ids = resources.getStringArray(R.array.timezone_values);
    400             String[] labels = resources.getStringArray(R.array.timezone_labels);
    401 
    402             int length = ids.length;
    403             sTimezones = new LinkedHashMap<String, TimezoneRow>(length);
    404 
    405             if (ids.length != labels.length) {
    406                 Log.wtf(TAG, "ids length (" + ids.length + ") and labels length(" + labels.length +
    407                         ") should be equal but aren't.");
    408             }
    409             for (int i = 0; i < length; i++) {
    410                 sTimezones.put(ids[i], new TimezoneRow(ids[i], labels[i]));
    411             }
    412         }
    413     }
    414 }
    415