blob: ce1d5250464e44b42ddfab7cc3a2c17935c45f65 [file] [log] [blame]
/*
* Copyright (C) 2021 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.timedetector;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.AlarmManager;
import android.app.time.UnixEpochTime;
import android.content.Context;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationManagerInternal;
import android.location.LocationRequest;
import android.location.LocationTime;
import android.os.Binder;
import android.os.Handler;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.SystemClock;
import android.util.LocalLog;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* Monitors the GNSS time.
*
* <p>When available, the time is always suggested to the {@link
* com.android.server.timedetector.TimeDetectorInternal} where it may be used to set the device
* system clock, depending on user settings and what other signals are available.
*/
public final class GnssTimeUpdateService extends Binder {
private static final String TAG = "GnssTimeUpdateService";
private static final boolean D = Log.isLoggable(TAG, Log.DEBUG);
/**
* Handles the lifecycle events for the GnssTimeUpdateService.
*/
public static class Lifecycle extends SystemService {
private GnssTimeUpdateService mService;
public Lifecycle(@NonNull Context context) {
super(context);
}
@Override
public void onStart() {
Context context = getContext().createAttributionContext(ATTRIBUTION_TAG);
AlarmManager alarmManager = context.getSystemService(AlarmManager.class);
LocationManager locationManager = context.getSystemService(LocationManager.class);
LocationManagerInternal locationManagerInternal =
LocalServices.getService(LocationManagerInternal.class);
TimeDetectorInternal timeDetectorInternal =
LocalServices.getService(TimeDetectorInternal.class);
mService = new GnssTimeUpdateService(context, alarmManager, locationManager,
locationManagerInternal, timeDetectorInternal);
publishBinderService("gnss_time_update_service", mService);
}
@Override
public void onBootPhase(int phase) {
// Need to wait for some location providers to be enabled. If done at
// PHASE_SYSTEM_SERVICES_READY, error where "gps" provider does not exist could occur.
if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
// Initiate location updates. On boot, GNSS might not be available right away.
// Instead of polling GNSS time periodically, passive location updates are enabled.
// Once an update is received, the gnss time will be queried and suggested to
// TimeDetectorService.
mService.startGnssListeningInternal();
}
}
}
private static final Duration GNSS_TIME_UPDATE_ALARM_INTERVAL = Duration.ofHours(4);
private static final String ATTRIBUTION_TAG = "GnssTimeUpdateService";
/**
* A log that records the decisions to fetch a GNSS time update.
* This is logged in bug reports to assist with debugging issues with GNSS time suggestions.
*/
private final LocalLog mLocalLog = new LocalLog(10, false /* useLocalTimestamps */);
/** The executor used for async operations */
private final Executor mExecutor = FgThread.getExecutor();
/** The handler used for async operations */
private final Handler mHandler = FgThread.getHandler();
private final Context mContext;
private final TimeDetectorInternal mTimeDetectorInternal;
private final AlarmManager mAlarmManager;
private final LocationManager mLocationManager;
private final LocationManagerInternal mLocationManagerInternal;
private final Object mLock = new Object();
@GuardedBy("mLock") @Nullable private AlarmManager.OnAlarmListener mAlarmListener;
@GuardedBy("mLock") @Nullable private LocationListener mLocationListener;
@Nullable private volatile UnixEpochTime mLastSuggestedGnssTime;
@VisibleForTesting
GnssTimeUpdateService(@NonNull Context context, @NonNull AlarmManager alarmManager,
@NonNull LocationManager locationManager,
@NonNull LocationManagerInternal locationManagerInternal,
@NonNull TimeDetectorInternal timeDetectorInternal) {
mContext = Objects.requireNonNull(context);
mAlarmManager = Objects.requireNonNull(alarmManager);
mLocationManager = Objects.requireNonNull(locationManager);
mLocationManagerInternal = Objects.requireNonNull(locationManagerInternal);
mTimeDetectorInternal = Objects.requireNonNull(timeDetectorInternal);
}
/**
* Used by {@link com.android.server.timedetector.GnssTimeUpdateServiceShellCommand} to force
* the service into GNSS listening mode.
*/
@RequiresPermission(android.Manifest.permission.SET_TIME)
boolean startGnssListening() {
mContext.enforceCallingPermission(
android.Manifest.permission.SET_TIME, "Start GNSS listening");
mLocalLog.log("startGnssListening() called");
final long token = Binder.clearCallingIdentity();
try {
return startGnssListeningInternal();
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Starts listening for passive location updates. Such a request will not trigger any active
* locations or power usage itself. Returns {@code true} if the service is listening after the
* method returns and {@code false} otherwise. At present this method only returns {@code false}
* if there is no GPS provider on the device.
*
* <p>If the service is already listening for locations this is a no-op. If the device is in a
* "sleeping" state between listening periods then it will return to listening.
*/
@VisibleForTesting
boolean startGnssListeningInternal() {
if (!mLocationManager.hasProvider(LocationManager.GPS_PROVIDER)) {
logError("GPS provider does not exist on this device");
return false;
}
synchronized (mLock) {
if (mLocationListener != null) {
logDebug("Already listening for GNSS updates");
return true;
}
// If startGnssListening() is called during manual tests to jump back into location
// listening then there will usually be an alarm set.
if (mAlarmListener != null) {
mAlarmManager.cancel(mAlarmListener);
mAlarmListener = null;
}
startGnssListeningLocked();
return true;
}
}
@GuardedBy("mLock")
private void startGnssListeningLocked() {
logDebug("startGnssListeningLocked()");
// Location Listener triggers onLocationChanged() when GNSS data is available, so
// that the getGnssTimeMillis() function doesn't need to be continuously polled.
mLocationListener = location -> handleLocationAvailable();
mLocationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
new LocationRequest.Builder(LocationRequest.PASSIVE_INTERVAL)
.setMinUpdateIntervalMillis(0)
.build(),
mExecutor,
mLocationListener);
}
private void handleLocationAvailable() {
logDebug("handleLocationAvailable()");
// getGnssTimeMillis() can return null when the Master Location Switch for the
// foreground user is disabled.
LocationTime locationTime = mLocationManagerInternal.getGnssTimeMillis();
if (locationTime != null) {
String msg = "Passive location time received: " + locationTime;
logDebug(msg);
mLocalLog.log(msg);
suggestGnssTime(locationTime);
} else {
logDebug("getGnssTimeMillis() returned null");
}
synchronized (mLock) {
if (mLocationListener == null) {
logWarning("mLocationListener unexpectedly null");
} else {
mLocationManager.removeUpdates(mLocationListener);
mLocationListener = null;
}
if (mAlarmListener != null) {
logWarning("mAlarmListener was unexpectedly non-null");
mAlarmManager.cancel(mAlarmListener);
}
// Set next alarm to re-enable location updates.
long next = SystemClock.elapsedRealtime()
+ GNSS_TIME_UPDATE_ALARM_INTERVAL.toMillis();
mAlarmListener = this::handleAlarmFired;
mAlarmManager.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
next,
TAG,
mAlarmListener,
mHandler);
}
}
private void handleAlarmFired() {
logDebug("handleAlarmFired()");
synchronized (mLock) {
mAlarmListener = null;
startGnssListeningLocked();
}
}
/**
* Convert LocationTime to TimestampedValue. Then suggest TimestampedValue to Time Detector.
*/
private void suggestGnssTime(LocationTime locationTime) {
logDebug("suggestGnssTime()");
long gnssUnixEpochTimeMillis = locationTime.getUnixEpochTimeMillis();
long elapsedRealtimeMs = locationTime.getElapsedRealtimeNanos() / 1_000_000L;
UnixEpochTime unixEpochTime = new UnixEpochTime(elapsedRealtimeMs, gnssUnixEpochTimeMillis);
mLastSuggestedGnssTime = unixEpochTime;
GnssTimeSuggestion suggestion = new GnssTimeSuggestion(unixEpochTime);
mTimeDetectorInternal.suggestGnssTime(suggestion);
}
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
pw.println("mLastSuggestedGnssTime: " + mLastSuggestedGnssTime);
synchronized (mLock) {
pw.print("state: ");
if (mLocationListener != null) {
pw.println("time updates enabled");
} else {
pw.println("alarm enabled");
}
}
pw.println("Log:");
mLocalLog.dump(pw);
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
new GnssTimeUpdateServiceShellCommand(this).exec(
this, in, out, err, args, callback, resultReceiver);
}
private void logError(String msg) {
Log.e(TAG, msg);
mLocalLog.log(msg);
}
private void logWarning(String msg) {
Log.w(TAG, msg);
mLocalLog.log(msg);
}
private void logDebug(String msg) {
if (D) {
Log.d(TAG, msg);
}
}
}