network
+
+ false
+
false
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index f2b96ef5eee31..996fbb3665064 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -450,6 +450,8 @@
understand which sub-unit of an application is requesting permissions and using power.
[CHAR LIMIT=NONE]-->
Time Zone Detector (No connectivity)
+
+ GNSS Time Update Service
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b5b835c030270..e2271d17f29c1 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2177,6 +2177,7 @@
+
diff --git a/services/core/java/com/android/server/timedetector/GnssTimeUpdateService.java b/services/core/java/com/android/server/timedetector/GnssTimeUpdateService.java
new file mode 100644
index 0000000000000..fb75ae1609cdb
--- /dev/null
+++ b/services/core/java/com/android/server/timedetector/GnssTimeUpdateService.java
@@ -0,0 +1,203 @@
+/*
+ * 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.app.AlarmManager;
+import android.app.timedetector.GnssTimeSuggestion;
+import android.app.timedetector.TimeDetector;
+import android.content.Context;
+import android.location.Location;
+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.SystemClock;
+import android.os.TimestampedValue;
+import android.util.Log;
+
+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;
+
+/**
+ * Monitors the GNSS time.
+ *
+ * When available, the time is always suggested to the {@link
+ * com.android.server.timedetector.TimeDetectorService} 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() {
+ mService = new GnssTimeUpdateService(getContext());
+ 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.requestGnssTimeUpdates();
+ }
+ }
+ }
+
+ private static final Duration GNSS_TIME_UPDATE_ALARM_INTERVAL = Duration.ofHours(4);
+ private static final String ATTRIBUTION_TAG = "GnssTimeUpdateService";
+
+ private final Context mContext;
+ private final TimeDetector mTimeDetector;
+ private final AlarmManager mAlarmManager;
+ private final LocationManager mLocationManager;
+ private final LocationManagerInternal mLocationManagerInternal;
+
+ @Nullable private AlarmManager.OnAlarmListener mAlarmListener;
+ @Nullable private LocationListener mLocationListener;
+ @Nullable private TimestampedValue mLastSuggestedGnssTime;
+
+ @VisibleForTesting
+ GnssTimeUpdateService(@NonNull Context context) {
+ mContext = context.createAttributionContext(ATTRIBUTION_TAG);
+ mTimeDetector = mContext.getSystemService(TimeDetector.class);
+ mLocationManager = mContext.getSystemService(LocationManager.class);
+ mAlarmManager = mContext.getSystemService(AlarmManager.class);
+ mLocationManagerInternal = LocalServices.getService(LocationManagerInternal.class);
+ }
+
+ /**
+ * Request passive location updates. Such a request will not trigger any active locations or
+ * power usage itself.
+ */
+ @VisibleForTesting
+ void requestGnssTimeUpdates() {
+ if (D) {
+ Log.d(TAG, "requestGnssTimeUpdates()");
+ }
+
+ // Location Listener triggers onLocationChanged() when GNSS data is available, so
+ // that the getGnssTimeMillis() function doesn't need to be continuously polled.
+ mLocationListener = new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ if (D) {
+ Log.d(TAG, "onLocationChanged()");
+ }
+
+ // getGnssTimeMillis() can return null when the Master Location Switch for the
+ // foreground user is disabled.
+ LocationTime locationTime = mLocationManagerInternal.getGnssTimeMillis();
+ if (locationTime != null) {
+ suggestGnssTime(locationTime);
+ } else {
+ if (D) {
+ Log.d(TAG, "getGnssTimeMillis() returned null");
+ }
+ }
+
+ mLocationManager.removeUpdates(mLocationListener);
+ mLocationListener = null;
+
+ mAlarmListener = new AlarmManager.OnAlarmListener() {
+ @Override
+ public void onAlarm() {
+ if (D) {
+ Log.d(TAG, "onAlarm()");
+ }
+ mAlarmListener = null;
+ requestGnssTimeUpdates();
+ }
+ };
+
+ // Set next alarm to re-enable location updates.
+ long next = SystemClock.elapsedRealtime()
+ + GNSS_TIME_UPDATE_ALARM_INTERVAL.toMillis();
+ mAlarmManager.set(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ next,
+ TAG,
+ mAlarmListener,
+ FgThread.getHandler());
+ }
+ };
+
+ mLocationManager.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ new LocationRequest.Builder(LocationRequest.PASSIVE_INTERVAL)
+ .setMinUpdateIntervalMillis(0)
+ .build(),
+ FgThread.getExecutor(),
+ mLocationListener);
+ }
+
+ /**
+ * Convert LocationTime to TimestampedValue. Then suggest TimestampedValue to Time Detector.
+ */
+ private void suggestGnssTime(LocationTime locationTime) {
+ if (D) {
+ Log.d(TAG, "suggestGnssTime()");
+ }
+ long gnssTime = locationTime.getTime();
+ long elapsedRealtimeMs = locationTime.getElapsedRealtimeNanos() / 1_000_000L;
+
+ TimestampedValue timeSignal = new TimestampedValue<>(
+ elapsedRealtimeMs, gnssTime);
+ mLastSuggestedGnssTime = timeSignal;
+
+ GnssTimeSuggestion timeSuggestion = new GnssTimeSuggestion(timeSignal);
+ mTimeDetector.suggestGnssTime(timeSuggestion);
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+ pw.println("mLastSuggestedGnssTime: " + mLastSuggestedGnssTime);
+ pw.print("state: ");
+ if (mLocationListener != null) {
+ pw.println("time updates enabled");
+ } else {
+ pw.println("alarm enabled");
+ }
+ }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 57b3e8da88857..c613dfb22c777 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -323,6 +323,8 @@ public final class SystemServer implements Dumpable {
"com.android.server.timezonedetector.TimeZoneDetectorService$Lifecycle";
private static final String LOCATION_TIME_ZONE_MANAGER_SERVICE_CLASS =
"com.android.server.location.timezone.LocationTimeZoneManagerService$Lifecycle";
+ private static final String GNSS_TIME_UPDATE_SERVICE_CLASS =
+ "com.android.server.timedetector.GnssTimeUpdateService$Lifecycle";
private static final String ACCESSIBILITY_MANAGER_SERVICE_CLASS =
"com.android.server.accessibility.AccessibilityManagerService$Lifecycle";
private static final String ADB_SERVICE_CLASS =
@@ -1874,6 +1876,16 @@ public final class SystemServer implements Dumpable {
}
t.traceEnd();
+ if (context.getResources().getBoolean(R.bool.config_enableGnssTimeUpdateService)) {
+ t.traceBegin("StartGnssTimeUpdateService");
+ try {
+ mSystemServiceManager.startService(GNSS_TIME_UPDATE_SERVICE_CLASS);
+ } catch (Throwable e) {
+ reportWtf("starting GnssTimeUpdateService service", e);
+ }
+ t.traceEnd();
+ }
+
if (!isWatch) {
t.traceBegin("StartSearchManagerService");
try {
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/GnssTimeUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/GnssTimeUpdateServiceTest.java
new file mode 100644
index 0000000000000..8ac6dfb6d960e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/timedetector/GnssTimeUpdateServiceTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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 static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.app.timedetector.GnssTimeSuggestion;
+import android.app.timedetector.TimeDetector;
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.location.LocationManagerInternal;
+import android.location.LocationRequest;
+import android.location.LocationTime;
+import android.os.TimestampedValue;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public final class GnssTimeUpdateServiceTest {
+ private static final long GNSS_TIME = 999_999_999L;
+ private static final long ELAPSED_REALTIME_NS = 123_000_000L;
+ private static final long ELAPSED_REALTIME_MS = ELAPSED_REALTIME_NS / 1_000_000L;
+
+ @Mock private Context mMockContext;
+ @Mock private TimeDetector mMockTimeDetector;
+ @Mock private AlarmManager mMockAlarmManager;
+ @Mock private LocationManager mMockLocationManager;
+ @Mock private LocationManagerInternal mLocationManagerInternal;
+
+ private GnssTimeUpdateService mGnssTimeUpdateService;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mMockContext.createAttributionContext(anyString()))
+ .thenReturn(mMockContext);
+
+ when(mMockContext.getSystemServiceName(TimeDetector.class))
+ .thenReturn((TimeDetector.class).getSimpleName());
+ when(mMockContext.getSystemService(TimeDetector.class))
+ .thenReturn(mMockTimeDetector);
+
+ when(mMockContext.getSystemServiceName(LocationManager.class))
+ .thenReturn((LocationManager.class).getSimpleName());
+ when(mMockContext.getSystemService(LocationManager.class))
+ .thenReturn(mMockLocationManager);
+
+ when(mMockContext.getSystemServiceName(AlarmManager.class))
+ .thenReturn((AlarmManager.class).getSimpleName());
+ when(mMockContext.getSystemService(AlarmManager.class))
+ .thenReturn(mMockAlarmManager);
+
+ LocalServices.addService(LocationManagerInternal.class, mLocationManagerInternal);
+
+ mGnssTimeUpdateService =
+ new GnssTimeUpdateService(mMockContext);
+ }
+
+ @After
+ public void tearDown() {
+ LocalServices.removeServiceForTest(LocationManagerInternal.class);
+ }
+
+ @Test
+ public void testLocationListenerOnLocationChanged_validLocationTime_suggestsGnssTime() {
+ TimestampedValue timeSignal = new TimestampedValue<>(
+ ELAPSED_REALTIME_MS, GNSS_TIME);
+ GnssTimeSuggestion timeSuggestion = new GnssTimeSuggestion(timeSignal);
+ LocationTime locationTime = new LocationTime(GNSS_TIME, ELAPSED_REALTIME_NS);
+ doReturn(locationTime).when(mLocationManagerInternal).getGnssTimeMillis();
+
+ mGnssTimeUpdateService.requestGnssTimeUpdates();
+
+ ArgumentCaptor argumentCaptor =
+ ArgumentCaptor.forClass(LocationListener.class);
+ verify(mMockLocationManager).requestLocationUpdates(
+ eq(LocationManager.GPS_PROVIDER),
+ eq(new LocationRequest.Builder(LocationRequest.PASSIVE_INTERVAL)
+ .setMinUpdateIntervalMillis(0)
+ .build()),
+ any(),
+ argumentCaptor.capture());
+ LocationListener locationListener = argumentCaptor.getValue();
+ Location location = new Location(LocationManager.GPS_PROVIDER);
+
+ locationListener.onLocationChanged(location);
+
+ verify(mMockLocationManager).removeUpdates(locationListener);
+ verify(mMockTimeDetector).suggestGnssTime(timeSuggestion);
+ verify(mMockAlarmManager).set(
+ eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+ anyLong(),
+ any(),
+ any(),
+ any());
+ }
+
+ @Test
+ public void testLocationListenerOnLocationChanged_nullLocationTime_doesNotSuggestGnssTime() {
+ doReturn(null).when(mLocationManagerInternal).getGnssTimeMillis();
+
+ mGnssTimeUpdateService.requestGnssTimeUpdates();
+
+ ArgumentCaptor argumentCaptor =
+ ArgumentCaptor.forClass(LocationListener.class);
+ verify(mMockLocationManager).requestLocationUpdates(
+ eq(LocationManager.GPS_PROVIDER),
+ eq(new LocationRequest.Builder(LocationRequest.PASSIVE_INTERVAL)
+ .setMinUpdateIntervalMillis(0)
+ .build()),
+ any(),
+ argumentCaptor.capture());
+ LocationListener locationListener = argumentCaptor.getValue();
+ Location location = new Location(LocationManager.GPS_PROVIDER);
+
+ locationListener.onLocationChanged(location);
+
+ verify(mMockLocationManager).removeUpdates(locationListener);
+ verify(mMockTimeDetector, never()).suggestGnssTime(any());
+ verify(mMockAlarmManager).set(
+ eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+ anyLong(),
+ any(),
+ any(),
+ any());
+ }
+}