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