blob: d682164c988d11a41d28b994e16dc214465eafc5 [file] [log] [blame]
/*
* Copyright (C) 2020 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.server.location.gnss;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;
import android.util.Log;
import android.util.NtpTrustedTime;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
/**
* Handles injecting network time to GNSS by explicitly making NTP requests when needed.
*/
class NtpNetworkTimeHelper extends NetworkTimeHelper {
private static final String TAG = "NtpNetworkTimeHelper";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// states for injecting ntp
private static final int STATE_PENDING_NETWORK = 0;
private static final int STATE_RETRIEVING_AND_INJECTING = 1;
private static final int STATE_IDLE = 2;
// how often to request NTP time, in milliseconds
// current setting 24 hours
@VisibleForTesting
static final long NTP_INTERVAL = 24 * 60 * 60 * 1000;
// how long to wait if we have a network error in NTP
// the initial value of the exponential backoff
// current setting - 5 minutes
@VisibleForTesting
static final long RETRY_INTERVAL = 5 * 60 * 1000;
// how long to wait if we have a network error in NTP
// the max value of the exponential backoff
// current setting - 4 hours
private static final long MAX_RETRY_INTERVAL = 4 * 60 * 60 * 1000;
private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
private static final String WAKELOCK_KEY = "NtpTimeHelper";
private final LocalLog mDumpLog = new LocalLog(10, /*useLocalTimestamps=*/false);
@GuardedBy("this")
private final ExponentialBackOff mNtpBackOff = new ExponentialBackOff(RETRY_INTERVAL,
MAX_RETRY_INTERVAL);
private final ConnectivityManager mConnMgr;
private final NtpTrustedTime mNtpTime;
private final WakeLock mWakeLock;
private final Handler mHandler;
private final InjectTimeCallback mCallback;
// flags to trigger NTP when network becomes available
// initialized to STATE_PENDING_NETWORK so we do NTP when the network comes up after booting
@GuardedBy("this")
private int mInjectNtpTimeState = STATE_PENDING_NETWORK;
// Enables periodic time injection in addition to injection for other reasons.
@GuardedBy("this")
private boolean mPeriodicTimeInjection;
@VisibleForTesting
NtpNetworkTimeHelper(Context context, Looper looper, InjectTimeCallback callback,
NtpTrustedTime ntpTime) {
mConnMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
mCallback = callback;
mNtpTime = ntpTime;
mHandler = new Handler(looper);
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
}
NtpNetworkTimeHelper(Context context, Looper looper, InjectTimeCallback callback) {
this(context, looper, callback, NtpTrustedTime.getInstance(context));
}
@Override
synchronized void setPeriodicTimeInjectionMode(boolean periodicTimeInjectionEnabled) {
if (periodicTimeInjectionEnabled) {
mPeriodicTimeInjection = true;
}
}
@Override
void demandUtcTimeInjection() {
retrieveAndInjectNtpTime("demandUtcTimeInjection");
}
@Override
synchronized void onNetworkAvailable() {
if (mInjectNtpTimeState == STATE_PENDING_NETWORK) {
retrieveAndInjectNtpTime("onNetworkAvailable");
}
}
@Override
void dump(PrintWriter pw) {
pw.println("NtpNetworkTimeHelper:");
IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
ipw.increaseIndent();
synchronized (this) {
ipw.println("mInjectNtpTimeState=" + mInjectNtpTimeState);
ipw.println("mPeriodicTimeInjection=" + mPeriodicTimeInjection);
ipw.println("mNtpBackOff=" + mNtpBackOff);
}
ipw.println("Debug log:");
ipw.increaseIndent();
mDumpLog.dump(ipw);
ipw.decreaseIndent();
ipw.println("NtpTrustedTime:");
ipw.increaseIndent();
mNtpTime.dump(ipw);
ipw.decreaseIndent();
}
/**
* @return {@code true} if there is a network available for outgoing connections,
* {@code false} otherwise.
*/
private boolean isNetworkConnected() {
NetworkInfo activeNetworkInfo = mConnMgr.getActiveNetworkInfo();
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}
private synchronized void retrieveAndInjectNtpTime(String reason) {
if (mInjectNtpTimeState == STATE_RETRIEVING_AND_INJECTING) {
// already downloading data
return;
}
if (!isNetworkConnected()) {
// try to inject the cached NTP time
maybeInjectCachedNtpTime(reason + "[Network not connected]");
// try again when network is up
mInjectNtpTimeState = STATE_PENDING_NETWORK;
return;
}
mInjectNtpTimeState = STATE_RETRIEVING_AND_INJECTING;
// hold wake lock while task runs
mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
new Thread(this::blockingGetNtpTimeAndInject).start();
}
/** {@link NtpTrustedTime#forceRefresh} is a blocking network operation. */
private void blockingGetNtpTimeAndInject() {
long debugId = SystemClock.elapsedRealtime();
long delayMillis;
// force refresh NTP cache when outdated
boolean refreshSuccess = true;
NtpTrustedTime.TimeResult ntpResult = mNtpTime.getCachedTimeResult();
if (ntpResult == null || ntpResult.getAgeMillis() >= NTP_INTERVAL) {
// Blocking network operation.
refreshSuccess = mNtpTime.forceRefresh();
}
synchronized (this) {
mInjectNtpTimeState = STATE_IDLE;
// only update when NTP time is fresh
// If refreshSuccess is false, cacheAge does not drop down.
String injectReason = "blockingGetNtpTimeAndInject:"
+ ", debugId=" + debugId
+ ", refreshSuccess=" + refreshSuccess;
if (maybeInjectCachedNtpTime(injectReason)) {
delayMillis = NTP_INTERVAL;
mNtpBackOff.reset();
} else {
logWarn("maybeInjectCachedNtpTime() returned false");
delayMillis = mNtpBackOff.nextBackoffMillis();
}
if (mPeriodicTimeInjection || !refreshSuccess) {
String debugMsg = "blockingGetNtpTimeAndInject: Scheduling later NTP retrieval"
+ ", debugId=" + debugId
+ ", mPeriodicTimeInjection=" + mPeriodicTimeInjection
+ ", refreshSuccess=" + refreshSuccess
+ ", delayMillis=" + delayMillis;
logDebug(debugMsg);
// Schedule next NTP injection.
// Since this is delayed, the wake lock is released right away, and will be held
// again when the delayed task runs.
String reason = "scheduled: debugId=" + debugId;
mHandler.postDelayed(() -> retrieveAndInjectNtpTime(reason), delayMillis);
}
}
// release wake lock held by task
mWakeLock.release();
}
/** Returns true if successfully inject cached NTP time. */
private synchronized boolean maybeInjectCachedNtpTime(String reason) {
NtpTrustedTime.TimeResult ntpResult = mNtpTime.getCachedTimeResult();
if (ntpResult == null || ntpResult.getAgeMillis() >= NTP_INTERVAL) {
String debugMsg = "maybeInjectCachedNtpTime: Not injecting latest NTP time"
+ ", reason=" + reason
+ ", ntpResult=" + ntpResult;
logDebug(debugMsg);
return false;
}
long unixEpochTimeMillis = ntpResult.getTimeMillis();
long currentTimeMillis = System.currentTimeMillis();
String debugMsg = "maybeInjectCachedNtpTime: Injecting latest NTP time"
+ ", reason=" + reason
+ ", ntpResult=" + ntpResult
+ ", System time offset millis=" + (unixEpochTimeMillis - currentTimeMillis);
logDebug(debugMsg);
long timeReferenceMillis = ntpResult.getElapsedRealtimeMillis();
int uncertaintyMillis = ntpResult.getUncertaintyMillis();
mHandler.post(() -> mCallback.injectTime(unixEpochTimeMillis, timeReferenceMillis,
uncertaintyMillis));
return true;
}
private void logWarn(String logMsg) {
mDumpLog.log(logMsg);
Log.e(TAG, logMsg);
}
private void logDebug(String debugMsg) {
mDumpLog.log(debugMsg);
if (DEBUG) {
Log.d(TAG, debugMsg);
}
}
}