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