• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
TimezoneRow(String id, String displayName)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
toString()104         public String toString() {
105             return mGmtDisplayName;
106         }
107 
108         @Override
hashCode()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
equals(Object obj)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
compareTo(TimezoneRow another)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      */
TimezoneAdapter(Context context, String currentTimezone)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      */
getRowById(String id)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      */
showInitialTimezones()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 = GeneralPreferences.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      */
showAllTimezones()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      */
setCurrentTimezone(String currentTimezone)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      */
saveRecentTimezone(String id)305     public void saveRecentTimezone(String id) {
306         SharedPreferences prefs = GeneralPreferences.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         Utils.setSharedPreference(mContext, KEY_RECENT_TIMEZONES, recentsString);
322     }
323 
324     /**
325      * Returns an array of ids/time zones. This returns a double indexed array
326      * of ids and time zones for Calendar. It is an inefficient method and
327      * shouldn't be called often, but can be used for one time generation of
328      * this list.
329      *
330      * @return double array of tz ids and tz names
331      */
getAllTimezones()332     public CharSequence[][] getAllTimezones() {
333         CharSequence[][] timeZones = new CharSequence[2][sTimezones.size()];
334         List<String> ids = new ArrayList<String>(sTimezones.keySet());
335         List<TimezoneRow> timezones = new ArrayList<TimezoneRow>(sTimezones.values());
336         int i = 0;
337         for (TimezoneRow row : timezones) {
338             timeZones[0][i] = ids.get(i);
339             timeZones[1][i++] = row.toString();
340         }
341         return timeZones;
342     }
343 
loadFromResources(Resources resources)344     private void loadFromResources(Resources resources) {
345         if (sTimezones == null) {
346             String[] ids = resources.getStringArray(R.array.timezone_values);
347             String[] labels = resources.getStringArray(R.array.timezone_labels);
348 
349             int length = ids.length;
350             sTimezones = new LinkedHashMap<String, TimezoneRow>(length);
351 
352             if (ids.length != labels.length) {
353                 Log.wtf(TAG, "ids length (" + ids.length + ") and labels length(" + labels.length +
354                         ") should be equal but aren't.");
355             }
356             for (int i = 0; i < length; i++) {
357                 sTimezones.put(ids[i], new TimezoneRow(ids[i], labels[i]));
358             }
359         }
360     }
361 }
362