From a10381aa7a5568962b3cde0590b1696549977d67 Mon Sep 17 00:00:00 2001 From: Eva Chen Date: Tue, 12 Jan 2021 23:56:03 -0800 Subject: [PATCH 1/2] Add meta-text about gnss time update attribution. Bug: 157265008 Test: Presubmit Change-Id: I1a893ea86f5e4d0c722e8b6d5fc405580ff8ec0f --- core/res/AndroidManifest.xml | 3 +++ core/res/res/values/strings.xml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 51fb264cfeb8a..1d48eb65da864 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -5361,6 +5361,9 @@ on-device data --> + + Time Zone Detector (No connectivity) + + GNSS Time Update Service From 270ae737c2ca28a6aa416436897bf7e4a23b9a1f Mon Sep 17 00:00:00 2001 From: Eva Chen Date: Mon, 11 Jan 2021 11:28:02 -0800 Subject: [PATCH 2/2] Add GnssTimeUpdateService. GnssTimeUpdateService listens for location updates. Once an update is received, it reads the time provided by GPS and suggests it to TimeDetectorService. TimeDetectorService can then use it to set the SystemClock based on included time sources and priority. Bug: 157265008 Test: Presubmits + atest + Manual - atest frameworks/base/services/tests/servicestests/src/com/android/server/timedetector/GnssTimeUpdateServiceTest.java - For manual testing, the Location Shell Commands can be used to add / enable a gps test provider to set gps locations with a specified time. The time detector dumpsys can then be used to verify that a GNSS suggestion has been made. Change-Id: Ife27ecd6d122f33b7cc93d7832e3085acc33c160 --- core/res/res/values/config.xml | 4 + core/res/res/values/symbols.xml | 1 + .../timedetector/GnssTimeUpdateService.java | 203 ++++++++++++++++++ .../java/com/android/server/SystemServer.java | 12 ++ .../GnssTimeUpdateServiceTest.java | 162 ++++++++++++++ 5 files changed, 382 insertions(+) create mode 100644 services/core/java/com/android/server/timedetector/GnssTimeUpdateService.java create mode 100644 services/tests/servicestests/src/com/android/server/timedetector/GnssTimeUpdateServiceTest.java diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1845faacc891e..bb111f74e9668 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1570,6 +1570,10 @@ network + + false + false diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c72a0cd63b36d..46e95e4d2d1e8 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2175,6 +2175,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 94df1851aa874..3b23e3694f4cd 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()); + } +}