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