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