Home | History | Annotate | Download | only in timezonepicker
      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 
     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 
     90     public void setTime(long timeMillis) {
     91         mTimeMillis = timeMillis;
     92     }
     93 
     94     public TimeZoneInfo get(int position) {
     95         return mTimeZones.get(position);
     96     }
     97 
     98     public int size() {
     99         return mTimeZones.size();
    100     }
    101 
    102     public int getDefaultTimeZoneIndex() {
    103         return mTimeZones.indexOf(mDefaultTimeZoneInfo);
    104     }
    105 
    106     // TODO speed this up
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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