• 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 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 
TimezoneRow(String id, String displayName)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
toString()84         public String toString() {
85             if (mGmtDisplayName == null) {
86                 buildGmtDisplayName();
87             }
88 
89             return mGmtDisplayName;
90         }
91 
92         /**
93          *
94          */
buildGmtDisplayName()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
hashCode()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
equals(Object obj)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
compareTo(TimezoneRow another)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      */
TimezoneAdapter(Context context, String currentTimezone, long time)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      */
getRowById(String id)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      */
showInitialTimezones()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      */
showAllTimezones()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      */
setCurrentTimezone(String currentTimezone)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      */
setTime(long time)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      */
saveRecentTimezone(String id)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      */
getAllTimezones()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 
loadFromResources(Resources resources)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