blob: 9302a3cebe1573edf2ca942a476bfac0027aeab1 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.timezonepicker;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.SparseArray;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.TimeZone;
public class TimeZoneData {
private static final String TAG = "TimeZoneData";
private static final boolean DEBUG = false;
private static final int OFFSET_ARRAY_OFFSET = 20;
private static final String PALESTINE_COUNTRY_CODE = "PS";
ArrayList<TimeZoneInfo> mTimeZones;
LinkedHashMap<String, ArrayList<Integer>> mTimeZonesByCountry;
HashSet<String> mTimeZoneNames = new HashSet<String>();
private long mTimeMillis;
private HashMap<String, String> mCountryCodeToNameMap = new HashMap<String, String>();
public String mDefaultTimeZoneId;
public static boolean is24HourFormat;
private TimeZoneInfo mDefaultTimeZoneInfo;
private String mAlternateDefaultTimeZoneId;
private String mDefaultTimeZoneCountry;
private HashMap<String, TimeZoneInfo> mTimeZonesById;
private boolean[] mHasTimeZonesInHrOffset = new boolean[40];
SparseArray<ArrayList<Integer>> mTimeZonesByOffsets;
private Context mContext;
private String mPalestineDisplayName;
public TimeZoneData(Context context, String defaultTimeZoneId, long timeMillis) {
mContext = context;
is24HourFormat = TimeZoneInfo.is24HourFormat = DateFormat.is24HourFormat(context);
mDefaultTimeZoneId = mAlternateDefaultTimeZoneId = defaultTimeZoneId;
long now = System.currentTimeMillis();
if (timeMillis == 0) {
mTimeMillis = now;
} else {
mTimeMillis = timeMillis;
}
mPalestineDisplayName = context.getResources().getString(R.string.palestine_display_name);
loadTzs(context);
Log.i(TAG, "Time to load time zones (ms): " + (System.currentTimeMillis() - now));
// now = System.currentTimeMillis();
// printTz();
// Log.i(TAG, "Time to print time zones (ms): " +
// (System.currentTimeMillis() - now));
}
public void setTime(long timeMillis) {
mTimeMillis = timeMillis;
}
public TimeZoneInfo get(int position) {
return mTimeZones.get(position);
}
public int size() {
return mTimeZones.size();
}
public int getDefaultTimeZoneIndex() {
return mTimeZones.indexOf(mDefaultTimeZoneInfo);
}
// TODO speed this up
public int findIndexByTimeZoneIdSlow(String timeZoneId) {
int idx = 0;
for (TimeZoneInfo tzi : mTimeZones) {
if (timeZoneId.equals(tzi.mTzId)) {
return idx;
}
idx++;
}
return -1;
}
void loadTzs(Context context) {
mTimeZones = new ArrayList<TimeZoneInfo>();
HashSet<String> processedTimeZones = loadTzsInZoneTab(context);
String[] tzIds = TimeZone.getAvailableIDs();
if (DEBUG) {
Log.e(TAG, "Available time zones: " + tzIds.length);
}
for (String tzId : tzIds) {
if (processedTimeZones.contains(tzId)) {
continue;
}
/*
* Dropping non-GMT tzs without a country code. They are not really
* needed and they are dups but missing proper country codes. e.g.
* WET CET MST7MDT PST8PDT Asia/Khandyga Asia/Ust-Nera EST
*/
if (!tzId.startsWith("Etc/GMT")) {
continue;
}
final TimeZone tz = TimeZone.getTimeZone(tzId);
if (tz == null) {
Log.e(TAG, "Timezone not found: " + tzId);
continue;
}
TimeZoneInfo tzInfo = new TimeZoneInfo(tz, null);
if (getIdenticalTimeZoneInTheCountry(tzInfo) == -1) {
if (DEBUG) {
Log.e(TAG, "# Adding time zone from getAvailId: " + tzInfo.toString());
}
mTimeZones.add(tzInfo);
} else {
if (DEBUG) {
Log.e(TAG,
"# Dropping identical time zone from getAvailId: " + tzInfo.toString());
}
continue;
}
//
// TODO check for dups
// checkForNameDups(tz, tzInfo.mCountry, false /* dls */,
// TimeZone.SHORT, groupIdx, !found);
// checkForNameDups(tz, tzInfo.mCountry, false /* dls */,
// TimeZone.LONG, groupIdx, !found);
// if (tz.useDaylightTime()) {
// checkForNameDups(tz, tzInfo.mCountry, true /* dls */,
// TimeZone.SHORT, groupIdx,
// !found);
// checkForNameDups(tz, tzInfo.mCountry, true /* dls */,
// TimeZone.LONG, groupIdx,
// !found);
// }
}
// Don't change the order of mTimeZones after this sort
Collections.sort(mTimeZones);
mTimeZonesByCountry = new LinkedHashMap<String, ArrayList<Integer>>();
mTimeZonesByOffsets = new SparseArray<ArrayList<Integer>>(mHasTimeZonesInHrOffset.length);
mTimeZonesById = new HashMap<String, TimeZoneInfo>(mTimeZones.size());
for (TimeZoneInfo tz : mTimeZones) {
// /////////////////////
// Lookup map for id -> tz
mTimeZonesById.put(tz.mTzId, tz);
}
populateDisplayNameOverrides(mContext.getResources());
Date date = new Date(mTimeMillis);
Locale defaultLocal = Locale.getDefault();
int idx = 0;
for (TimeZoneInfo tz : mTimeZones) {
// /////////////////////
// Populate display name
if (tz.mDisplayName == null) {
tz.mDisplayName = tz.mTz.getDisplayName(tz.mTz.inDaylightTime(date),
TimeZone.LONG, defaultLocal);
}
// /////////////////////
// Grouping tz's by country for search by country
ArrayList<Integer> group = mTimeZonesByCountry.get(tz.mCountry);
if (group == null) {
group = new ArrayList<Integer>();
mTimeZonesByCountry.put(tz.mCountry, group);
}
group.add(idx);
// /////////////////////
// Grouping tz's by GMT offsets
indexByOffsets(idx, tz);
// Skip all the GMT+xx:xx style display names from search
if (!tz.mDisplayName.endsWith(":00")) {
mTimeZoneNames.add(tz.mDisplayName);
} else if (DEBUG) {
Log.e(TAG, "# Hiding from pretty name search: " +
tz.mDisplayName);
}
idx++;
}
// printTimeZones();
}
private void printTimeZones() {
TimeZoneInfo last = null;
boolean first = true;
for (TimeZoneInfo tz : mTimeZones) {
// All
if (false) {
Log.e("ALL", tz.toString());
}
// GMT
if (true) {
String name = tz.mTz.getDisplayName();
if (name.startsWith("GMT") && !tz.mTzId.startsWith("Etc/GMT")) {
Log.e("GMT", tz.toString());
}
}
// Dups
if (true && last != null) {
if (last.compareTo(tz) == 0) {
if (first) {
Log.e("SAME", last.toString());
first = false;
}
Log.e("SAME", tz.toString());
} else {
first = true;
}
}
last = tz;
}
Log.e(TAG, "Total number of tz's = " + mTimeZones.size());
}
private void populateDisplayNameOverrides(Resources resources) {
String[] ids = resources.getStringArray(R.array.timezone_rename_ids);
String[] labels = resources.getStringArray(R.array.timezone_rename_labels);
int length = ids.length;
if (ids.length != labels.length) {
Log.e(TAG, "timezone_rename_ids len=" + ids.length + " timezone_rename_labels len="
+ labels.length);
length = Math.min(ids.length, labels.length);
}
for (int i = 0; i < length; i++) {
TimeZoneInfo tzi = mTimeZonesById.get(ids[i]);
if (tzi != null) {
tzi.mDisplayName = labels[i];
} else {
Log.e(TAG, "Could not find timezone with label: "+labels[i]);
}
}
}
public boolean hasTimeZonesInHrOffset(int offsetHr) {
int index = OFFSET_ARRAY_OFFSET + offsetHr;
if (index >= mHasTimeZonesInHrOffset.length || index < 0) {
return false;
}
return mHasTimeZonesInHrOffset[index];
}
private void indexByOffsets(int idx, TimeZoneInfo tzi) {
int offsetMillis = tzi.getNowOffsetMillis();
int index = OFFSET_ARRAY_OFFSET + (int) (offsetMillis / DateUtils.HOUR_IN_MILLIS);
mHasTimeZonesInHrOffset[index] = true;
ArrayList<Integer> group = mTimeZonesByOffsets.get(index);
if (group == null) {
group = new ArrayList<Integer>();
mTimeZonesByOffsets.put(index, group);
}
group.add(idx);
}
public ArrayList<Integer> getTimeZonesByOffset(int offsetHr) {
int index = OFFSET_ARRAY_OFFSET + offsetHr;
if (index >= mHasTimeZonesInHrOffset.length || index < 0) {
return null;
}
return mTimeZonesByOffsets.get(index);
}
private HashSet<String> loadTzsInZoneTab(Context context) {
HashSet<String> processedTimeZones = new HashSet<String>();
AssetManager am = context.getAssets();
InputStream is = null;
/*
* The 'backward' file contain mappings between new and old time zone
* ids. We will explicitly ignore the old ones.
*/
try {
is = am.open("backward");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = reader.readLine()) != null) {
// Skip comment lines
if (!line.startsWith("#") && line.length() > 0) {
// 0: "Link"
// 1: New tz id
// Last: Old tz id
String[] fields = line.split("\t+");
String newTzId = fields[1];
String oldTzId = fields[fields.length - 1];
final TimeZone tz = TimeZone.getTimeZone(newTzId);
if (tz == null) {
Log.e(TAG, "Timezone not found: " + newTzId);
continue;
}
processedTimeZones.add(oldTzId);
if (DEBUG) {
Log.e(TAG, "# Dropping identical time zone from backward: " + oldTzId);
}
// Remember the cooler/newer time zone id
if (mDefaultTimeZoneId != null && mDefaultTimeZoneId.equals(oldTzId)) {
mAlternateDefaultTimeZoneId = newTzId;
}
}
}
} catch (IOException ex) {
Log.e(TAG, "Failed to read 'backward' file.");
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException ignored) {
}
}
/*
* zone.tab contains a list of time zones and country code. They are
* "sorted first by country, then an order within the country that (1)
* makes some geographical sense, and (2) puts the most populous zones
* first, where that does not contradict (1)."
*/
try {
String lang = Locale.getDefault().getLanguage();
is = am.open("zone.tab");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = reader.readLine()) != null) {
if (!line.startsWith("#")) { // Skip comment lines
// 0: country code
// 1: coordinates
// 2: time zone id
// 3: comments
final String[] fields = line.split("\t");
final String timeZoneId = fields[2];
final String countryCode = fields[0];
final TimeZone tz = TimeZone.getTimeZone(timeZoneId);
if (tz == null) {
Log.e(TAG, "Timezone not found: " + timeZoneId);
continue;
}
/*
* Dropping non-GMT tzs without a country code. They are not
* really needed and they are dups but missing proper
* country codes. e.g. WET CET MST7MDT PST8PDT Asia/Khandyga
* Asia/Ust-Nera EST
*/
if (countryCode == null && !timeZoneId.startsWith("Etc/GMT")) {
processedTimeZones.add(timeZoneId);
continue;
}
// Remember the mapping between the country code and display
// name
String country = mCountryCodeToNameMap.get(countryCode);
if (country == null) {
country = getCountryNames(lang, countryCode);
mCountryCodeToNameMap.put(countryCode, country);
}
// TODO Don't like this here but need to get the country of
// the default tz.
// Find the country of the default tz
if (mDefaultTimeZoneId != null && mDefaultTimeZoneCountry == null
&& timeZoneId.equals(mAlternateDefaultTimeZoneId)) {
mDefaultTimeZoneCountry = country;
TimeZone defaultTz = TimeZone.getTimeZone(mDefaultTimeZoneId);
if (defaultTz != null) {
mDefaultTimeZoneInfo = new TimeZoneInfo(defaultTz, country);
int tzToOverride = getIdenticalTimeZoneInTheCountry(mDefaultTimeZoneInfo);
if (tzToOverride == -1) {
if (DEBUG) {
Log.e(TAG, "Adding default time zone: "
+ mDefaultTimeZoneInfo.toString());
}
mTimeZones.add(mDefaultTimeZoneInfo);
} else {
mTimeZones.add(tzToOverride, mDefaultTimeZoneInfo);
if (DEBUG) {
TimeZoneInfo tzInfoToOverride = mTimeZones.get(tzToOverride);
String tzIdToOverride = tzInfoToOverride.mTzId;
Log.e(TAG, "Replaced by default tz: "
+ tzInfoToOverride.toString());
Log.e(TAG, "Adding default time zone: "
+ mDefaultTimeZoneInfo.toString());
}
}
}
}
// Add to the list of time zones if the time zone is unique
// in the given country.
TimeZoneInfo timeZoneInfo = new TimeZoneInfo(tz, country);
int identicalTzIdx = getIdenticalTimeZoneInTheCountry(timeZoneInfo);
if (identicalTzIdx == -1) {
if (DEBUG) {
Log.e(TAG, "# Adding time zone: " + timeZoneId + " ## " +
tz.getDisplayName());
}
mTimeZones.add(timeZoneInfo);
} else {
if (DEBUG) {
Log.e(TAG, "# Dropping identical time zone: " + timeZoneId + " ## " +
tz.getDisplayName());
}
}
processedTimeZones.add(timeZoneId);
}
}
} catch (IOException ex) {
Log.e(TAG, "Failed to read 'zone.tab'.");
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException ignored) {
}
}
return processedTimeZones;
}
private static Locale mBackupCountryLocale;
private static String[] mBackupCountryCodes;
private static String[] mBackupCountryNames;
private String getCountryNames(String lang, String countryCode) {
final Locale defaultLocale = Locale.getDefault();
String countryDisplayName;
if (PALESTINE_COUNTRY_CODE.equalsIgnoreCase(countryCode)) {
countryDisplayName = mPalestineDisplayName;
} else {
countryDisplayName = new Locale(lang, countryCode).getDisplayCountry(defaultLocale);
}
if (!countryCode.equals(countryDisplayName)) {
return countryDisplayName;
}
if (mBackupCountryCodes == null || !defaultLocale.equals(mBackupCountryLocale)) {
mBackupCountryLocale = defaultLocale;
mBackupCountryCodes = mContext.getResources().getStringArray(
R.array.backup_country_codes);
mBackupCountryNames = mContext.getResources().getStringArray(
R.array.backup_country_names);
}
int length = Math.min(mBackupCountryCodes.length, mBackupCountryNames.length);
for (int i = 0; i < length; i++) {
if (mBackupCountryCodes[i].equals(countryCode)) {
return mBackupCountryNames[i];
}
}
return countryCode;
}
private int getIdenticalTimeZoneInTheCountry(TimeZoneInfo timeZoneInfo) {
int idx = 0;
for (TimeZoneInfo tzi : mTimeZones) {
if (tzi.hasSameRules(timeZoneInfo)) {
if (tzi.mCountry == null) {
if (timeZoneInfo.mCountry == null) {
return idx;
}
} else if (tzi.mCountry.equals(timeZoneInfo.mCountry)) {
return idx;
}
}
++idx;
}
return -1;
}
}