• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.settingslib.datetime;
18 
19 import android.content.Context;
20 import android.content.res.XmlResourceParser;
21 import android.text.BidiFormatter;
22 import android.text.TextDirectionHeuristics;
23 import android.text.TextUtils;
24 import android.util.Log;
25 import android.view.View;
26 
27 import com.android.settingslib.R;
28 
29 import libcore.icu.TimeZoneNames;
30 
31 import org.xmlpull.v1.XmlPullParserException;
32 
33 import java.text.SimpleDateFormat;
34 import java.util.ArrayList;
35 import java.util.Date;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.Map;
40 import java.util.Set;
41 import java.util.TimeZone;
42 import java.util.TreeSet;
43 
44 public 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 
ZoneGetter()54     private ZoneGetter() {}
55 
getTimeZoneOffsetAndName(TimeZone tz, Date now)56     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;
62         }
63 
64         // We don't use punctuation here to avoid having to worry about localizing that too!
65         return gmtString + " " + zoneNameString;
66     }
67 
getZonesList(Context context)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 
createDisplayEntry( TimeZone tz, String gmtOffsetString, String displayName, int offsetMillis)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      */
getZoneDisplayName(Locale locale, TimeZone tz, Date now, boolean preferLongName)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 
getZoneExemplarLocation(Locale locale, TimeZone tz)161     private static String getZoneExemplarLocation(Locale locale, TimeZone tz) {
162         return TimeZoneNames.getExemplarLocation(locale.toString(), tz.getID());
163     }
164 
readTimezonesToDisplay(Context context)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)) {
168             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) {
175                         return olsonIds;
176                     }
177                     xrp.next();
178                 }
179                 if (xrp.getName().equals(XMLTAG_TIMEZONE)) {
180                     String olsonId = xrp.getAttributeValue(0);
181                     olsonIds.add(olsonId);
182                 }
183                 while (xrp.getEventType() != XmlResourceParser.END_TAG) {
184                     xrp.next();
185                 }
186                 xrp.next();
187             }
188         } 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         }
193         return olsonIds;
194     }
195 
getZoneLongName(Locale locale, TimeZone tz, Date now)196     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);
200     }
201 
getGmtOffsetString(Locale locale, TimeZone tz, Date now)202     private static String getGmtOffsetString(Locale locale, TimeZone tz, Date now) {
203         // 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();
210         boolean isRtl = TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL;
211         gmtString = bidiFormatter.unicodeWrap(gmtString,
212                 isRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR);
213         return gmtString;
214     }
215 }
216