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.icu.text.TimeZoneNames; 22 import android.text.BidiFormatter; 23 import android.text.TextDirectionHeuristics; 24 import android.text.TextUtils; 25 import android.util.Log; 26 import android.view.View; 27 28 import com.android.settingslib.R; 29 30 import org.xmlpull.v1.XmlPullParserException; 31 32 import java.text.SimpleDateFormat; 33 import java.util.ArrayList; 34 import java.util.Date; 35 import java.util.HashMap; 36 import java.util.HashSet; 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 43 public class ZoneGetter { 44 private static final String TAG = "ZoneGetter"; 45 46 private static final String XMLTAG_TIMEZONE = "timezone"; 47 48 public static final String KEY_ID = "id"; // value: String 49 public static final String KEY_DISPLAYNAME = "name"; // value: String 50 public static final String KEY_GMT = "gmt"; // value: String 51 public static final String KEY_OFFSET = "offset"; // value: int (Integer) 52 53 private ZoneGetter() {} 54 55 public static String getTimeZoneOffsetAndName(TimeZone tz, Date now) { 56 Locale locale = Locale.getDefault(); 57 String gmtString = getGmtOffsetString(locale, tz, now); 58 TimeZoneNames timeZoneNames = TimeZoneNames.getInstance(locale); 59 String zoneNameString = getZoneLongName(timeZoneNames, 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 68 public static List<Map<String, Object>> getZonesList(Context context) { 69 final Locale locale = Locale.getDefault(); 70 final Date now = new Date(); 71 final TimeZoneNames timeZoneNames = TimeZoneNames.getInstance(locale); 72 73 // The display name chosen for each zone entry depends on whether the zone is one associated 74 // with the country of the user's chosen locale. For "local" zones we prefer the "long name" 75 // (e.g. "Europe/London" -> "British Summer Time" for people in the UK). For "non-local" 76 // zones we prefer the exemplar location (e.g. "Europe/London" -> "London" for English 77 // speakers from outside the UK). This heuristic is based on the fact that people are 78 // typically familiar with their local timezones and exemplar locations don't always match 79 // modern-day expectations for people living in the country covered. Large countries like 80 // China that mostly use a single timezone (olson id: "Asia/Shanghai") may not live near 81 // "Shanghai" and prefer the long name over the exemplar location. The only time we don't 82 // follow this policy for local zones is when Android supplies multiple olson IDs to choose 83 // from and the use of a zone's long name leads to ambiguity. For example, at the time of 84 // writing Android lists 5 olson ids for Australia which collapse to 2 different zone names 85 // in winter but 4 different zone names in summer. The ambiguity leads to the users 86 // selecting the wrong olson ids. 87 88 // Get the list of olson ids to display to the user. 89 List<String> olsonIdsToDisplayList = readTimezonesToDisplay(context); 90 91 // Store the information we are going to need more than once. 92 final int zoneCount = olsonIdsToDisplayList.size(); 93 final String[] olsonIdsToDisplay = new String[zoneCount]; 94 final TimeZone[] timeZones = new TimeZone[zoneCount]; 95 final String[] gmtOffsetStrings = new String[zoneCount]; 96 for (int i = 0; i < zoneCount; i++) { 97 String olsonId = olsonIdsToDisplayList.get(i); 98 olsonIdsToDisplay[i] = olsonId; 99 TimeZone tz = TimeZone.getTimeZone(olsonId); 100 timeZones[i] = tz; 101 gmtOffsetStrings[i] = getGmtOffsetString(locale, tz, now); 102 } 103 104 // Create a lookup of local zone IDs. 105 Set<String> localZoneIds = new HashSet<String>(); 106 for (String olsonId : libcore.icu.TimeZoneNames.forLocale(locale)) { 107 localZoneIds.add(olsonId); 108 } 109 110 // Work out whether the display names we would show by default would be ambiguous. 111 Set<String> localZoneNames = new HashSet<String>(); 112 boolean useExemplarLocationForLocalNames = false; 113 for (int i = 0; i < zoneCount; i++) { 114 String olsonId = olsonIdsToDisplay[i]; 115 if (localZoneIds.contains(olsonId)) { 116 TimeZone tz = timeZones[i]; 117 String displayName = getZoneLongName(timeZoneNames, tz, now); 118 if (displayName == null) { 119 displayName = gmtOffsetStrings[i]; 120 } 121 boolean nameIsUnique = localZoneNames.add(displayName); 122 if (!nameIsUnique) { 123 useExemplarLocationForLocalNames = true; 124 break; 125 } 126 } 127 } 128 129 // Generate the list of zone entries to return. 130 List<Map<String, Object>> zones = new ArrayList<Map<String, Object>>(); 131 for (int i = 0; i < zoneCount; i++) { 132 String olsonId = olsonIdsToDisplay[i]; 133 TimeZone tz = timeZones[i]; 134 String gmtOffsetString = gmtOffsetStrings[i]; 135 136 boolean isLocalZoneId = localZoneIds.contains(olsonId); 137 boolean preferLongName = isLocalZoneId && !useExemplarLocationForLocalNames; 138 String displayName; 139 if (preferLongName) { 140 displayName = getZoneLongName(timeZoneNames, tz, now); 141 } else { 142 displayName = timeZoneNames.getExemplarLocationName(tz.getID()); 143 if (displayName == null || displayName.isEmpty()) { 144 // getZoneExemplarLocation can return null. Fall back to the long name. 145 displayName = getZoneLongName(timeZoneNames, tz, now); 146 } 147 } 148 if (displayName == null || displayName.isEmpty()) { 149 displayName = gmtOffsetString; 150 } 151 152 int offsetMillis = tz.getOffset(now.getTime()); 153 Map<String, Object> displayEntry = 154 createDisplayEntry(tz, gmtOffsetString, displayName, offsetMillis); 155 zones.add(displayEntry); 156 } 157 return zones; 158 } 159 160 private static Map<String, Object> createDisplayEntry( 161 TimeZone tz, String gmtOffsetString, String displayName, int offsetMillis) { 162 Map<String, Object> map = new HashMap<String, Object>(); 163 map.put(KEY_ID, tz.getID()); 164 map.put(KEY_DISPLAYNAME, displayName); 165 map.put(KEY_GMT, gmtOffsetString); 166 map.put(KEY_OFFSET, offsetMillis); 167 return map; 168 } 169 170 private static List<String> readTimezonesToDisplay(Context context) { 171 List<String> olsonIds = new ArrayList<String>(); 172 try (XmlResourceParser xrp = context.getResources().getXml(R.xml.timezones)) { 173 while (xrp.next() != XmlResourceParser.START_TAG) { 174 continue; 175 } 176 xrp.next(); 177 while (xrp.getEventType() != XmlResourceParser.END_TAG) { 178 while (xrp.getEventType() != XmlResourceParser.START_TAG) { 179 if (xrp.getEventType() == XmlResourceParser.END_DOCUMENT) { 180 return olsonIds; 181 } 182 xrp.next(); 183 } 184 if (xrp.getName().equals(XMLTAG_TIMEZONE)) { 185 String olsonId = xrp.getAttributeValue(0); 186 olsonIds.add(olsonId); 187 } 188 while (xrp.getEventType() != XmlResourceParser.END_TAG) { 189 xrp.next(); 190 } 191 xrp.next(); 192 } 193 } catch (XmlPullParserException xppe) { 194 Log.e(TAG, "Ill-formatted timezones.xml file"); 195 } catch (java.io.IOException ioe) { 196 Log.e(TAG, "Unable to read timezones.xml file"); 197 } 198 return olsonIds; 199 } 200 201 /** 202 * Returns the long name for the timezone for the given locale at the time specified. 203 * Can return {@code null}. 204 */ 205 private static String getZoneLongName(TimeZoneNames names, TimeZone tz, Date now) { 206 TimeZoneNames.NameType nameType = 207 tz.inDaylightTime(now) ? TimeZoneNames.NameType.LONG_DAYLIGHT 208 : TimeZoneNames.NameType.LONG_STANDARD; 209 return names.getDisplayName(tz.getID(), nameType, now.getTime()); 210 } 211 212 private static String getGmtOffsetString(Locale locale, TimeZone tz, Date now) { 213 // Use SimpleDateFormat to format the GMT+00:00 string. 214 SimpleDateFormat gmtFormatter = new SimpleDateFormat("ZZZZ"); 215 gmtFormatter.setTimeZone(tz); 216 String gmtString = gmtFormatter.format(now); 217 218 // Ensure that the "GMT+" stays with the "00:00" even if the digits are RTL. 219 BidiFormatter bidiFormatter = BidiFormatter.getInstance(); 220 boolean isRtl = TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL; 221 gmtString = bidiFormatter.unicodeWrap(gmtString, 222 isRtl ? TextDirectionHeuristics.RTL : TextDirectionHeuristics.LTR); 223 return gmtString; 224 } 225 } 226