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