blob: a0f74472eb9533799ff579232afdd977a72714a9 [file] [log] [blame]
/*
* 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;
}
}
}