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