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