blob: 04c6c713c5e03aa16ca4e065cec8a7b72c303282 [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.app.timedetector.PhoneTimeSuggestion;
import android.content.Context;
import android.os.PowerManager;
import android.os.TimestampedValue;
import com.android.telephony.Rlog;
import android.text.TextUtils;
import android.util.LocalLog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.TimeZoneLookupHelper.CountryResult;
import com.android.internal.telephony.TimeZoneLookupHelper.OffsetResult;
import com.android.internal.telephony.metrics.TelephonyMetrics;
import com.android.internal.util.IndentingPrintWriter;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Objects;
/**
* {@hide}
*/
public final class NitzStateMachineImpl implements NitzStateMachine {
private static final String LOG_TAG = ServiceStateTracker.LOG_TAG;
private static final boolean DBG = ServiceStateTracker.DBG;
// Time detection state.
/**
* The last NITZ-sourced time considered sent to the time detector service. Used to rate-limit
* calls to the time detector.
*/
private TimestampedValue<Long> mSavedNitzTime;
// Time Zone detection state.
/** We always keep the last NITZ signal received in mLatestNitzSignal. */
private TimestampedValue<NitzData> mLatestNitzSignal;
/**
* Records the country to use for time zone detection. It can be a valid ISO 3166 alpha-2 code
* (lower case), empty (test network) or null (no country detected). A country code is required
* to determine time zone except when on a test network.
*/
private String mCountryIsoCode;
/**
* The last time zone ID that has been determined. It may not have been set as the device time
* zone if automatic time zone detection is disabled but may later be used to set the time zone
* if the user enables automatic time zone detection.
*/
private String mSavedTimeZoneId;
/**
* The last time zone ID that was set. It is used for log entry filtering. This is different
* from {@link #mSavedTimeZoneId} in that this records the last zone ID this class actually
* suggested should be set as the device zone ID; i.e. it is only set if automatic time zone
* detection is enabled.
*/
private String mLastSetTimeZoneId;
/**
* Boolean is {@code true} if NITZ has been used to determine a time zone (which may not
* ultimately have been used due to user settings). Cleared by {@link
* #handleNetworkAvailable()}, {@link #handleCountryUnavailable()},
* {@link #handleNetworkUnavailable()}, and {@link #handleAirplaneModeChanged(boolean)}. The
* flag can be used when historic NITZ data may no longer be valid. {@code false} indicates it
* is reasonable to try to set the time zone using less reliable algorithms than NITZ-based
* detection such as by just using network country code.
*/
private boolean mNitzTimeZoneDetectionSuccessful = false;
// Miscellaneous dependencies and helpers not related to detection state.
private final LocalLog mTimeLog = new LocalLog(30, false /* useLocalTimestamps */);
private final LocalLog mTimeZoneLog = new LocalLog(30, false /* useLocalTimestamps */);
private final Phone mPhone;
private final DeviceState mDeviceState;
private final TimeServiceHelper mTimeServiceHelper;
private final TimeZoneLookupHelper mTimeZoneLookupHelper;
/** Wake lock used while setting time of day. */
private final PowerManager.WakeLock mWakeLock;
private static final String WAKELOCK_TAG = "NitzStateMachine";
public NitzStateMachineImpl(Phone phone) {
this(phone,
new TimeServiceHelperImpl(phone.getContext()),
new DeviceStateImpl(phone),
new TimeZoneLookupHelper());
}
@VisibleForTesting
public NitzStateMachineImpl(Phone phone, TimeServiceHelper timeServiceHelper,
DeviceState deviceState, TimeZoneLookupHelper timeZoneLookupHelper) {
mPhone = phone;
Context context = phone.getContext();
PowerManager powerManager =
(PowerManager) context.getSystemService(Context.POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
mDeviceState = deviceState;
mTimeZoneLookupHelper = timeZoneLookupHelper;
mTimeServiceHelper = timeServiceHelper;
mTimeServiceHelper.setListener(new TimeServiceHelper.Listener() {
@Override
public void onTimeZoneDetectionChange(boolean enabled) {
if (enabled) {
handleAutoTimeZoneEnabled();
}
}
});
}
@Override
public void handleCountryDetected(String countryIsoCode) {
String oldCountryIsoCode = mCountryIsoCode;
mCountryIsoCode = Objects.requireNonNull(countryIsoCode);
if (!TextUtils.isEmpty(countryIsoCode) && !mNitzTimeZoneDetectionSuccessful) {
updateTimeZoneFromNetworkCountryCode(countryIsoCode);
}
boolean countryChanged = Objects.equals(oldCountryIsoCode, countryIsoCode);
if (mLatestNitzSignal != null && (countryChanged || oldCountryIsoCode == null)) {
updateTimeZoneFromCountryAndNitz();
}
}
private void updateTimeZoneFromCountryAndNitz() {
// This method must only be called after mLatestNitzSignal has been set to a non-null
// value.
TimestampedValue<NitzData> nitzSignal = Objects.requireNonNull(mLatestNitzSignal);
String isoCountryCode = mCountryIsoCode;
// TimeZone.getDefault() returns a default zone (GMT) even when time zone have never
// been set which makes it difficult to tell if it's what the user / time zone detection
// has chosen. isTimeZoneSettingInitialized() tells us whether the time zone of the
// device has ever been explicitly set by the user or code.
final boolean isTimeZoneSettingInitialized =
mTimeServiceHelper.isTimeZoneSettingInitialized();
if (DBG) {
Rlog.d(LOG_TAG, "updateTimeZoneFromCountryAndNitz:"
+ " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
+ " nitzSignal=" + nitzSignal
+ " isoCountryCode=" + isoCountryCode);
}
try {
NitzData nitzData = nitzSignal.getValue();
String zoneId;
if (nitzData.getEmulatorHostTimeZone() != null) {
zoneId = nitzData.getEmulatorHostTimeZone().getID();
} else if (isoCountryCode == null) {
// We don't have a country code so we won't try to look up the time zone.
zoneId = null;
} else if (TextUtils.isEmpty(isoCountryCode)) {
// 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: it's only going to be correct in a few cases but it
// should at least have the correct offset.
OffsetResult lookupResult = mTimeZoneLookupHelper.lookupByNitz(nitzData);
String logMsg = "updateTimeZoneFromCountryAndNitz: lookupByNitz returned"
+ " lookupResult=" + lookupResult;
if (DBG) {
Rlog.d(LOG_TAG, logMsg);
}
// We log this in the time zone log because it has been a source of bugs.
mTimeZoneLog.log(logMsg);
zoneId = lookupResult != null ? lookupResult.getTimeZone().getID() : null;
} else if (isTimeZoneSettingInitialized
&& isNitzSignalOffsetInfoBogus(nitzSignal, isoCountryCode)) {
String logMsg = "updateTimeZoneFromCountryAndNitz: Received NITZ looks bogus, "
+ " isoCountryCode=" + isoCountryCode
+ " nitzSignal=" + nitzSignal;
if (DBG) {
Rlog.d(LOG_TAG, logMsg);
}
// We log this in the time zone log because it has been a source of bugs.
mTimeZoneLog.log(logMsg);
zoneId = null;
} else {
OffsetResult lookupResult =
mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, isoCountryCode);
if (lookupResult != null) {
if (DBG) {
Rlog.d(LOG_TAG, "updateTimeZoneFromCountryAndNitz: using"
+ " lookupByNitzCountry(nitzData, isoCountryCode),"
+ " nitzData=" + nitzData
+ " isoCountryCode=" + isoCountryCode
+ " lookupResult=" + lookupResult);
}
zoneId = lookupResult.getTimeZone().getID();
} else {
// The country + offset provided no match, so see if the country by itself
// would be enough.
CountryResult countryResult = mTimeZoneLookupHelper.lookupByCountry(
isoCountryCode, nitzData.getCurrentTimeInMillis());
if (DBG) {
Rlog.d(LOG_TAG, "updateTimeZoneFromCountryAndNitz: fallback to"
+ " lookupByCountry(isoCountryCode, whenMillis),"
+ " nitzData=" + nitzData
+ " isoCountryCode=" + isoCountryCode
+ " countryResult=" + countryResult);
}
if (countryResult != null) {
// If the country has a single zone, or it has multiple zones but the
// default is "boosted" (i.e. it is considered a good result in most cases)
// then use it.
if (countryResult.quality == CountryResult.QUALITY_SINGLE_ZONE
|| countryResult.quality == CountryResult.QUALITY_DEFAULT_BOOSTED) {
zoneId = countryResult.zoneId;
} else {
// Quality is not high enough.
zoneId = null;
}
} else {
// Country not recognized.
zoneId = null;
}
}
}
String logMsg = "updateTimeZoneFromCountryAndNitz:"
+ " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
+ " isoCountryCode=" + isoCountryCode
+ " nitzSignal=" + nitzSignal
+ " zoneId=" + zoneId
+ " isTimeZoneDetectionEnabled()="
+ mTimeServiceHelper.isTimeZoneDetectionEnabled();
// Set state as needed.
if (zoneId != null) {
if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
setTimeZone(zoneId, logMsg);
} else {
if (DBG) {
logMsg += " [Not setting device time zone]";
Rlog.d(LOG_TAG, logMsg);
}
}
mSavedTimeZoneId = zoneId;
mNitzTimeZoneDetectionSuccessful = true;
} else {
if (DBG) {
logMsg += " [Not setting device time zone (zoneId == null)]";
Rlog.d(LOG_TAG, logMsg);
}
mSavedTimeZoneId = null;
mNitzTimeZoneDetectionSuccessful = false;
}
} catch (RuntimeException ex) {
Rlog.e(LOG_TAG, "updateTimeZoneFromCountryAndNitz: Processing NITZ data"
+ " nitzSignal=" + nitzSignal
+ " isoCountryCode=" + isoCountryCode
+ " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
+ " ex=" + ex);
}
}
/**
* Returns true if the NITZ signal is definitely bogus, assuming that the country is correct.
*/
private boolean isNitzSignalOffsetInfoBogus(
TimestampedValue<NitzData> nitzSignal, String isoCountryCode) {
if (TextUtils.isEmpty(isoCountryCode)) {
// We cannot say for sure.
return false;
}
NitzData newNitzData = nitzSignal.getValue();
boolean zeroOffsetNitz = newNitzData.getLocalOffsetMillis() == 0;
return zeroOffsetNitz && !countryUsesUtc(isoCountryCode, nitzSignal);
}
private boolean countryUsesUtc(
String isoCountryCode, TimestampedValue<NitzData> nitzSignal) {
return mTimeZoneLookupHelper.countryUsesUtc(
isoCountryCode,
nitzSignal.getValue().getCurrentTimeInMillis());
}
@Override
public void handleNetworkAvailable() {
if (DBG) {
Rlog.d(LOG_TAG, "handleNetworkAvailable: mNitzTimeZoneDetectionSuccessful="
+ mNitzTimeZoneDetectionSuccessful
+ ", Setting mNitzTimeZoneDetectionSuccessful=false");
}
mNitzTimeZoneDetectionSuccessful = false;
}
@Override
public void handleNetworkUnavailable() {
if (DBG) {
Rlog.d(LOG_TAG, "handleNetworkUnavailable: Clearing NITZ and detection state");
}
// Clear state related to NITZ.
mSavedNitzTime = null;
mTimeLog.log("handleNetworkUnavailable: NITZ state cleared.");
TimestampedValue<NitzData> oldNitzSignal = mLatestNitzSignal;
mLatestNitzSignal = null;
mNitzTimeZoneDetectionSuccessful = false;
mSavedTimeZoneId = null;
mTimeZoneLog.log("handleNetworkUnavailable: NITZ state cleared.");
// Avoid doing work unnecessarily: if the mLatestNitzSignal was already null we have nothing
// to do as it will have been done last time the mLatestNitzSignal was cleared.
if (oldNitzSignal == null) {
return;
}
// mSavedTimeZoneId has been cleared but using only the country information that is left
// might be sufficient to detect the time zone.
String isoCountryCode = mCountryIsoCode;
// We don't need to do country-based time zone detection if the isoCountryCode is null
// (unknown) or empty (test cell). TextUtils.isEmpty() does both checks in one.
if (!TextUtils.isEmpty(isoCountryCode)) {
updateTimeZoneFromNetworkCountryCode(isoCountryCode);
}
sendEmptyTimeSuggestion("handleNetworkUnavailable");
}
@Override
public void handleCountryUnavailable() {
if (DBG) {
Rlog.d(LOG_TAG, "handleNetworkCountryCodeUnavailable");
}
mSavedTimeZoneId = null;
mCountryIsoCode = null;
mNitzTimeZoneDetectionSuccessful = false;
}
@Override
public void handleNitzReceived(TimestampedValue<NitzData> nitzSignal) {
// Always store the latest NITZ signal received.
mLatestNitzSignal = Objects.requireNonNull(nitzSignal);
updateTimeZoneFromCountryAndNitz();
updateTimeFromNitz();
}
@Override
public void handleAirplaneModeChanged(boolean on) {
if (DBG) {
Rlog.d(LOG_TAG, "handleAirplaneModeChanged: on=" + on);
}
// Treat entry / exit from airplane mode as a strong signal that the user wants to clear
// cached state. If the user really is boarding a plane they won't want cached state from
// before their flight influencing behavior.
//
// State is cleared on entry AND exit: on entry because the detection code shouldn't be
// opinionated while in airplane mode, and on exit to avoid any unexpected signals received
// while in airplane mode from influencing behavior afterwards.
//
// After clearing detection state, the time zone detection should work out from first
// principles what the time / time zone is. This assumes calls like handleNetworkAvailable()
// will be made after airplane mode is re-enabled as the device re-establishes network
// connectivity.
mSavedNitzTime = null;
mTimeLog.log("handleAirplaneModeChanged(" + on + "): Time state cleared.");
mCountryIsoCode = null;
TimestampedValue<NitzData> oldNitzSignal = mLatestNitzSignal;
mLatestNitzSignal = null;
mNitzTimeZoneDetectionSuccessful = false;
mSavedTimeZoneId = null;
if (oldNitzSignal == null) {
// No work to do - terminate early.
return;
}
mTimeZoneLog.log("handleAirplaneModeChanged(" + on + "): Time zone state cleared.");
sendEmptyTimeSuggestion("handleAirplaneModeChanged(" + on + ")");
}
private void sendEmptyTimeSuggestion(String reason) {
PhoneTimeSuggestion timeSuggestion =
new PhoneTimeSuggestion.Builder(mPhone.getPhoneId()).build();
timeSuggestion.addDebugInfo("Empty suggestion, reason=" + reason);
mTimeServiceHelper.suggestDeviceTime(timeSuggestion);
}
private void updateTimeFromNitz() {
TimestampedValue<NitzData> nitzSignal = Objects.requireNonNull(mLatestNitzSignal);
try {
boolean ignoreNitz = mDeviceState.getIgnoreNitz();
if (ignoreNitz) {
if (DBG) {
Rlog.d(LOG_TAG, "updateTimeFromNitz: Not suggesting system clock because"
+ " gsm.ignore-nitz is set");
}
return;
}
// Validate the nitzTimeSignal to reject obviously bogus elapsedRealtime values.
try {
// Acquire the wake lock as we are reading the elapsed realtime clock below.
mWakeLock.acquire();
long elapsedRealtime = mDeviceState.elapsedRealtime();
long millisSinceNitzReceived =
elapsedRealtime - nitzSignal.getReferenceTimeMillis();
if (millisSinceNitzReceived < 0 || millisSinceNitzReceived > Integer.MAX_VALUE) {
if (DBG) {
Rlog.d(LOG_TAG, "updateTimeFromNitz: not setting time, unexpected"
+ " elapsedRealtime=" + elapsedRealtime
+ " nitzSignal=" + nitzSignal);
}
return;
}
} finally {
mWakeLock.release();
}
TimestampedValue<Long> newNitzTime = new TimestampedValue<>(
nitzSignal.getReferenceTimeMillis(),
nitzSignal.getValue().getCurrentTimeInMillis());
// Perform rate limiting: a NITZ signal received too close to a previous
// one will be disregarded unless there is a significant difference between the
// UTC times they represent.
if (mSavedNitzTime != null) {
int nitzUpdateSpacing = mDeviceState.getNitzUpdateSpacingMillis();
int nitzUpdateDiff = mDeviceState.getNitzUpdateDiffMillis();
// Calculate the elapsed time between the new signal and the last signal.
long elapsedRealtimeSinceLastSaved = newNitzTime.getReferenceTimeMillis()
- mSavedNitzTime.getReferenceTimeMillis();
// Calculate the UTC difference between the time the two signals hold.
long utcTimeDifferenceMillis =
newNitzTime.getValue() - mSavedNitzTime.getValue();
// Ideally the difference between elapsedRealtimeSinceLastSaved and
// utcTimeDifferenceMillis would be zero.
long millisGained = utcTimeDifferenceMillis - elapsedRealtimeSinceLastSaved;
if (elapsedRealtimeSinceLastSaved <= nitzUpdateSpacing
&& Math.abs(millisGained) <= nitzUpdateDiff) {
if (DBG) {
Rlog.d(LOG_TAG, "updateTimeFromNitz: not setting time. NITZ signal is"
+ " too similar to previous value received "
+ " mSavedNitzTime=" + mSavedNitzTime
+ ", nitzSignal=" + nitzSignal
+ ", nitzUpdateSpacing=" + nitzUpdateSpacing
+ ", nitzUpdateDiff=" + nitzUpdateDiff);
}
return;
}
}
String logMsg = "updateTimeFromNitz: suggesting system clock update"
+ " nitzSignal=" + nitzSignal
+ ", newNitzTime=" + newNitzTime
+ ", mSavedNitzTime= " + mSavedNitzTime;
if (DBG) {
Rlog.d(LOG_TAG, logMsg);
}
mTimeLog.log(logMsg);
PhoneTimeSuggestion phoneTimeSuggestion =
new PhoneTimeSuggestion.Builder(mPhone.getPhoneId())
.setUtcTime(newNitzTime)
.addDebugInfo(logMsg)
.build();
mTimeServiceHelper.suggestDeviceTime(phoneTimeSuggestion);
TelephonyMetrics.getInstance().writeNITZEvent(
mPhone.getPhoneId(), newNitzTime.getValue());
// Save the last NITZ time signal that was suggested to enable rate limiting.
mSavedNitzTime = newNitzTime;
} catch (RuntimeException ex) {
Rlog.e(LOG_TAG, "updateTimeFromNitz: Processing NITZ data"
+ " nitzSignal=" + nitzSignal
+ " ex=" + ex);
}
}
private void setTimeZone(String zoneId, String logMessage) {
logMessage += " [Setting device time zone to zoneId=" + zoneId + "]";
if (DBG) {
Rlog.d(LOG_TAG, logMessage);
}
// Filter mTimeZoneLog entries to only store "interesting" ones. NITZ signals can be
// quite frequent (e.g. every few minutes) and logging each one soon obliterates useful
// entries from bug reports. http://b/138187241
if (!zoneId.equals(mLastSetTimeZoneId)) {
mTimeZoneLog.log(logMessage);
mLastSetTimeZoneId = zoneId;
}
mTimeServiceHelper.setDeviceTimeZone(zoneId);
if (DBG) {
Rlog.d(LOG_TAG,
"setAndBroadcastNetworkSetTimeZone: called setDeviceTimeZone()"
+ " zoneId=" + zoneId);
}
}
private void handleAutoTimeZoneEnabled() {
String logMsg = "handleAutoTimeZoneEnabled: "
+ " mSavedTimeZoneId=" + mSavedTimeZoneId;
if (mSavedTimeZoneId != null) {
setTimeZone(mSavedTimeZoneId, logMsg);
} else {
if (DBG) {
logMsg += " [Not setting device time zone]";
Rlog.d(LOG_TAG, logMsg);
}
}
}
@Override
public void dumpState(PrintWriter pw) {
// Time Detection State
pw.println(" mSavedTime=" + mSavedNitzTime);
// Time Zone Detection State
pw.println(" mLatestNitzSignal=" + mLatestNitzSignal);
pw.println(" mCountryIsoCode=" + mCountryIsoCode);
pw.println(" mSavedTimeZoneId=" + mSavedTimeZoneId);
pw.println(" mNitzTimeZoneDetectionSuccessful=" + mNitzTimeZoneDetectionSuccessful);
// Miscellaneous
pw.println(" mWakeLock=" + mWakeLock);
pw.flush();
}
@Override
public void dumpLogs(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
ipw.println(" Time Logs:");
ipw.increaseIndent();
mTimeLog.dump(fd, ipw, args);
ipw.decreaseIndent();
ipw.println(" Time zone Logs:");
ipw.increaseIndent();
mTimeZoneLog.dump(fd, ipw, args);
ipw.decreaseIndent();
}
/**
* Update time zone by network country code, works well on countries which only have one time
* zone or multiple zones with the same offset.
*
* @param iso Country code from network MCC
*/
private void updateTimeZoneFromNetworkCountryCode(String iso) {
CountryResult lookupResult = mTimeZoneLookupHelper.lookupByCountry(
iso, mDeviceState.currentTimeMillis());
boolean isTimeZoneSettingInitialized = mTimeServiceHelper.isTimeZoneSettingInitialized();
if (lookupResult != null
&& (!isTimeZoneSettingInitialized
|| lookupResult.quality == CountryResult.QUALITY_SINGLE_ZONE
|| lookupResult.quality == CountryResult.QUALITY_DEFAULT_BOOSTED)) {
String logMsg = "updateTimeZoneFromNetworkCountryCode: tz result found"
+ " iso=" + iso
+ " lookupResult=" + lookupResult
+ " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized;
String zoneId = lookupResult.zoneId;
if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
setTimeZone(zoneId, logMsg);
} else {
if (DBG) {
logMsg += " [Not setting device time zone]";
Rlog.d(LOG_TAG, logMsg);
}
}
mSavedTimeZoneId = zoneId;
} else {
if (DBG) {
Rlog.d(LOG_TAG, "updateTimeZoneFromNetworkCountryCode: no good zone for"
+ " iso=" + iso
+ " lookupResult=" + lookupResult);
}
mSavedTimeZoneId = null;
}
}
// VisibleForTesting
public boolean getNitzTimeZoneDetectionSuccessful() {
return mNitzTimeZoneDetectionSuccessful;
}
// VisibleForTesting
public NitzData getCachedNitzData() {
return mLatestNitzSignal != null ? mLatestNitzSignal.getValue() : null;
}
// VisibleForTesting
public String getSavedTimeZoneId() {
return mSavedTimeZoneId;
}
}