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