TimeZoneData.java revision fa1c198c60768f9beeb5694502b3dcd3b9528e0c
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 really 127 * needed and they are dups but missing proper country codes. e.g. 128 * WET CET MST7MDT PST8PDT Asia/Khandyga Asia/Ust-Nera EST 129 */ 130 if (!tzId.startsWith("Etc/GMT")) { 131 continue; 132 } 133 134 final TimeZone tz = TimeZone.getTimeZone(tzId); 135 if (tz == null) { 136 Log.e(TAG, "Timezone not found: " + tzId); 137 continue; 138 } 139 140 TimeZoneInfo tzInfo = new TimeZoneInfo(tz, null); 141 142 if (getIdenticalTimeZoneInTheCountry(tzInfo) == -1) { 143 if (DEBUG) { 144 Log.e(TAG, "# Adding time zone from getAvailId: " + tzInfo.toString()); 145 } 146 mTimeZones.add(tzInfo); 147 } else { 148 if (DEBUG) { 149 Log.e(TAG, 150 "# Dropping identical time zone from getAvailId: " + tzInfo.toString()); 151 } 152 continue; 153 } 154 // 155 // TODO check for dups 156 // checkForNameDups(tz, tzInfo.mCountry, false /* dls */, 157 // TimeZone.SHORT, groupIdx, !found); 158 // checkForNameDups(tz, tzInfo.mCountry, false /* dls */, 159 // TimeZone.LONG, groupIdx, !found); 160 // if (tz.useDaylightTime()) { 161 // checkForNameDups(tz, tzInfo.mCountry, true /* dls */, 162 // TimeZone.SHORT, groupIdx, 163 // !found); 164 // checkForNameDups(tz, tzInfo.mCountry, true /* dls */, 165 // TimeZone.LONG, groupIdx, 166 // !found); 167 // } 168 } 169 170 // Don't change the order of mTimeZones after this sort 171 Collections.sort(mTimeZones); 172 173 mTimeZonesByCountry = new LinkedHashMap<String, ArrayList<Integer>>(); 174 mTimeZonesByOffsets = new SparseArray<ArrayList<Integer>>(mHasTimeZonesInHrOffset.length); 175 mTimeZonesById = new HashMap<String, TimeZoneInfo>(mTimeZones.size()); 176 for (TimeZoneInfo tz : mTimeZones) { 177 // ///////////////////// 178 // Lookup map for id -> tz 179 mTimeZonesById.put(tz.mTzId, tz); 180 } 181 populateDisplayNameOverrides(mContext.getResources()); 182 183 Date date = new Date(mTimeMillis); 184 Locale defaultLocal = Locale.getDefault(); 185 186 int idx = 0; 187 for (TimeZoneInfo tz : mTimeZones) { 188 // ///////////////////// 189 // Populate display name 190 if (tz.mDisplayName == null) { 191 tz.mDisplayName = tz.mTz.getDisplayName(tz.mTz.inDaylightTime(date), 192 TimeZone.LONG, defaultLocal); 193 } 194 195 // ///////////////////// 196 // Grouping tz's by country for search by country 197 ArrayList<Integer> group = mTimeZonesByCountry.get(tz.mCountry); 198 if (group == null) { 199 group = new ArrayList<Integer>(); 200 mTimeZonesByCountry.put(tz.mCountry, group); 201 } 202 203 group.add(idx); 204 205 // ///////////////////// 206 // Grouping tz's by GMT offsets 207 indexByOffsets(idx, tz); 208 209 // Skip all the GMT+xx:xx style display names from search 210 if (!tz.mDisplayName.endsWith(":00")) { 211 mTimeZoneNames.add(tz.mDisplayName); 212 } else if (DEBUG) { 213 Log.e(TAG, "# Hiding from pretty name search: " + 214 tz.mDisplayName); 215 } 216 217 idx++; 218 } 219 220 // printTimeZones(); 221 } 222 223 private void printTimeZones() { 224 TimeZoneInfo last = null; 225 boolean first = true; 226 for (TimeZoneInfo tz : mTimeZones) { 227 // All 228 if (false) { 229 Log.e("ALL", tz.toString()); 230 } 231 232 // GMT 233 if (true) { 234 String name = tz.mTz.getDisplayName(); 235 if (name.startsWith("GMT") && !tz.mTzId.startsWith("Etc/GMT")) { 236 Log.e("GMT", tz.toString()); 237 } 238 } 239 240 // Dups 241 if (true && last != null) { 242 if (last.compareTo(tz) == 0) { 243 if (first) { 244 Log.e("SAME", last.toString()); 245 first = false; 246 } 247 Log.e("SAME", tz.toString()); 248 } else { 249 first = true; 250 } 251 } 252 last = tz; 253 } 254 Log.e(TAG, "Total number of tz's = " + mTimeZones.size()); 255 } 256 257 private void populateDisplayNameOverrides(Resources resources) { 258 String[] ids = resources.getStringArray(R.array.timezone_rename_ids); 259 String[] labels = resources.getStringArray(R.array.timezone_rename_labels); 260 261 int length = ids.length; 262 if (ids.length != labels.length) { 263 Log.e(TAG, "timezone_rename_ids len=" + ids.length + " timezone_rename_labels len=" 264 + labels.length); 265 length = Math.min(ids.length, labels.length); 266 } 267 268 for (int i = 0; i < length; i++) { 269 TimeZoneInfo tzi = mTimeZonesById.get(ids[i]); 270 tzi.mDisplayName = labels[i]; 271 } 272 } 273 274 public boolean hasTimeZonesInHrOffset(int offsetHr) { 275 int index = OFFSET_ARRAY_OFFSET + offsetHr; 276 if (index >= mHasTimeZonesInHrOffset.length || index < 0) { 277 return false; 278 } 279 return mHasTimeZonesInHrOffset[index]; 280 } 281 282 private void indexByOffsets(int idx, TimeZoneInfo tzi) { 283 int offsetMillis = tzi.getNowOffsetMillis(); 284 int index = OFFSET_ARRAY_OFFSET + (int) (offsetMillis / DateUtils.HOUR_IN_MILLIS); 285 mHasTimeZonesInHrOffset[index] = true; 286 287 ArrayList<Integer> group = mTimeZonesByOffsets.get(index); 288 if (group == null) { 289 group = new ArrayList<Integer>(); 290 mTimeZonesByOffsets.put(index, group); 291 } 292 group.add(idx); 293 } 294 295 public ArrayList<Integer> getTimeZonesByOffset(int offsetHr) { 296 int index = OFFSET_ARRAY_OFFSET + offsetHr; 297 if (index >= mHasTimeZonesInHrOffset.length || index < 0) { 298 return null; 299 } 300 return mTimeZonesByOffsets.get(index); 301 } 302 303 private HashSet<String> loadTzsInZoneTab(Context context) { 304 HashSet<String> processedTimeZones = new HashSet<String>(); 305 AssetManager am = context.getAssets(); 306 InputStream is = null; 307 308 /* 309 * The 'backward' file contain mappings between new and old time zone 310 * ids. We will explicitly ignore the old ones. 311 */ 312 try { 313 is = am.open("backward"); 314 BufferedReader reader = new BufferedReader(new InputStreamReader(is)); 315 String line; 316 317 while ((line = reader.readLine()) != null) { 318 // Skip comment lines 319 if (!line.startsWith("#") && line.length() > 0) { 320 // 0: "Link" 321 // 1: New tz id 322 // Last: Old tz id 323 String[] fields = line.split("\t+"); 324 String newTzId = fields[1]; 325 String oldTzId = fields[fields.length - 1]; 326 327 final TimeZone tz = TimeZone.getTimeZone(newTzId); 328 if (tz == null) { 329 Log.e(TAG, "Timezone not found: " + newTzId); 330 continue; 331 } 332 333 processedTimeZones.add(oldTzId); 334 335 if (DEBUG) { 336 Log.e(TAG, "# Dropping identical time zone from backward: " + oldTzId); 337 } 338 339 // Remember the cooler/newer time zone id 340 if (mDefaultTimeZoneId != null && mDefaultTimeZoneId.equals(oldTzId)) { 341 mAlternateDefaultTimeZoneId = newTzId; 342 } 343 } 344 } 345 } catch (IOException ex) { 346 Log.e(TAG, "Failed to read 'backward' file."); 347 } finally { 348 try { 349 if (is != null) { 350 is.close(); 351 } 352 } catch (IOException ignored) { 353 } 354 } 355 356 /* 357 * zone.tab contains a list of time zones and country code. They are 358 * "sorted first by country, then an order within the country that (1) 359 * makes some geographical sense, and (2) puts the most populous zones 360 * first, where that does not contradict (1)." 361 */ 362 try { 363 String lang = Locale.getDefault().getLanguage(); 364 is = am.open("zone.tab"); 365 BufferedReader reader = new BufferedReader(new InputStreamReader(is)); 366 String line; 367 while ((line = reader.readLine()) != null) { 368 if (!line.startsWith("#")) { // Skip comment lines 369 // 0: country code 370 // 1: coordinates 371 // 2: time zone id 372 // 3: comments 373 final String[] fields = line.split("\t"); 374 final String timeZoneId = fields[2]; 375 final String countryCode = fields[0]; 376 final TimeZone tz = TimeZone.getTimeZone(timeZoneId); 377 if (tz == null) { 378 Log.e(TAG, "Timezone not found: " + timeZoneId); 379 continue; 380 } 381 382 /* 383 * Dropping non-GMT tzs without a country code. They are not 384 * really needed and they are dups but missing proper 385 * country codes. e.g. WET CET MST7MDT PST8PDT Asia/Khandyga 386 * Asia/Ust-Nera EST 387 */ 388 if (countryCode == null && !timeZoneId.startsWith("Etc/GMT")) { 389 processedTimeZones.add(timeZoneId); 390 continue; 391 } 392 393 // Remember the mapping between the country code and display 394 // name 395 String country = mCountryCodeToNameMap.get(countryCode); 396 if (country == null) { 397 country = getCountryNames(lang, countryCode); 398 mCountryCodeToNameMap.put(countryCode, country); 399 } 400 401 // TODO Don't like this here but need to get the country of 402 // the default tz. 403 404 // Find the country of the default tz 405 if (mDefaultTimeZoneId != null && mDefaultTimeZoneCountry == null 406 && timeZoneId.equals(mAlternateDefaultTimeZoneId)) { 407 mDefaultTimeZoneCountry = country; 408 TimeZone defaultTz = TimeZone.getTimeZone(mDefaultTimeZoneId); 409 if (defaultTz != null) { 410 mDefaultTimeZoneInfo = new TimeZoneInfo(defaultTz, country); 411 412 int tzToOverride = getIdenticalTimeZoneInTheCountry(mDefaultTimeZoneInfo); 413 if (tzToOverride == -1) { 414 if (DEBUG) { 415 Log.e(TAG, "Adding default time zone: " 416 + mDefaultTimeZoneInfo.toString()); 417 } 418 mTimeZones.add(mDefaultTimeZoneInfo); 419 } else { 420 mTimeZones.add(tzToOverride, mDefaultTimeZoneInfo); 421 if (DEBUG) { 422 TimeZoneInfo tzInfoToOverride = mTimeZones.get(tzToOverride); 423 String tzIdToOverride = tzInfoToOverride.mTzId; 424 Log.e(TAG, "Replaced by default tz: " 425 + tzInfoToOverride.toString()); 426 Log.e(TAG, "Adding default time zone: " 427 + mDefaultTimeZoneInfo.toString()); 428 } 429 } 430 } 431 } 432 433 // Add to the list of time zones if the time zone is unique 434 // in the given country. 435 TimeZoneInfo timeZoneInfo = new TimeZoneInfo(tz, country); 436 int identicalTzIdx = getIdenticalTimeZoneInTheCountry(timeZoneInfo); 437 if (identicalTzIdx == -1) { 438 if (DEBUG) { 439 Log.e(TAG, "# Adding time zone: " + timeZoneId + " ## " + 440 tz.getDisplayName()); 441 } 442 mTimeZones.add(timeZoneInfo); 443 } else { 444 if (DEBUG) { 445 Log.e(TAG, "# Dropping identical time zone: " + timeZoneId + " ## " + 446 tz.getDisplayName()); 447 } 448 } 449 processedTimeZones.add(timeZoneId); 450 } 451 } 452 453 } catch (IOException ex) { 454 Log.e(TAG, "Failed to read 'zone.tab'."); 455 } finally { 456 try { 457 if (is != null) { 458 is.close(); 459 } 460 } catch (IOException ignored) { 461 } 462 } 463 464 return processedTimeZones; 465 } 466 467 @SuppressWarnings("unused") 468 private static Locale mBackupCountryLocale; 469 private static String[] mBackupCountryCodes; 470 private static String[] mBackupCountryNames; 471 472 private String getCountryNames(String lang, String countryCode) { 473 final Locale defaultLocale = Locale.getDefault(); 474 String countryDisplayName = new Locale(lang, countryCode).getDisplayCountry(defaultLocale); 475 476 if (!countryCode.equals(countryDisplayName)) { 477 return countryDisplayName; 478 } 479 480 if (mBackupCountryCodes == null || !defaultLocale.equals(mBackupCountryLocale)) { 481 mBackupCountryLocale = defaultLocale; 482 mBackupCountryCodes = mContext.getResources().getStringArray( 483 R.array.backup_country_codes); 484 mBackupCountryNames = mContext.getResources().getStringArray( 485 R.array.backup_country_names); 486 } 487 488 int length = Math.min(mBackupCountryCodes.length, mBackupCountryNames.length); 489 490 for (int i = 0; i < length; i++) { 491 if (mBackupCountryCodes[i].equals(countryCode)) { 492 return mBackupCountryNames[i]; 493 } 494 } 495 496 return countryCode; 497 } 498 499 private int getIdenticalTimeZoneInTheCountry(TimeZoneInfo timeZoneInfo) { 500 int idx = 0; 501 for (TimeZoneInfo tzi : mTimeZones) { 502 if (tzi.hasSameRules(timeZoneInfo)) { 503 if (tzi.mCountry == null) { 504 if (timeZoneInfo.mCountry == null) { 505 return idx; 506 } 507 } else if (tzi.mCountry.equals(timeZoneInfo.mCountry)) { 508 return idx; 509 } 510 } 511 ++idx; 512 } 513 return -1; 514 } 515} 516