Home | History | Annotate | Download | only in telephony
      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 
     44         public OffsetResult(String zoneId, boolean isOnlyMatch) {
     45             this.zoneId = zoneId;
     46             this.isOnlyMatch = isOnlyMatch;
     47         }
     48 
     49         @Override
     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
     67         public int hashCode() {
     68             int result = zoneId.hashCode();
     69             result = 31 * result + (isOnlyMatch ? 1 : 0);
     70             return result;
     71         }
     72 
     73         @Override
     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 
     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
    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
    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
    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 
    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      */
    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      */
    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      */
    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      */
    220     static TimeZone guessZoneByNitzStatic(NitzData nitzData) {
    221         OffsetResult result = lookupByNitzStatic(nitzData);
    222         return result != null ? TimeZone.getTimeZone(result.zoneId) : null;
    223     }
    224 
    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 
    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      */
    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 
    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