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