1 /* 2 * Copyright 2017 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.internal.telephony; 18 19 import android.text.TextUtils; 20 21 import libcore.util.CountryTimeZones; 22 import libcore.util.TimeZoneFinder; 23 24 import java.util.Date; 25 import java.util.TimeZone; 26 27 /** 28 * An interface to various time zone lookup behaviors. 29 */ 30 // Non-final to allow mocking. 31 public class TimeZoneLookupHelper { 32 33 /** 34 * The result of looking up a time zone using offset information (and possibly more). 35 */ 36 public static final class OffsetResult { 37 38 /** A zone that matches the supplied criteria. See also {@link #isOnlyMatch}. */ 39 public final String zoneId; 40 41 /** True if there is only one matching time zone for the supplied criteria. */ 42 public final boolean isOnlyMatch; 43 OffsetResult(String zoneId, boolean isOnlyMatch)44 public OffsetResult(String zoneId, boolean isOnlyMatch) { 45 this.zoneId = zoneId; 46 this.isOnlyMatch = isOnlyMatch; 47 } 48 49 @Override equals(Object o)50 public boolean equals(Object o) { 51 if (this == o) { 52 return true; 53 } 54 if (o == null || getClass() != o.getClass()) { 55 return false; 56 } 57 58 OffsetResult result = (OffsetResult) o; 59 60 if (isOnlyMatch != result.isOnlyMatch) { 61 return false; 62 } 63 return zoneId.equals(result.zoneId); 64 } 65 66 @Override hashCode()67 public int hashCode() { 68 int result = zoneId.hashCode(); 69 result = 31 * result + (isOnlyMatch ? 1 : 0); 70 return result; 71 } 72 73 @Override toString()74 public String toString() { 75 return "Result{" 76 + "zoneId='" + zoneId + '\'' 77 + ", isOnlyMatch=" + isOnlyMatch 78 + '}'; 79 } 80 } 81 82 /** 83 * The result of looking up a time zone using country information. 84 */ 85 public static final class CountryResult { 86 87 /** A time zone for the country. */ 88 public final String zoneId; 89 90 /** 91 * True if all the time zones in the country have the same offset at {@link #whenMillis}. 92 */ 93 public final boolean allZonesHaveSameOffset; 94 95 /** The time associated with {@link #allZonesHaveSameOffset}. */ 96 public final long whenMillis; 97 CountryResult(String zoneId, boolean allZonesHaveSameOffset, long whenMillis)98 public CountryResult(String zoneId, boolean allZonesHaveSameOffset, long whenMillis) { 99 this.zoneId = zoneId; 100 this.allZonesHaveSameOffset = allZonesHaveSameOffset; 101 this.whenMillis = whenMillis; 102 } 103 104 @Override equals(Object o)105 public boolean equals(Object o) { 106 if (this == o) { 107 return true; 108 } 109 if (o == null || getClass() != o.getClass()) { 110 return false; 111 } 112 113 CountryResult that = (CountryResult) o; 114 115 if (allZonesHaveSameOffset != that.allZonesHaveSameOffset) { 116 return false; 117 } 118 if (whenMillis != that.whenMillis) { 119 return false; 120 } 121 return zoneId.equals(that.zoneId); 122 } 123 124 @Override hashCode()125 public int hashCode() { 126 int result = zoneId.hashCode(); 127 result = 31 * result + (allZonesHaveSameOffset ? 1 : 0); 128 result = 31 * result + (int) (whenMillis ^ (whenMillis >>> 32)); 129 return result; 130 } 131 132 @Override toString()133 public String toString() { 134 return "CountryResult{" 135 + "zoneId='" + zoneId + '\'' 136 + ", allZonesHaveSameOffset=" + allZonesHaveSameOffset 137 + ", whenMillis=" + whenMillis 138 + '}'; 139 } 140 } 141 142 private static final int MS_PER_HOUR = 60 * 60 * 1000; 143 144 /** The last CountryTimeZones object retrieved. */ 145 private CountryTimeZones mLastCountryTimeZones; 146 TimeZoneLookupHelper()147 public TimeZoneLookupHelper() {} 148 149 /** 150 * Looks for a time zone for the supplied NITZ and country information. 151 * 152 * <p><em>Note:</em> When there are multiple matching zones then one of the matching candidates 153 * will be returned in the result. If the current device default zone matches it will be 154 * returned in preference to other candidates. This method can return {@code null} if no 155 * matching time zones are found. 156 */ lookupByNitzCountry(NitzData nitzData, String isoCountryCode)157 public OffsetResult lookupByNitzCountry(NitzData nitzData, String isoCountryCode) { 158 CountryTimeZones countryTimeZones = getCountryTimeZones(isoCountryCode); 159 if (countryTimeZones == null) { 160 return null; 161 } 162 android.icu.util.TimeZone bias = android.icu.util.TimeZone.getDefault(); 163 164 CountryTimeZones.OffsetResult offsetResult = countryTimeZones.lookupByOffsetWithBias( 165 nitzData.getLocalOffsetMillis(), nitzData.isDst(), 166 nitzData.getCurrentTimeInMillis(), bias); 167 168 if (offsetResult == null) { 169 return null; 170 } 171 return new OffsetResult(offsetResult.mTimeZone.getID(), offsetResult.mOneMatch); 172 } 173 174 /** 175 * Looks for a time zone using only information present in the supplied {@link NitzData} object. 176 * 177 * <p><em>Note:</em> Because multiple time zones can have the same offset / DST state at a given 178 * time this process is error prone; an arbitrary match is returned when there are multiple 179 * candidates. The algorithm can also return a non-exact match by assuming that the DST 180 * information provided by NITZ is incorrect. This method can return {@code null} if no matching 181 * time zones are found. 182 */ lookupByNitz(NitzData nitzData)183 public OffsetResult lookupByNitz(NitzData nitzData) { 184 return lookupByNitzStatic(nitzData); 185 } 186 187 /** 188 * Returns a time zone ID for the country if possible. For counties that use a single time zone 189 * this will provide a good choice. For countries with multiple time zones, a time zone is 190 * returned if all time zones used in the country currently have the same offset (currently == 191 * according to the device's current system clock time). If this is not the case then 192 * {@code null} can be returned. 193 */ lookupByCountry(String isoCountryCode, long whenMillis)194 public CountryResult lookupByCountry(String isoCountryCode, long whenMillis) { 195 CountryTimeZones countryTimeZones = getCountryTimeZones(isoCountryCode); 196 if (countryTimeZones == null) { 197 // Unknown country code. 198 return null; 199 } 200 if (countryTimeZones.getDefaultTimeZoneId() == null) { 201 return null; 202 } 203 204 return new CountryResult( 205 countryTimeZones.getDefaultTimeZoneId(), 206 countryTimeZones.isDefaultOkForCountryTimeZoneDetection(whenMillis), 207 whenMillis); 208 } 209 210 /** 211 * Finds a time zone using only information present in the supplied {@link NitzData} object. 212 * This is a static method for use by {@link ServiceStateTracker}. 213 * 214 * <p><em>Note:</em> Because multiple time zones can have the same offset / DST state at a given 215 * time this process is error prone; an arbitrary match is returned when there are multiple 216 * candidates. The algorithm can also return a non-exact match by assuming that the DST 217 * information provided by NITZ is incorrect. This method can return {@code null} if no matching 218 * time zones are found. 219 */ guessZoneByNitzStatic(NitzData nitzData)220 static TimeZone guessZoneByNitzStatic(NitzData nitzData) { 221 OffsetResult result = lookupByNitzStatic(nitzData); 222 return result != null ? TimeZone.getTimeZone(result.zoneId) : null; 223 } 224 lookupByNitzStatic(NitzData nitzData)225 private static OffsetResult lookupByNitzStatic(NitzData nitzData) { 226 int utcOffsetMillis = nitzData.getLocalOffsetMillis(); 227 boolean isDst = nitzData.isDst(); 228 long timeMillis = nitzData.getCurrentTimeInMillis(); 229 230 OffsetResult match = lookupByInstantOffsetDst(timeMillis, utcOffsetMillis, isDst); 231 if (match == null) { 232 // Couldn't find a proper timezone. Perhaps the DST data is wrong. 233 match = lookupByInstantOffsetDst(timeMillis, utcOffsetMillis, !isDst); 234 } 235 return match; 236 } 237 lookupByInstantOffsetDst(long timeMillis, int utcOffsetMillis, boolean isDst)238 private static OffsetResult lookupByInstantOffsetDst(long timeMillis, int utcOffsetMillis, 239 boolean isDst) { 240 int rawOffset = utcOffsetMillis; 241 if (isDst) { 242 rawOffset -= MS_PER_HOUR; 243 } 244 String[] zones = TimeZone.getAvailableIDs(rawOffset); 245 TimeZone match = null; 246 Date d = new Date(timeMillis); 247 boolean isOnlyMatch = true; 248 for (String zone : zones) { 249 TimeZone tz = TimeZone.getTimeZone(zone); 250 if (tz.getOffset(timeMillis) == utcOffsetMillis && tz.inDaylightTime(d) == isDst) { 251 if (match == null) { 252 match = tz; 253 } else { 254 isOnlyMatch = false; 255 break; 256 } 257 } 258 } 259 260 if (match == null) { 261 return null; 262 } 263 return new OffsetResult(match.getID(), isOnlyMatch); 264 } 265 266 /** 267 * Returns {@code true} if the supplied (lower-case) ISO country code is for a country known to 268 * use a raw offset of zero from UTC at the time specified. 269 */ countryUsesUtc(String isoCountryCode, long whenMillis)270 public boolean countryUsesUtc(String isoCountryCode, long whenMillis) { 271 if (TextUtils.isEmpty(isoCountryCode)) { 272 return false; 273 } 274 275 CountryTimeZones countryTimeZones = getCountryTimeZones(isoCountryCode); 276 return countryTimeZones != null && countryTimeZones.hasUtcZone(whenMillis); 277 } 278 getCountryTimeZones(String isoCountryCode)279 private CountryTimeZones getCountryTimeZones(String isoCountryCode) { 280 // A single entry cache of the last CountryTimeZones object retrieved since there should 281 // be strong consistency across calls. 282 synchronized (this) { 283 if (mLastCountryTimeZones != null) { 284 if (mLastCountryTimeZones.isForCountryCode(isoCountryCode)) { 285 return mLastCountryTimeZones; 286 } 287 } 288 289 // Perform the lookup. It's very unlikely to return null, but we won't cache null. 290 CountryTimeZones countryTimeZones = 291 TimeZoneFinder.getInstance().lookupCountryTimeZones(isoCountryCode); 292 if (countryTimeZones != null) { 293 mLastCountryTimeZones = countryTimeZones; 294 } 295 return countryTimeZones; 296 } 297 } 298 } 299