blob: 48491dfb47f93545fdd1314d1ed675c968e1f351 [file] [log] [blame]
/*
* Copyright 2019 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.nitz;
import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.createEmptySuggestion;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
import android.os.TimestampedValue;
import android.text.TextUtils;
import android.timezone.CountryTimeZones.OffsetResult;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.NitzData;
import com.android.internal.telephony.NitzStateMachine.DeviceState;
import com.android.internal.telephony.nitz.NitzStateMachineImpl.TimeZoneSuggester;
import com.android.internal.telephony.nitz.TimeZoneLookupHelper.CountryResult;
import com.android.telephony.Rlog;
import java.util.Objects;
/**
* The real implementation of {@link TimeZoneSuggester}.
*/
@VisibleForTesting
public class TimeZoneSuggesterImpl implements TimeZoneSuggester {
private static final String LOG_TAG = NitzStateMachineImpl.LOG_TAG;
private final DeviceState mDeviceState;
private final TimeZoneLookupHelper mTimeZoneLookupHelper;
@VisibleForTesting
public TimeZoneSuggesterImpl(
@NonNull DeviceState deviceState, @NonNull TimeZoneLookupHelper timeZoneLookupHelper) {
mDeviceState = Objects.requireNonNull(deviceState);
mTimeZoneLookupHelper = Objects.requireNonNull(timeZoneLookupHelper);
}
@Override
@NonNull
public TelephonyTimeZoneSuggestion getTimeZoneSuggestion(int slotIndex,
@Nullable String countryIsoCode, @Nullable TimestampedValue<NitzData> nitzSignal) {
try {
// Check for overriding NITZ-based signals from Android running in an emulator.
TelephonyTimeZoneSuggestion overridingSuggestion = null;
if (nitzSignal != null) {
NitzData nitzData = nitzSignal.getValue();
if (nitzData.getEmulatorHostTimeZone() != null) {
TelephonyTimeZoneSuggestion.Builder builder =
new TelephonyTimeZoneSuggestion.Builder(slotIndex)
.setZoneId(nitzData.getEmulatorHostTimeZone().getID())
.setMatchType(TelephonyTimeZoneSuggestion.MATCH_TYPE_EMULATOR_ZONE_ID)
.setQuality(TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE)
.addDebugInfo("Emulator time zone override: " + nitzData);
overridingSuggestion = builder.build();
}
}
TelephonyTimeZoneSuggestion suggestion;
if (overridingSuggestion != null) {
suggestion = overridingSuggestion;
} else if (countryIsoCode == null) {
if (nitzSignal == null) {
suggestion = createEmptySuggestion(slotIndex,
"getTimeZoneSuggestion: nitzSignal=null, countryIsoCode=null");
} else {
// NITZ only - wait until we have a country.
suggestion = createEmptySuggestion(slotIndex, "getTimeZoneSuggestion:"
+ " nitzSignal=" + nitzSignal + ", countryIsoCode=null");
}
} else { // countryIsoCode != null
if (nitzSignal == null) {
if (countryIsoCode.isEmpty()) {
// This is assumed to be a test network with no NITZ data to go on.
suggestion = createEmptySuggestion(slotIndex,
"getTimeZoneSuggestion: nitzSignal=null, countryIsoCode=\"\"");
} else {
// Country only
suggestion = findTimeZoneFromNetworkCountryCode(
slotIndex, countryIsoCode, mDeviceState.currentTimeMillis());
}
} else { // nitzSignal != null
if (countryIsoCode.isEmpty()) {
// We have been told we have a country code but it's empty. This is most
// likely because we're on a test network that's using a bogus MCC
// (eg, "001"). Obtain a TimeZone based only on the NITZ parameters: without
// a country it will be arbitrary, but it should at least have the correct
// offset.
suggestion = findTimeZoneForTestNetwork(slotIndex, nitzSignal);
} else {
// We have both NITZ and Country code.
suggestion = findTimeZoneFromCountryAndNitz(
slotIndex, countryIsoCode, nitzSignal);
}
}
}
// Ensure the return value is never null.
Objects.requireNonNull(suggestion);
return suggestion;
} catch (RuntimeException e) {
// This would suggest a coding error. Log at a high level and try to avoid leaving the
// device in a bad state by making an "empty" suggestion.
String message = "getTimeZoneSuggestion: Error during lookup: "
+ " countryIsoCode=" + countryIsoCode
+ ", nitzSignal=" + nitzSignal
+ ", e=" + e.getMessage();
TelephonyTimeZoneSuggestion errorSuggestion = createEmptySuggestion(slotIndex, message);
Rlog.w(LOG_TAG, message, e);
return errorSuggestion;
}
}
/**
* Creates a {@link TelephonyTimeZoneSuggestion} using only NITZ. This happens when the device
* is attached to a test cell with an unrecognized MCC. In these cases we try to return a
* suggestion for an arbitrary time zone that matches the NITZ offset information.
*/
@NonNull
private TelephonyTimeZoneSuggestion findTimeZoneForTestNetwork(
int slotIndex, @NonNull TimestampedValue<NitzData> nitzSignal) {
Objects.requireNonNull(nitzSignal);
NitzData nitzData = Objects.requireNonNull(nitzSignal.getValue());
TelephonyTimeZoneSuggestion.Builder suggestionBuilder =
new TelephonyTimeZoneSuggestion.Builder(slotIndex);
suggestionBuilder.addDebugInfo("findTimeZoneForTestNetwork: nitzSignal=" + nitzSignal);
OffsetResult lookupResult =
mTimeZoneLookupHelper.lookupByNitz(nitzData);
if (lookupResult == null) {
suggestionBuilder.addDebugInfo("findTimeZoneForTestNetwork: No zone found");
} else {
suggestionBuilder.setZoneId(lookupResult.getTimeZone().getID());
suggestionBuilder.setMatchType(
TelephonyTimeZoneSuggestion.MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY);
int quality = lookupResult.isOnlyMatch()
? TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE
: TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
suggestionBuilder.setQuality(quality);
suggestionBuilder.addDebugInfo(
"findTimeZoneForTestNetwork: lookupResult=" + lookupResult);
}
return suggestionBuilder.build();
}
/**
* Creates a {@link TelephonyTimeZoneSuggestion} using network country code and NITZ.
*/
@NonNull
private TelephonyTimeZoneSuggestion findTimeZoneFromCountryAndNitz(
int slotIndex, @NonNull String countryIsoCode,
@NonNull TimestampedValue<NitzData> nitzSignal) {
Objects.requireNonNull(countryIsoCode);
Objects.requireNonNull(nitzSignal);
TelephonyTimeZoneSuggestion.Builder suggestionBuilder =
new TelephonyTimeZoneSuggestion.Builder(slotIndex);
suggestionBuilder.addDebugInfo("findTimeZoneFromCountryAndNitz:"
+ " countryIsoCode=" + countryIsoCode
+ ", nitzSignal=" + nitzSignal);
NitzData nitzData = Objects.requireNonNull(nitzSignal.getValue());
if (isNitzSignalOffsetInfoBogus(countryIsoCode, nitzData)) {
suggestionBuilder.addDebugInfo(
"findTimeZoneFromCountryAndNitz: NITZ signal looks bogus");
return suggestionBuilder.build();
}
// Try to find a match using both country + NITZ signal.
OffsetResult lookupResult =
mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, countryIsoCode);
if (lookupResult != null) {
suggestionBuilder.setZoneId(lookupResult.getTimeZone().getID());
suggestionBuilder.setMatchType(
TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET);
int quality = lookupResult.isOnlyMatch()
? TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE
: TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
suggestionBuilder.setQuality(quality);
suggestionBuilder.addDebugInfo("findTimeZoneFromCountryAndNitz:"
+ " lookupResult=" + lookupResult);
return suggestionBuilder.build();
}
// The country + offset provided no match, so see if the country by itself would be enough.
CountryResult countryResult = mTimeZoneLookupHelper.lookupByCountry(
countryIsoCode, nitzData.getCurrentTimeInMillis());
if (countryResult == null) {
// Country not recognized.
suggestionBuilder.addDebugInfo(
"findTimeZoneFromCountryAndNitz: lookupByCountry() country not recognized");
return suggestionBuilder.build();
}
// If the country has a single zone, or it has multiple zones but the default zone is
// "boosted" (i.e. the country default is considered a good suggestion in most cases) then
// use it.
if (countryResult.quality == CountryResult.QUALITY_SINGLE_ZONE
|| countryResult.quality == CountryResult.QUALITY_DEFAULT_BOOSTED) {
suggestionBuilder.setZoneId(countryResult.zoneId);
suggestionBuilder.setMatchType(
TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY);
suggestionBuilder.setQuality(TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE);
suggestionBuilder.addDebugInfo(
"findTimeZoneFromCountryAndNitz: high quality country-only suggestion:"
+ " countryResult=" + countryResult);
return suggestionBuilder.build();
}
// Quality is not high enough to set the zone using country only.
suggestionBuilder.addDebugInfo("findTimeZoneFromCountryAndNitz: country-only suggestion"
+ " quality not high enough. countryResult=" + countryResult);
return suggestionBuilder.build();
}
/**
* Creates a {@link TelephonyTimeZoneSuggestion} using only network country code; works well on
* countries which only have one time zone or multiple zones with the same offset.
*
* @param countryIsoCode country code from network MCC
* @param whenMillis the time to use when looking at time zone rules data
*/
@NonNull
private TelephonyTimeZoneSuggestion findTimeZoneFromNetworkCountryCode(
int slotIndex, @NonNull String countryIsoCode, long whenMillis) {
Objects.requireNonNull(countryIsoCode);
if (TextUtils.isEmpty(countryIsoCode)) {
throw new IllegalArgumentException("countryIsoCode must not be empty");
}
TelephonyTimeZoneSuggestion.Builder suggestionBuilder =
new TelephonyTimeZoneSuggestion.Builder(slotIndex);
suggestionBuilder.addDebugInfo("findTimeZoneFromNetworkCountryCode:"
+ " whenMillis=" + whenMillis + ", countryIsoCode=" + countryIsoCode);
CountryResult lookupResult = mTimeZoneLookupHelper.lookupByCountry(
countryIsoCode, whenMillis);
if (lookupResult != null) {
suggestionBuilder.setZoneId(lookupResult.zoneId);
suggestionBuilder.setMatchType(
TelephonyTimeZoneSuggestion.MATCH_TYPE_NETWORK_COUNTRY_ONLY);
int quality;
if (lookupResult.quality == CountryResult.QUALITY_SINGLE_ZONE
|| lookupResult.quality == CountryResult.QUALITY_DEFAULT_BOOSTED) {
quality = TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
} else if (lookupResult.quality == CountryResult.QUALITY_MULTIPLE_ZONES_SAME_OFFSET) {
quality = TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
} else if (lookupResult.quality
== CountryResult.QUALITY_MULTIPLE_ZONES_DIFFERENT_OFFSETS) {
quality = TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
} else {
// This should never happen.
throw new IllegalArgumentException(
"lookupResult.quality not recognized: countryIsoCode=" + countryIsoCode
+ ", whenMillis=" + whenMillis + ", lookupResult=" + lookupResult);
}
suggestionBuilder.setQuality(quality);
suggestionBuilder.addDebugInfo(
"findTimeZoneFromNetworkCountryCode: lookupResult=" + lookupResult);
} else {
suggestionBuilder.addDebugInfo(
"findTimeZoneFromNetworkCountryCode: Country not recognized?");
}
return suggestionBuilder.build();
}
/**
* Returns true if the NITZ signal is definitely bogus, assuming that the country is correct.
*/
private boolean isNitzSignalOffsetInfoBogus(String countryIsoCode, NitzData nitzData) {
if (TextUtils.isEmpty(countryIsoCode)) {
// We cannot say for sure.
return false;
}
boolean zeroOffsetNitz = nitzData.getLocalOffsetMillis() == 0;
return zeroOffsetNitz && !countryUsesUtc(countryIsoCode, nitzData);
}
private boolean countryUsesUtc(String countryIsoCode, NitzData nitzData) {
return mTimeZoneLookupHelper.countryUsesUtc(
countryIsoCode, nitzData.getCurrentTimeInMillis());
}
}