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 54 private ZoneGetter() {} 55 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 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)) { 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 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 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