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