TimeZoneData.java revision b506b1dddd4007997bbe6773cb0ebced27ba96df
1/* 2 * Copyright (C) 2013 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 17package com.android.timezonepicker; 18 19import android.content.Context; 20import android.content.res.AssetManager; 21import android.content.res.Resources; 22import android.text.format.DateFormat; 23import android.text.format.DateUtils; 24import android.util.Log; 25import android.util.SparseArray; 26 27import java.io.BufferedReader; 28import java.io.IOException; 29import java.io.InputStream; 30import java.io.InputStreamReader; 31import java.util.ArrayList; 32import java.util.Collections; 33import java.util.Date; 34import java.util.HashMap; 35import java.util.HashSet; 36import java.util.LinkedHashMap; 37import java.util.Locale; 38import java.util.TimeZone; 39 40public class TimeZoneData { 41 private static final String TAG = "TimeZoneData"; 42 private static final boolean DEBUG = false; 43 private static final int OFFSET_ARRAY_OFFSET = 20; 44 45 ArrayList<TimeZoneInfo> mTimeZones; 46 LinkedHashMap<String, ArrayList<Integer>> mTimeZonesByCountry; 47 HashSet<String> mTimeZoneNames = new HashSet<String>(); 48 49 private long mTimeMillis; 50 private HashMap<String, String> mCountryCodeToNameMap = new HashMap<String, String>(); 51 52 public String mDefaultTimeZoneId; 53 public static boolean is24HourFormat; 54 private TimeZoneInfo mDefaultTimeZoneInfo; 55 private String mAlternateDefaultTimeZoneId; 56 private String mDefaultTimeZoneCountry; 57 private HashMap<String, TimeZoneInfo> mTimeZonesById; 58 private boolean[] mHasTimeZonesInHrOffset = new boolean[40]; 59 SparseArray<ArrayList<Integer>> mTimeZonesByOffsets; 60 private Context mContext; 61 62 public TimeZoneData(Context context, String defaultTimeZoneId, long timeMillis) { 63 mContext = context; 64 is24HourFormat = TimeZoneInfo.is24HourFormat = DateFormat.is24HourFormat(context); 65 mDefaultTimeZoneId = mAlternateDefaultTimeZoneId = defaultTimeZoneId; 66 long now = System.currentTimeMillis(); 67 68 if (timeMillis == 0) { 69 mTimeMillis = now; 70 } else { 71 mTimeMillis = timeMillis; 72 } 73 loadTzs(context); 74 75 Log.i(TAG, "Time to load time zones (ms): " + (System.currentTimeMillis() - now)); 76 77 // now = System.currentTimeMillis(); 78 // printTz(); 79 // Log.i(TAG, "Time to print time zones (ms): " + 80 // (System.currentTimeMillis() - now)); 81 } 82 83 public void setTime(long timeMillis) { 84 mTimeMillis = timeMillis; 85 } 86 87 public TimeZoneInfo get(int position) { 88 return mTimeZones.get(position); 89 } 90 91 public int size() { 92 return mTimeZones.size(); 93 } 94 95 public int getDefaultTimeZoneIndex() { 96 return mTimeZones.indexOf(mDefaultTimeZoneInfo); 97 } 98 99 // TODO speed this up 100 public int findIndexByTimeZoneIdSlow(String timeZoneId) { 101 int idx = 0; 102 for (TimeZoneInfo tzi : mTimeZones) { 103 if (timeZoneId.equals(tzi.mTzId)) { 104 return idx; 105 } 106 idx++; 107 } 108 return -1; 109 } 110 111 void loadTzs(Context context) { 112 mTimeZones = new ArrayList<TimeZoneInfo>(); 113 HashSet<String> processedTimeZones = loadTzsInZoneTab(context); 114 String[] tzIds = TimeZone.getAvailableIDs(); 115 116 if (DEBUG) { 117 Log.e(TAG, "Available time zones: " + tzIds.length); 118 } 119 120 for (String tzId : tzIds) { 121 if (processedTimeZones.contains(tzId)) { 122 continue; 123 } 124 125 /* 126 * Dropping non-GMT tzs without a country code. They are not 127 * really needed and they are dups but missing proper 128 * country codes. e.g. WET CET MST7MDT PST8PDT Asia/Khandyga 129 * Asia/Ust-Nera EST 130 */ 131 if (!tzId.startsWith("Etc/GMT")) { 132 continue; 133 } 134 135 final TimeZone tz = TimeZone.getTimeZone(tzId); 136 if (tz == null) { 137 Log.e(TAG, "Timezone not found: " + tzId); 138 continue; 139 } 140 141 TimeZoneInfo tzInfo = new TimeZoneInfo(tz, null); 142 143 if (getIdenticalTimeZoneInTheCountry(tzInfo) == -1) { 144 if (DEBUG) { 145 Log.e(TAG, "# Adding time zone from getAvailId: " + tzInfo.toString()); 146 } 147 mTimeZones.add(tzInfo); 148 } else { 149 if (DEBUG) { 150 Log.e(TAG, 151 "# Dropping identical time zone from getAvailId: " + tzInfo.toString()); 152 } 153 continue; 154 } 155 // 156 // TODO check for dups 157 // checkForNameDups(tz, tzInfo.mCountry, false /* dls */, 158 // TimeZone.SHORT, groupIdx, !found); 159 // checkForNameDups(tz, tzInfo.mCountry, false /* dls */, 160 // TimeZone.LONG, groupIdx, !found); 161 // if (tz.useDaylightTime()) { 162 // checkForNameDups(tz, tzInfo.mCountry, true /* dls */, 163 // TimeZone.SHORT, groupIdx, 164 // !found); 165 // checkForNameDups(tz, tzInfo.mCountry, true /* dls */, 166 // TimeZone.LONG, groupIdx, 167 // !found); 168 // } 169 } 170 171 // Don't change the order of mTimeZones after this sort 172 Collections.sort(mTimeZones); 173 // TimeZoneInfo last = null; 174 // boolean first = true; 175 // for (TimeZoneInfo tz : mTimeZones) { 176 // // All 177 // Log.e("ALL", tz.toString()); 178 // 179 // // GMT 180 // String name = tz.mTz.getDisplayName(); 181 // if (name.startsWith("GMT") && !tz.mTzId.startsWith("Etc/GMT")) { 182 // Log.e("GMT", tz.toString()); 183 // } 184 // 185 // // Dups 186 // if (last != null) { 187 // if (last.compareTo(tz) == 0) { 188 // if (first) { 189 // Log.e("SAME", last.toString()); 190 // first = false; 191 // } 192 // Log.e("SAME", tz.toString()); 193 // } else { 194 // first = true; 195 // } 196 // } 197 // last = tz; 198 // } 199 200 mTimeZonesByCountry = new LinkedHashMap<String, ArrayList<Integer>>(); 201 mTimeZonesByOffsets = new SparseArray<ArrayList<Integer>>(mHasTimeZonesInHrOffset.length); 202 mTimeZonesById = new HashMap<String, TimeZoneInfo>(mTimeZones.size()); 203 for (TimeZoneInfo tz : mTimeZones) { 204 // ///////////////////// 205 // Lookup map for id -> tz 206 mTimeZonesById.put(tz.mTzId, tz); 207 } 208 populateDisplayNameOverrides(mContext.getResources()); 209 210 Date date = new Date(mTimeMillis); 211 Locale defaultLocal = Locale.getDefault(); 212 213 int idx = 0; 214 for (TimeZoneInfo tz : mTimeZones) { 215 // ///////////////////// 216 // Populate display name 217 if (tz.mDisplayName == null) { 218 tz.mDisplayName = tz.mTz.getDisplayName(tz.mTz.inDaylightTime(date), 219 TimeZone.LONG, defaultLocal); 220 } 221 222 // ///////////////////// 223 // Grouping tz's by country for search by country 224 ArrayList<Integer> group = mTimeZonesByCountry.get(tz.mCountry); 225 if (group == null) { 226 group = new ArrayList<Integer>(); 227 mTimeZonesByCountry.put(tz.mCountry, group); 228 } 229 230 group.add(idx); 231 232 // ///////////////////// 233 // Grouping tz's by GMT offsets 234 indexByOffsets(idx, tz); 235 236 // Skip all the GMT+xx:xx style display names from search 237 if (!tz.mDisplayName.endsWith(":00")) { 238 mTimeZoneNames.add(tz.mDisplayName); 239 } else if (DEBUG) { 240 Log.e(TAG, "# Hiding from pretty name search: " + 241 tz.mDisplayName); 242 } 243 244 idx++; 245 } 246 } 247 248 private void populateDisplayNameOverrides(Resources resources) { 249 String[] ids = resources.getStringArray(R.array.timezone_rename_ids); 250 String[] labels = resources.getStringArray(R.array.timezone_rename_labels); 251 252 int length = ids.length; 253 if (ids.length != labels.length) { 254 Log.e(TAG, "timezone_rename_ids len=" + ids.length + " timezone_rename_labels len=" 255 + labels.length); 256 length = Math.min(ids.length, labels.length); 257 } 258 259 for (int i = 0; i < length; i++) { 260 TimeZoneInfo tzi = mTimeZonesById.get(ids[i]); 261 tzi.mDisplayName = labels[i]; 262 } 263 } 264 265 public boolean hasTimeZonesInHrOffset(int offsetHr) { 266 int index = OFFSET_ARRAY_OFFSET + offsetHr; 267 if (index >= mHasTimeZonesInHrOffset.length || index < 0) { 268 return false; 269 } 270 return mHasTimeZonesInHrOffset[index]; 271 } 272 273 private void indexByOffsets(int idx, TimeZoneInfo tzi) { 274 int offsetMillis = tzi.getNowOffsetMillis(); 275 int index = OFFSET_ARRAY_OFFSET + (int) (offsetMillis / DateUtils.HOUR_IN_MILLIS); 276 mHasTimeZonesInHrOffset[index] = true; 277 278 ArrayList<Integer> group = mTimeZonesByOffsets.get(index); 279 if (group == null) { 280 group = new ArrayList<Integer>(); 281 mTimeZonesByOffsets.put(index, group); 282 } 283 group.add(idx); 284 } 285 286 public ArrayList<Integer> getTimeZonesByOffset(int offsetHr) { 287 int index = OFFSET_ARRAY_OFFSET + offsetHr; 288 if (index >= mHasTimeZonesInHrOffset.length || index < 0) { 289 return null; 290 } 291 return mTimeZonesByOffsets.get(index); 292 } 293 294 private HashSet<String> loadTzsInZoneTab(Context context) { 295 HashSet<String> processedTimeZones = new HashSet<String>(); 296 AssetManager am = context.getAssets(); 297 InputStream is = null; 298 299 /* 300 * The 'backward' file contain mappings between new and old time zone 301 * ids. We will explicitly ignore the old ones. 302 */ 303 try { 304 is = am.open("backward"); 305 BufferedReader reader = new BufferedReader(new InputStreamReader(is)); 306 String line; 307 308 while ((line = reader.readLine()) != null) { 309 // Skip comment lines 310 if (!line.startsWith("#") && line.length() > 0) { 311 // 0: "Link" 312 // 1: New tz id 313 // Last: Old tz id 314 String[] fields = line.split("\t+"); 315 String newTzId = fields[1]; 316 String oldTzId = fields[fields.length - 1]; 317 318 final TimeZone tz = TimeZone.getTimeZone(newTzId); 319 if (tz == null) { 320 Log.e(TAG, "Timezone not found: " + newTzId); 321 continue; 322 } 323 324 processedTimeZones.add(oldTzId); 325 326 if (DEBUG) { 327 Log.e(TAG, "# Dropping identical time zone from backward: " + oldTzId); 328 } 329 330 // Remember the cooler/newer time zone id 331 if (mDefaultTimeZoneId != null && mDefaultTimeZoneId.equals(oldTzId)) { 332 mAlternateDefaultTimeZoneId = newTzId; 333 } 334 } 335 } 336 } catch (IOException ex) { 337 Log.e(TAG, "Failed to read 'backward' file."); 338 } finally { 339 try { 340 if (is != null) { 341 is.close(); 342 } 343 } catch (IOException ignored) { 344 } 345 } 346 347 /* 348 * zone.tab contains a list of time zones and country code. They are 349 * "sorted first by country, then an order within the country that (1) 350 * makes some geographical sense, and (2) puts the most populous zones 351 * first, where that does not contradict (1)." 352 */ 353 try { 354 String lang = Locale.getDefault().getLanguage(); 355 is = am.open("zone.tab"); 356 BufferedReader reader = new BufferedReader(new InputStreamReader(is)); 357 String line; 358 while ((line = reader.readLine()) != null) { 359 if (!line.startsWith("#")) { // Skip comment lines 360 // 0: country code 361 // 1: coordinates 362 // 2: time zone id 363 // 3: comments 364 final String[] fields = line.split("\t"); 365 final String timeZoneId = fields[2]; 366 final String countryCode = fields[0]; 367 final TimeZone tz = TimeZone.getTimeZone(timeZoneId); 368 if (tz == null) { 369 Log.e(TAG, "Timezone not found: " + timeZoneId); 370 continue; 371 } 372 373 /* 374 * Dropping non-GMT tzs without a country code. They are not 375 * really needed and they are dups but missing proper 376 * country codes. e.g. WET CET MST7MDT PST8PDT Asia/Khandyga 377 * Asia/Ust-Nera EST 378 */ 379 if (countryCode == null && !timeZoneId.startsWith("Etc/GMT")) { 380 processedTimeZones.add(timeZoneId); 381 continue; 382 } 383 384 // Remember the mapping between the country code and display 385 // name 386 String country = mCountryCodeToNameMap.get(countryCode); 387 if (country == null) { 388 country = getCountryNames(lang, countryCode); 389 mCountryCodeToNameMap.put(countryCode, country); 390 } 391 392 // TODO Don't like this here but need to get the country of 393 // the default tz. 394 395 // Find the country of the default tz 396 if (mDefaultTimeZoneId != null && mDefaultTimeZoneCountry == null 397 && timeZoneId.equals(mAlternateDefaultTimeZoneId)) { 398 mDefaultTimeZoneCountry = country; 399 TimeZone defaultTz = TimeZone.getTimeZone(mDefaultTimeZoneId); 400 if (defaultTz != null) { 401 mDefaultTimeZoneInfo = new TimeZoneInfo(defaultTz, country); 402 403 int tzToOverride = getIdenticalTimeZoneInTheCountry(mDefaultTimeZoneInfo); 404 if (tzToOverride == -1) { 405 if (DEBUG) { 406 Log.e(TAG, "Adding default time zone: " 407 + mDefaultTimeZoneInfo.toString()); 408 } 409 mTimeZones.add(mDefaultTimeZoneInfo); 410 } else { 411 mTimeZones.add(tzToOverride, mDefaultTimeZoneInfo); 412 if (DEBUG) { 413 TimeZoneInfo tzInfoToOverride = mTimeZones.get(tzToOverride); 414 String tzIdToOverride = tzInfoToOverride.mTzId; 415 Log.e(TAG, "Replaced by default tz: " 416 + tzInfoToOverride.toString()); 417 Log.e(TAG, "Adding default time zone: " 418 + mDefaultTimeZoneInfo.toString()); 419 } 420 } 421 } 422 } 423 424 // Add to the list of time zones if the time zone is unique 425 // in the given country. 426 TimeZoneInfo timeZoneInfo = new TimeZoneInfo(tz, country); 427 int identicalTzIdx = getIdenticalTimeZoneInTheCountry(timeZoneInfo); 428 if (identicalTzIdx == -1) { 429 if (DEBUG) { 430 Log.e(TAG, "# Adding time zone: " + timeZoneId + " ## " + 431 tz.getDisplayName()); 432 } 433 mTimeZones.add(timeZoneInfo); 434 } else { 435 if (DEBUG) { 436 Log.e(TAG, "# Dropping identical time zone: " + timeZoneId + " ## " + 437 tz.getDisplayName()); 438 } 439 } 440 processedTimeZones.add(timeZoneId); 441 } 442 } 443 444 } catch (IOException ex) { 445 Log.e(TAG, "Failed to read 'zone.tab'."); 446 } finally { 447 try { 448 if (is != null) { 449 is.close(); 450 } 451 } catch (IOException ignored) { 452 } 453 } 454 455 return processedTimeZones; 456 } 457 458 @SuppressWarnings("unused") 459 private static Locale mBackupCountryLocale; 460 private static String[] mBackupCountryCodes; 461 private static String[] mBackupCountryNames; 462 463 private String getCountryNames(String lang, String countryCode) { 464 final Locale defaultLocale = Locale.getDefault(); 465 String countryDisplayName = new Locale(lang, countryCode).getDisplayCountry(defaultLocale); 466 467 if (!countryCode.equals(countryDisplayName)) { 468 return countryDisplayName; 469 } 470 471 if (mBackupCountryCodes == null || !defaultLocale.equals(mBackupCountryLocale)) { 472 mBackupCountryLocale = defaultLocale; 473 mBackupCountryCodes = mContext.getResources().getStringArray( 474 R.array.backup_country_codes); 475 mBackupCountryNames = mContext.getResources().getStringArray( 476 R.array.backup_country_names); 477 } 478 479 int length = Math.min(mBackupCountryCodes.length, mBackupCountryNames.length); 480 481 for (int i = 0; i < length; i++) { 482 if (mBackupCountryCodes[i].equals(countryCode)) { 483 return mBackupCountryNames[i]; 484 } 485 } 486 487 return countryCode; 488 } 489 490 private int getIdenticalTimeZoneInTheCountry(TimeZoneInfo timeZoneInfo) { 491 int idx = 0; 492 for (TimeZoneInfo tzi : mTimeZones) { 493 if (tzi.hasSameRules(timeZoneInfo)) { 494 if (tzi.mCountry == null) { 495 if (timeZoneInfo.mCountry == null) { 496 return idx; 497 } 498 } else if (tzi.mCountry.equals(timeZoneInfo.mCountry)) { 499 return idx; 500 } 501 } 502 ++idx; 503 } 504 return -1; 505 } 506 507 private void printTz() { 508 for (TimeZoneInfo tz : mTimeZones) { 509 Log.e(TAG, "" + tz.toString()); 510 } 511 512 Log.e(TAG, "Total number of tz's = " + mTimeZones.size()); 513 } 514 515 // void checkForNameDups(TimeZone tz, String country, boolean dls, int 516 // style, int idx, 517 // boolean print) { 518 // if (country == null) { 519 // return; 520 // } 521 // String displayName = tz.getDisplayName(dls, style); 522 // 523 // if (print) { 524 // Log.e(TAG, "" + idx + " " + tz.getID() + " " + country + " ## " + 525 // displayName); 526 // } 527 // 528 // if (tz.useDaylightTime()) { 529 // if (displayName.matches("GMT[+-][0-9][0-9]:[0-9][0-9]")) { 530 // return; 531 // } 532 // 533 // if (displayName.length() == 3 && displayName.charAt(2) == 'T' && 534 // (displayName.charAt(1) == 'S' || displayName.charAt(1) == 'D')) { 535 // displayName = "" + displayName.charAt(0) + 'T'; 536 // } else { 537 // displayName = displayName.replace(" Daylight ", 538 // " ").replace(" Standard ", " "); 539 // } 540 // } 541 // 542 // String tzNameWithCountry = country + " ## " + displayName; 543 // Integer groupId = mCountryPlusTzName2Tzs.get(tzNameWithCountry); 544 // if (groupId == null) { 545 // mCountryPlusTzName2Tzs.put(tzNameWithCountry, idx); 546 // } else if (groupId != idx) { 547 // Log.e(TAG, "Yikes: " + tzNameWithCountry + " matches " + groupId + 548 // " and " + idx); 549 // } 550 // } 551 552} 553