content: /*
   * Copyright 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.internal.telephony;
  
  import android.text.TextUtils;
  
  import libcore.timezone.CountryTimeZones;
  import libcore.timezone.TimeZoneFinder;
  
  import java.util.Date;
  import java.util.TimeZone;
  
  /**
   * An interface to various time zone lookup behaviors.
   */
  // Non-final to allow mocking.
  public class TimeZoneLookupHelper {
  
      /**
       * The result of looking up a time zone using offset information (and possibly more).
       */
      public static final class OffsetResult {
  
          /** A zone that matches the supplied criteria. See also {@link #isOnlyMatch}. */
          public final String zoneId;
  
          /** True if there is only one matching time zone for the supplied criteria. */
          public final boolean isOnlyMatch;
  
          public OffsetResult(String zoneId, boolean isOnlyMatch) {
              this.zoneId = zoneId;
              this.isOnlyMatch = isOnlyMatch;
          }
  
          @Override
          public boolean equals(Object o) {
              if (this == o) {
                  return true;
              }
              if (o == null || getClass() != o.getClass()) {
                  return false;
              }
  
              OffsetResult result = (OffsetResult) o;
  
              if (isOnlyMatch != result.isOnlyMatch) {
                  return false;
              }
              return zoneId.equals(result.zoneId);
          }
  
          @Override
          public int hashCode() {
              int result = zoneId.hashCode();
              result = 31 * result + (isOnlyMatch ? 1 : 0);
              return result;
          }
  
          @Override
          public String toString() {
              return "Result{"
                      + "zoneId='" + zoneId + '\''
                      + ", isOnlyMatch=" + isOnlyMatch
                      + '}';
          }
      }
  
      /**
       * The result of looking up a time zone using country information.
       */
      public static final class CountryResult {
  
          /** A time zone for the country. */
          public final String zoneId;
  
          /**
           * True if all the time zones in the country have the same offset at {@link #whenMillis}.
           */
          public final boolean allZonesHaveSameOffset;
  
          /** The time associated with {@link #allZonesHaveSameOffset}. */
          public final long whenMillis;
  
          public CountryResult(String zoneId, boolean allZonesHaveSameOffset, long whenMillis) {
              this.zoneId = zoneId;
              this.allZonesHaveSameOffset = allZonesHaveSameOffset;
              this.whenMillis = whenMillis;
          }
  
          @Override
          public boolean equals(Object o) {
              if (this == o) {
                  return true;
              }
              if (o == null || getClass() != o.getClass()) {
                  return false;
              }
  
              CountryResult that = (CountryResult) o;
  
              if (allZonesHaveSameOffset != that.allZonesHaveSameOffset) {
                  return false;
              }
              if (whenMillis != that.whenMillis) {
                  return false;
              }
              return zoneId.equals(that.zoneId);
          }
  
          @Override
          public int hashCode() {
              int result = zoneId.hashCode();
              result = 31 * result + (allZonesHaveSameOffset ? 1 : 0);
              result = 31 * result + (int) (whenMillis ^ (whenMillis >>> 32));
              return result;
          }
  
          @Override
          public String toString() {
              return "CountryResult{"
                      + "zoneId='" + zoneId + '\''
                      + ", allZonesHaveSameOffset=" + allZonesHaveSameOffset
                      + ", whenMillis=" + whenMillis
                      + '}';
          }
      }
  
      private static final int MS_PER_HOUR = 60 * 60 * 1000;
  
      /** The last CountryTimeZones object retrieved. */
      private CountryTimeZones mLastCountryTimeZones;
  
      public TimeZoneLookupHelper() {}
  
      /**
       * Looks for a time zone for the supplied NITZ and country information.
       *
       * <p><em>Note:</em> When there are multiple matching zones then one of the matching candidates
       * will be returned in the result. If the current device default zone matches it will be
       * returned in preference to other candidates. This method can return {@code null} if no
       * matching time zones are found.
       */
      public OffsetResult lookupByNitzCountry(NitzData nitzData, String isoCountryCode) {
          CountryTimeZones countryTimeZones = getCountryTimeZones(isoCountryCode);
          if (countryTimeZones == null) {
              return null;
          }
          android.icu.util.TimeZone bias = android.icu.util.TimeZone.getDefault();
  
          CountryTimeZones.OffsetResult offsetResult = countryTimeZones.lookupByOffsetWithBias(
                  nitzData.getLocalOffsetMillis(), nitzData.isDst(),
                  nitzData.getCurrentTimeInMillis(), bias);
  
          if (offsetResult == null) {
              return null;
          }
          return new OffsetResult(offsetResult.mTimeZone.getID(), offsetResult.mOneMatch);
      }
  
      /**
       * Looks for a time zone using only information present in the supplied {@link NitzData} object.
       *
       * <p><em>Note:</em> Because multiple time zones can have the same offset / DST state at a given
       * time this process is error prone; an arbitrary match is returned when there are multiple
       * candidates. The algorithm can also return a non-exact match by assuming that the DST
       * information provided by NITZ is incorrect. This method can return {@code null} if no matching
       * time zones are found.
       */
      public OffsetResult lookupByNitz(NitzData nitzData) {
          return lookupByNitzStatic(nitzData);
      }
  
      /**
       * Returns a time zone ID for the country if possible. For counties that use a single time zone
       * this will provide a good choice. For countries with multiple time zones, a time zone is
       * returned if all time zones used in the country currently have the same offset (currently ==
       * according to the device's current system clock time). If this is not the case then
       * {@code null} can be returned.
       */
      public CountryResult lookupByCountry(String isoCountryCode, long whenMillis) {
          CountryTimeZones countryTimeZones = getCountryTimeZones(isoCountryCode);
          if (countryTimeZones == null) {
              // Unknown country code.
              return null;
          }
          if (countryTimeZones.getDefaultTimeZoneId() == null) {
              return null;
          }
  
          return new CountryResult(
                  countryTimeZones.getDefaultTimeZoneId(),
                  countryTimeZones.isDefaultOkForCountryTimeZoneDetection(whenMillis),
                  whenMillis);
      }
  
      /**
       * Finds a time zone using only information present in the supplied {@link NitzData} object.
       * This is a static method for use by {@link ServiceStateTracker}.
       *
       * <p><em>Note:</em> Because multiple time zones can have the same offset / DST state at a given
       * time this process is error prone; an arbitrary match is returned when there are multiple
       * candidates. The algorithm can also return a non-exact match by assuming that the DST
       * information provided by NITZ is incorrect. This method can return {@code null} if no matching
       * time zones are found.
       */
      static TimeZone guessZoneByNitzStatic(NitzData nitzData) {
          OffsetResult result = lookupByNitzStatic(nitzData);
          return result != null ? TimeZone.getTimeZone(result.zoneId) : null;
      }
  
      private static OffsetResult lookupByNitzStatic(NitzData nitzData) {
          int utcOffsetMillis = nitzData.getLocalOffsetMillis();
          boolean isDst = nitzData.isDst();
          long timeMillis = nitzData.getCurrentTimeInMillis();
  
          OffsetResult match = lookupByInstantOffsetDst(timeMillis, utcOffsetMillis, isDst);
          if (match == null) {
              // Couldn't find a proper timezone.  Perhaps the DST data is wrong.
              match = lookupByInstantOffsetDst(timeMillis, utcOffsetMillis, !isDst);
          }
          return match;
      }
  
      private static OffsetResult lookupByInstantOffsetDst(long timeMillis, int utcOffsetMillis,
              boolean isDst) {
          int rawOffset = utcOffsetMillis;
          if (isDst) {
              rawOffset -= MS_PER_HOUR;
          }
          String[] zones = TimeZone.getAvailableIDs(rawOffset);
          TimeZone match = null;
          Date d = new Date(timeMillis);
          boolean isOnlyMatch = true;
          for (String zone : zones) {
              TimeZone tz = TimeZone.getTimeZone(zone);
              if (tz.getOffset(timeMillis) == utcOffsetMillis && tz.inDaylightTime(d) == isDst) {
                  if (match == null) {
                      match = tz;
                  } else {
                      isOnlyMatch = false;
                      break;
                  }
              }
          }
  
          if (match == null) {
              return null;
          }
          return new OffsetResult(match.getID(), isOnlyMatch);
      }
  
      /**
       * Returns {@code true} if the supplied (lower-case) ISO country code is for a country known to
       * use a raw offset of zero from UTC at the time specified.
       */
      public boolean countryUsesUtc(String isoCountryCode, long whenMillis) {
          if (TextUtils.isEmpty(isoCountryCode)) {
              return false;
          }
  
          CountryTimeZones countryTimeZones = getCountryTimeZones(isoCountryCode);
          return countryTimeZones != null && countryTimeZones.hasUtcZone(whenMillis);
      }
  
      private CountryTimeZones getCountryTimeZones(String isoCountryCode) {
          // A single entry cache of the last CountryTimeZones object retrieved since there should
          // be strong consistency across calls.
          synchronized (this) {
              if (mLastCountryTimeZones != null) {
                  if (mLastCountryTimeZones.isForCountryCode(isoCountryCode)) {
                      return mLastCountryTimeZones;
                  }
              }
  
              // Perform the lookup. It's very unlikely to return null, but we won't cache null.
              CountryTimeZones countryTimeZones =
                      TimeZoneFinder.getInstance().lookupCountryTimeZones(isoCountryCode);
              if (countryTimeZones != null) {
                  mLastCountryTimeZones = countryTimeZones;
              }
              return countryTimeZones;
          }
      }
  }
  