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