1 /* 2 * Copyright (C) 2006 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 android.util; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.TestApi; 22 import android.compat.annotation.UnsupportedAppUsage; 23 import android.os.Build; 24 import android.os.SystemClock; 25 26 import com.android.i18n.timezone.CountryTimeZones; 27 import com.android.i18n.timezone.CountryTimeZones.TimeZoneMapping; 28 import com.android.i18n.timezone.TimeZoneFinder; 29 import com.android.i18n.timezone.ZoneInfoDb; 30 31 import java.io.PrintWriter; 32 import java.text.SimpleDateFormat; 33 import java.time.Instant; 34 import java.time.LocalTime; 35 import java.util.ArrayList; 36 import java.util.Calendar; 37 import java.util.Collections; 38 import java.util.Date; 39 import java.util.List; 40 41 /** 42 * A class containing utility methods related to time zones. 43 */ 44 public class TimeUtils { TimeUtils()45 /** @hide */ public TimeUtils() {} 46 /** {@hide} */ 47 private static final SimpleDateFormat sLoggingFormat = 48 new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 49 50 /** @hide */ 51 public static final SimpleDateFormat sDumpDateFormat = 52 new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 53 54 /** 55 * This timestamp is used in TimeUtils methods and by the SettingsUI to filter time zones 56 * to only "effective" ones in a country. It is compared against the notUsedAfter metadata that 57 * Android records for some time zones. 58 * 59 * <p>What is notUsedAfter?</p> 60 * Android chooses to avoid making users choose between functionally identical time zones at the 61 * expense of not being able to represent local times in the past. 62 * 63 * notUsedAfter exists because some time zones can "merge" with other time zones after a given 64 * point in time (i.e. they change to have identical transitions, offsets, display names, etc.). 65 * From the notUsedAfter time, the zone will express the same local time as the one it merged 66 * with. 67 * 68 * <p>Why hardcoded?</p> 69 * Rather than using System.currentTimeMillis(), a timestamp known to be in the recent past is 70 * used to ensure consistent behavior across devices and time, and avoid assumptions that the 71 * system clock on a device is currently set correctly. The fixed value should be updated 72 * occasionally, but it doesn't have to be very often as effective time zones for a country 73 * don't change very often. 74 * 75 * @hide 76 */ 77 public static final Instant MIN_USE_DATE_OF_TIMEZONE = 78 Instant.ofEpochMilli(1546300800000L); // 1/1/2019 00:00 UTC 79 80 /** 81 * Tries to return a time zone that would have had the specified offset 82 * and DST value at the specified moment in the specified country. 83 * Returns null if no suitable zone could be found. 84 */ getTimeZone( int offset, boolean dst, long when, String country)85 public static java.util.TimeZone getTimeZone( 86 int offset, boolean dst, long when, String country) { 87 88 android.icu.util.TimeZone icuTimeZone = getIcuTimeZone(offset, dst, when, country); 89 // We must expose a java.util.TimeZone here for API compatibility because this is a public 90 // API method. 91 return icuTimeZone != null ? java.util.TimeZone.getTimeZone(icuTimeZone.getID()) : null; 92 } 93 94 /** 95 * Returns a frozen ICU time zone that has / would have had the specified offset and DST value 96 * at the specified moment in the specified country. Returns null if no suitable zone could be 97 * found. 98 */ getIcuTimeZone( int offsetMillis, boolean isDst, long whenMillis, String countryIso)99 private static android.icu.util.TimeZone getIcuTimeZone( 100 int offsetMillis, boolean isDst, long whenMillis, String countryIso) { 101 if (countryIso == null) { 102 return null; 103 } 104 105 android.icu.util.TimeZone bias = android.icu.util.TimeZone.getDefault(); 106 CountryTimeZones countryTimeZones = 107 TimeZoneFinder.getInstance().lookupCountryTimeZones(countryIso); 108 if (countryTimeZones == null) { 109 return null; 110 } 111 CountryTimeZones.OffsetResult offsetResult = countryTimeZones.lookupByOffsetWithBias( 112 whenMillis, bias, offsetMillis, isDst); 113 return offsetResult != null ? offsetResult.getTimeZone() : null; 114 } 115 116 /** 117 * Returns time zone IDs for time zones known to be associated with a country. 118 * 119 * <p>The list returned may be different from other on-device sources like 120 * {@link android.icu.util.TimeZone#getRegion(String)} as it can be curated to avoid 121 * contentious or obsolete mappings. 122 * 123 * @param countryCode the ISO 3166-1 alpha-2 code for the country as can be obtained using 124 * {@link java.util.Locale#getCountry()} 125 * @return IDs that can be passed to {@link java.util.TimeZone#getTimeZone(String)} or similar 126 * methods, or {@code null} if the countryCode is unrecognized 127 */ getTimeZoneIdsForCountryCode(@onNull String countryCode)128 public static @Nullable List<String> getTimeZoneIdsForCountryCode(@NonNull String countryCode) { 129 if (countryCode == null) { 130 throw new NullPointerException("countryCode == null"); 131 } 132 TimeZoneFinder timeZoneFinder = TimeZoneFinder.getInstance(); 133 CountryTimeZones countryTimeZones = 134 timeZoneFinder.lookupCountryTimeZones(countryCode.toLowerCase()); 135 if (countryTimeZones == null) { 136 return null; 137 } 138 139 List<String> timeZoneIds = new ArrayList<>(); 140 for (TimeZoneMapping timeZoneMapping : countryTimeZones.getTimeZoneMappings()) { 141 if (timeZoneMapping.isShownInPickerAt(MIN_USE_DATE_OF_TIMEZONE)) { 142 timeZoneIds.add(timeZoneMapping.getTimeZoneId()); 143 } 144 } 145 return Collections.unmodifiableList(timeZoneIds); 146 } 147 148 /** 149 * Returns a String indicating the version of the time zone database currently 150 * in use. The format of the string is dependent on the underlying time zone 151 * database implementation, but will typically contain the year in which the database 152 * was updated plus a letter from a to z indicating changes made within that year. 153 * 154 * <p>Time zone database updates should be expected to occur periodically due to 155 * political and legal changes that cannot be anticipated in advance. Therefore, 156 * when computing the UTC time for a future event, applications should be aware that 157 * the results may differ following a time zone database update. This method allows 158 * applications to detect that a database change has occurred, and to recalculate any 159 * cached times accordingly. 160 * 161 * <p>The time zone database may be assumed to change only when the device runtime 162 * is restarted. Therefore, it is not necessary to re-query the database version 163 * during the lifetime of an activity. 164 */ getTimeZoneDatabaseVersion()165 public static String getTimeZoneDatabaseVersion() { 166 return ZoneInfoDb.getInstance().getVersion(); 167 } 168 169 /** @hide Field length that can hold 999 days of time */ 170 public static final int HUNDRED_DAY_FIELD_LEN = 19; 171 172 private static final int SECONDS_PER_MINUTE = 60; 173 private static final int SECONDS_PER_HOUR = 60 * 60; 174 private static final int SECONDS_PER_DAY = 24 * 60 * 60; 175 176 /** @hide */ 177 public static final long NANOS_PER_MS = 1000000; 178 179 private static final Object sFormatSync = new Object(); 180 private static char[] sFormatStr = new char[HUNDRED_DAY_FIELD_LEN+10]; 181 private static char[] sTmpFormatStr = new char[HUNDRED_DAY_FIELD_LEN+10]; 182 accumField(int amt, int suffix, boolean always, int zeropad)183 static private int accumField(int amt, int suffix, boolean always, int zeropad) { 184 if (amt > 999) { 185 int num = 0; 186 while (amt != 0) { 187 num++; 188 amt /= 10; 189 } 190 return num + suffix; 191 } else { 192 if (amt > 99 || (always && zeropad >= 3)) { 193 return 3+suffix; 194 } 195 if (amt > 9 || (always && zeropad >= 2)) { 196 return 2+suffix; 197 } 198 if (always || amt > 0) { 199 return 1+suffix; 200 } 201 } 202 return 0; 203 } 204 printFieldLocked(char[] formatStr, int amt, char suffix, int pos, boolean always, int zeropad)205 static private int printFieldLocked(char[] formatStr, int amt, char suffix, int pos, 206 boolean always, int zeropad) { 207 if (always || amt > 0) { 208 final int startPos = pos; 209 if (amt > 999) { 210 int tmp = 0; 211 while (amt != 0 && tmp < sTmpFormatStr.length) { 212 int dig = amt % 10; 213 sTmpFormatStr[tmp] = (char)(dig + '0'); 214 tmp++; 215 amt /= 10; 216 } 217 tmp--; 218 while (tmp >= 0) { 219 formatStr[pos] = sTmpFormatStr[tmp]; 220 pos++; 221 tmp--; 222 } 223 } else { 224 if ((always && zeropad >= 3) || amt > 99) { 225 int dig = amt/100; 226 formatStr[pos] = (char)(dig + '0'); 227 pos++; 228 amt -= (dig*100); 229 } 230 if ((always && zeropad >= 2) || amt > 9 || startPos != pos) { 231 int dig = amt/10; 232 formatStr[pos] = (char)(dig + '0'); 233 pos++; 234 amt -= (dig*10); 235 } 236 formatStr[pos] = (char)(amt + '0'); 237 pos++; 238 } 239 formatStr[pos] = suffix; 240 pos++; 241 } 242 return pos; 243 } 244 formatDurationLocked(long duration, int fieldLen)245 private static int formatDurationLocked(long duration, int fieldLen) { 246 if (sFormatStr.length < fieldLen) { 247 sFormatStr = new char[fieldLen]; 248 } 249 250 char[] formatStr = sFormatStr; 251 252 if (duration == 0) { 253 int pos = 0; 254 fieldLen -= 1; 255 while (pos < fieldLen) { 256 formatStr[pos++] = ' '; 257 } 258 formatStr[pos] = '0'; 259 return pos+1; 260 } 261 262 char prefix; 263 if (duration > 0) { 264 prefix = '+'; 265 } else { 266 prefix = '-'; 267 duration = -duration; 268 } 269 270 int millis = (int)(duration%1000); 271 int seconds = (int) Math.floor(duration / 1000); 272 int days = 0, hours = 0, minutes = 0; 273 274 if (seconds >= SECONDS_PER_DAY) { 275 days = seconds / SECONDS_PER_DAY; 276 seconds -= days * SECONDS_PER_DAY; 277 } 278 if (seconds >= SECONDS_PER_HOUR) { 279 hours = seconds / SECONDS_PER_HOUR; 280 seconds -= hours * SECONDS_PER_HOUR; 281 } 282 if (seconds >= SECONDS_PER_MINUTE) { 283 minutes = seconds / SECONDS_PER_MINUTE; 284 seconds -= minutes * SECONDS_PER_MINUTE; 285 } 286 287 int pos = 0; 288 289 if (fieldLen != 0) { 290 int myLen = accumField(days, 1, false, 0); 291 myLen += accumField(hours, 1, myLen > 0, 2); 292 myLen += accumField(minutes, 1, myLen > 0, 2); 293 myLen += accumField(seconds, 1, myLen > 0, 2); 294 myLen += accumField(millis, 2, true, myLen > 0 ? 3 : 0) + 1; 295 while (myLen < fieldLen) { 296 formatStr[pos] = ' '; 297 pos++; 298 myLen++; 299 } 300 } 301 302 formatStr[pos] = prefix; 303 pos++; 304 305 int start = pos; 306 boolean zeropad = fieldLen != 0; 307 pos = printFieldLocked(formatStr, days, 'd', pos, false, 0); 308 pos = printFieldLocked(formatStr, hours, 'h', pos, pos != start, zeropad ? 2 : 0); 309 pos = printFieldLocked(formatStr, minutes, 'm', pos, pos != start, zeropad ? 2 : 0); 310 pos = printFieldLocked(formatStr, seconds, 's', pos, pos != start, zeropad ? 2 : 0); 311 pos = printFieldLocked(formatStr, millis, 'm', pos, true, (zeropad && pos != start) ? 3 : 0); 312 formatStr[pos] = 's'; 313 return pos + 1; 314 } 315 316 /** @hide Just for debugging; not internationalized. */ formatDuration(long duration, StringBuilder builder)317 public static void formatDuration(long duration, StringBuilder builder) { 318 synchronized (sFormatSync) { 319 int len = formatDurationLocked(duration, 0); 320 builder.append(sFormatStr, 0, len); 321 } 322 } 323 324 /** @hide Just for debugging; not internationalized. */ formatDuration(long duration, StringBuilder builder, int fieldLen)325 public static void formatDuration(long duration, StringBuilder builder, int fieldLen) { 326 synchronized (sFormatSync) { 327 int len = formatDurationLocked(duration, fieldLen); 328 builder.append(sFormatStr, 0, len); 329 } 330 } 331 332 /** @hide Just for debugging; not internationalized. */ 333 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) formatDuration(long duration, PrintWriter pw, int fieldLen)334 public static void formatDuration(long duration, PrintWriter pw, int fieldLen) { 335 synchronized (sFormatSync) { 336 int len = formatDurationLocked(duration, fieldLen); 337 pw.print(new String(sFormatStr, 0, len)); 338 } 339 } 340 341 /** @hide Just for debugging; not internationalized. */ 342 @TestApi formatDuration(long duration)343 public static String formatDuration(long duration) { 344 synchronized (sFormatSync) { 345 int len = formatDurationLocked(duration, 0); 346 return new String(sFormatStr, 0, len); 347 } 348 } 349 350 /** @hide Just for debugging; not internationalized. */ 351 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) formatDuration(long duration, PrintWriter pw)352 public static void formatDuration(long duration, PrintWriter pw) { 353 formatDuration(duration, pw, 0); 354 } 355 356 /** @hide Just for debugging; not internationalized. */ formatDuration(long time, long now, PrintWriter pw)357 public static void formatDuration(long time, long now, PrintWriter pw) { 358 if (time == 0) { 359 pw.print("--"); 360 return; 361 } 362 formatDuration(time-now, pw, 0); 363 } 364 365 /** @hide Just for debugging; not internationalized. */ formatUptime(long time)366 public static String formatUptime(long time) { 367 return formatTime(time, SystemClock.uptimeMillis()); 368 } 369 370 /** @hide Just for debugging; not internationalized. */ formatRealtime(long time)371 public static String formatRealtime(long time) { 372 return formatTime(time, SystemClock.elapsedRealtime()); 373 } 374 375 /** @hide Just for debugging; not internationalized. */ formatTime(long time, long referenceTime)376 public static String formatTime(long time, long referenceTime) { 377 long diff = time - referenceTime; 378 if (diff > 0) { 379 return time + " (in " + diff + " ms)"; 380 } 381 if (diff < 0) { 382 return time + " (" + -diff + " ms ago)"; 383 } 384 return time + " (now)"; 385 } 386 387 /** 388 * Convert a System.currentTimeMillis() value to a time of day value like 389 * that printed in logs. MM-DD HH:MM:SS.MMM 390 * 391 * @param millis since the epoch (1/1/1970) 392 * @return String representation of the time. 393 * @hide 394 */ 395 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) logTimeOfDay(long millis)396 public static String logTimeOfDay(long millis) { 397 Calendar c = Calendar.getInstance(); 398 if (millis >= 0) { 399 c.setTimeInMillis(millis); 400 return String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c); 401 } else { 402 return Long.toString(millis); 403 } 404 } 405 406 /** {@hide} */ formatForLogging(long millis)407 public static String formatForLogging(long millis) { 408 if (millis <= 0) { 409 return "unknown"; 410 } else { 411 return sLoggingFormat.format(new Date(millis)); 412 } 413 } 414 415 /** 416 * Dump a currentTimeMillis style timestamp for dumpsys. 417 * 418 * @hide 419 */ dumpTime(PrintWriter pw, long time)420 public static void dumpTime(PrintWriter pw, long time) { 421 pw.print(sDumpDateFormat.format(new Date(time))); 422 } 423 424 /** 425 * This method is used to find if a clock time is inclusively between two other clock times 426 * @param reference The time of the day we want check if it is between start and end 427 * @param start The start time reference 428 * @param end The end time 429 * @return true if the reference time is between the two clock times, and false otherwise. 430 */ isTimeBetween(@onNull LocalTime reference, @NonNull LocalTime start, @NonNull LocalTime end)431 public static boolean isTimeBetween(@NonNull LocalTime reference, 432 @NonNull LocalTime start, 433 @NonNull LocalTime end) { 434 // ////////E----+-----S//////// 435 if ((reference.isBefore(start) && reference.isAfter(end) 436 // -----+----S//////////E------ 437 || (reference.isBefore(end) && reference.isBefore(start) && start.isBefore(end)) 438 // ---------S//////////E---+--- 439 || (reference.isAfter(end) && reference.isAfter(start)) && start.isBefore(end))) { 440 return false; 441 } else { 442 return true; 443 } 444 } 445 446 /** 447 * Dump a currentTimeMillis style timestamp for dumpsys, with the delta time from now. 448 * 449 * @hide 450 */ dumpTimeWithDelta(PrintWriter pw, long time, long now)451 public static void dumpTimeWithDelta(PrintWriter pw, long time, long now) { 452 pw.print(sDumpDateFormat.format(new Date(time))); 453 if (time == now) { 454 pw.print(" (now)"); 455 } else { 456 pw.print(" ("); 457 TimeUtils.formatDuration(time, now, pw); 458 pw.print(")"); 459 } 460 }} 461