• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
TimeZoneData(Context context, String defaultTimeZoneId, long timeMillis)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 
setTime(long timeMillis)90     public void setTime(long timeMillis) {
91         mTimeMillis = timeMillis;
92     }
93 
get(int position)94     public TimeZoneInfo get(int position) {
95         return mTimeZones.get(position);
96     }
97 
size()98     public int size() {
99         return mTimeZones.size();
100     }
101 
getDefaultTimeZoneIndex()102     public int getDefaultTimeZoneIndex() {
103         return mTimeZones.indexOf(mDefaultTimeZoneInfo);
104     }
105 
106     // TODO speed this up
findIndexByTimeZoneIdSlow(String timeZoneId)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 
loadTzs(Context context)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 
printTimeZones()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 
populateDisplayNameOverrides(Resources resources)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 
hasTimeZonesInHrOffset(int offsetHr)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 
indexByOffsets(int idx, TimeZoneInfo tzi)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 
getTimeZonesByOffset(int offsetHr)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 
loadTzsInZoneTab(Context context)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 
getCountryNames(String lang, String countryCode)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 
getIdenticalTimeZoneInTheCountry(TimeZoneInfo timeZoneInfo)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