blob: 7b5bfb5e5dfc752f3c57dbec922522d450184e12 [file] [log] [blame]
Tony Mantlerb3543e02015-05-28 14:48:00 -07001/*
2 * Copyright (C) 2015 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.settingslib.datetime;
18
19import android.content.Context;
20import android.content.res.XmlResourceParser;
21import android.text.BidiFormatter;
22import android.text.TextDirectionHeuristics;
23import android.text.TextUtils;
24import android.util.Log;
25import android.view.View;
26
27import com.android.settingslib.R;
28
29import libcore.icu.TimeZoneNames;
30
31import org.xmlpull.v1.XmlPullParserException;
32
33import java.text.SimpleDateFormat;
34import java.util.ArrayList;
Tony Mantlerb3543e02015-05-28 14:48:00 -070035import java.util.Date;
36import java.util.HashMap;
Tony Mantlerb3543e02015-05-28 14:48:00 -070037import java.util.List;
38import java.util.Locale;
Neil Fuller6394a392015-06-09 10:04:43 +010039import java.util.Map;
40import java.util.Set;
Tony Mantlerb3543e02015-05-28 14:48:00 -070041import java.util.TimeZone;
Neil Fuller6394a392015-06-09 10:04:43 +010042import java.util.TreeSet;
Tony Mantlerb3543e02015-05-28 14:48:00 -070043
44public class ZoneGetter {
45 private static final String TAG = "ZoneGetter";
46
47 private static final String XMLTAG_TIMEZONE = "timezone";
48
49 public static final String KEY_ID = "id"; // value: String
50 public static final String KEY_DISPLAYNAME = "name"; // value: String
51 public static final String KEY_GMT = "gmt"; // value: String
52 public static final String KEY_OFFSET = "offset"; // value: int (Integer)
53
Neil Fuller6394a392015-06-09 10:04:43 +010054 private ZoneGetter() {}
Tony Mantlerb3543e02015-05-28 14:48:00 -070055
Neil Fuller6394a392015-06-09 10:04:43 +010056 public static String getTimeZoneOffsetAndName(TimeZone tz, Date now) {
57 Locale locale = Locale.getDefault();
58 String gmtString = getGmtOffsetString(locale, tz, now);
59 String zoneNameString = getZoneLongName(locale, tz, now);
60 if (zoneNameString == null) {
61 return gmtString;
Tony Mantlerb3543e02015-05-28 14:48:00 -070062 }
Neil Fuller6394a392015-06-09 10:04:43 +010063
64 // We don't use punctuation here to avoid having to worry about localizing that too!
65 return gmtString + " " + zoneNameString;
66 }
67
68 public static List<Map<String, Object>> getZonesList(Context context) {
69 final Locale locale = Locale.getDefault();
70 final Date now = new Date();
71
72 // The display name chosen for each zone entry depends on whether the zone is one associated
73 // with the country of the user's chosen locale. For "local" zones we prefer the "long name"
74 // (e.g. "Europe/London" -> "British Summer Time" for people in the UK). For "non-local"
75 // zones we prefer the exemplar location (e.g. "Europe/London" -> "London" for English
76 // speakers from outside the UK). This heuristic is based on the fact that people are
77 // typically familiar with their local timezones and exemplar locations don't always match
78 // modern-day expectations for people living in the country covered. Large countries like
79 // China that mostly use a single timezone (olson id: "Asia/Shanghai") may not live near
80 // "Shanghai" and prefer the long name over the exemplar location. The only time we don't
81 // follow this policy for local zones is when Android supplies multiple olson IDs to choose
82 // from and the use of a zone's long name leads to ambiguity. For example, at the time of
83 // writing Android lists 5 olson ids for Australia which collapse to 2 different zone names
84 // in winter but 4 different zone names in summer. The ambiguity leads to the users
85 // selecting the wrong olson ids.
86
87 // Get the list of olson ids to display to the user.
88 List<String> olsonIdsToDisplay = readTimezonesToDisplay(context);
89
90 // Create a lookup of local zone IDs.
91 Set<String> localZoneIds = new TreeSet<String>();
92 for (String olsonId : TimeZoneNames.forLocale(locale)) {
93 localZoneIds.add(olsonId);
94 }
95
96 // Work out whether the long names for the local entries that we would show by default would
97 // be ambiguous.
98 Set<String> localZoneNames = new TreeSet<String>();
99 boolean localLongNamesAreAmbiguous = false;
100 for (String olsonId : olsonIdsToDisplay) {
101 if (localZoneIds.contains(olsonId)) {
102 TimeZone tz = TimeZone.getTimeZone(olsonId);
103 String zoneLongName = getZoneLongName(locale, tz, now);
104 boolean longNameIsUnique = localZoneNames.add(zoneLongName);
105 if (!longNameIsUnique) {
106 localLongNamesAreAmbiguous = true;
107 break;
108 }
109 }
110 }
111
112 // Generate the list of zone entries to return.
113 List<Map<String, Object>> zones = new ArrayList<Map<String, Object>>();
114 for (String olsonId : olsonIdsToDisplay) {
115 final TimeZone tz = TimeZone.getTimeZone(olsonId);
116 // Exemplar location display is the default. The only time we intend to display the long
117 // name is when the olsonId is local AND long names are not ambiguous.
118 boolean isLocalZoneId = localZoneIds.contains(olsonId);
119 boolean preferLongName = isLocalZoneId && !localLongNamesAreAmbiguous;
120 String displayName = getZoneDisplayName(locale, tz, now, preferLongName);
121
122 String gmtOffsetString = getGmtOffsetString(locale, tz, now);
123 int offsetMillis = tz.getOffset(now.getTime());
124 Map<String, Object> displayEntry =
125 createDisplayEntry(tz, gmtOffsetString, displayName, offsetMillis);
126 zones.add(displayEntry);
127 }
128 return zones;
129 }
130
131 private static Map<String, Object> createDisplayEntry(
132 TimeZone tz, String gmtOffsetString, String displayName, int offsetMillis) {
133 Map<String, Object> map = new HashMap<String, Object>();
134 map.put(KEY_ID, tz.getID());
135 map.put(KEY_DISPLAYNAME, displayName);
136 map.put(KEY_GMT, gmtOffsetString);
137 map.put(KEY_OFFSET, offsetMillis);
138 return map;
139 }
140
141 /**
142 * Returns a name for the specific zone. If {@code preferLongName} is {@code true} then the
143 * long display name for the timezone will be used, otherwise the exemplar location will be
144 * preferred.
145 */
146 private static String getZoneDisplayName(Locale locale, TimeZone tz, Date now,
147 boolean preferLongName) {
148 String zoneNameString;
149 if (preferLongName) {
150 zoneNameString = getZoneLongName(locale, tz, now);
151 } else {
152 zoneNameString = getZoneExemplarLocation(locale, tz);
153 if (zoneNameString == null || zoneNameString.isEmpty()) {
154 // getZoneExemplarLocation can return null.
155 zoneNameString = getZoneLongName(locale, tz, now);
156 }
157 }
158 return zoneNameString;
159 }
160
161 private static String getZoneExemplarLocation(Locale locale, TimeZone tz) {
162 return TimeZoneNames.getExemplarLocation(locale.toString(), tz.getID());
163 }
164
165 private static List<String> readTimezonesToDisplay(Context context) {
166 List<String> olsonIds = new ArrayList<String>();
167 try (XmlResourceParser xrp = context.getResources().getXml(R.xml.timezones)) {
Tony Mantlerb3543e02015-05-28 14:48:00 -0700168 while (xrp.next() != XmlResourceParser.START_TAG) {
169 continue;
170 }
171 xrp.next();
172 while (xrp.getEventType() != XmlResourceParser.END_TAG) {
173 while (xrp.getEventType() != XmlResourceParser.START_TAG) {
174 if (xrp.getEventType() == XmlResourceParser.END_DOCUMENT) {
Neil Fuller6394a392015-06-09 10:04:43 +0100175 return olsonIds;
Tony Mantlerb3543e02015-05-28 14:48:00 -0700176 }
177 xrp.next();
178 }
179 if (xrp.getName().equals(XMLTAG_TIMEZONE)) {
180 String olsonId = xrp.getAttributeValue(0);
Neil Fuller6394a392015-06-09 10:04:43 +0100181 olsonIds.add(olsonId);
Tony Mantlerb3543e02015-05-28 14:48:00 -0700182 }
183 while (xrp.getEventType() != XmlResourceParser.END_TAG) {
184 xrp.next();
185 }
186 xrp.next();
187 }
Tony Mantlerb3543e02015-05-28 14:48:00 -0700188 } catch (XmlPullParserException xppe) {
189 Log.e(TAG, "Ill-formatted timezones.xml file");
190 } catch (java.io.IOException ioe) {
191 Log.e(TAG, "Unable to read timezones.xml file");
192 }
Neil Fuller6394a392015-06-09 10:04:43 +0100193 return olsonIds;
Tony Mantlerb3543e02015-05-28 14:48:00 -0700194 }
195
Neil Fuller6394a392015-06-09 10:04:43 +0100196 private static String getZoneLongName(Locale locale, TimeZone tz, Date now) {
197 boolean daylight = tz.inDaylightTime(now);
198 // This returns a name if it can, or will fall back to GMT+0X:00 format.
199 return tz.getDisplayName(daylight, TimeZone.LONG, locale);
Tony Mantlerb3543e02015-05-28 14:48:00 -0700200 }
201
Neil Fuller6394a392015-06-09 10:04:43 +0100202 private static String getGmtOffsetString(Locale locale, TimeZone tz, Date now) {
Tony Mantlerb3543e02015-05-28 14:48:00 -0700203 // Use SimpleDateFormat to format the GMT+00:00 string.
204 SimpleDateFormat gmtFormatter = new SimpleDateFormat("ZZZZ");
205 gmtFormatter.setTimeZone(tz);
206 String gmtString = gmtFormatter.format(now);
207
208 // Ensure that the "GMT+" stays with the "00:00" even if the digits are RTL.
209 BidiFormatter bidiFormatter = BidiFormatter.getInstance();
Neil Fuller6394a392015-06-09 10:04:43 +0100210 boolean isRtl = TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL;
Tony Mantlerb3543e02015-05-28 14:48:00 -0700211 gmtString = bidiFormatter.unicodeWrap(gmtString,
212 isRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR);
Neil Fuller6394a392015-06-09 10:04:43 +0100213 return gmtString;
Tony Mantlerb3543e02015-05-28 14:48:00 -0700214 }
215}