diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index e1cd81c5a18ef..841d4fed6fae9 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -5387,6 +5387,9 @@ on-device data --> + + 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()); + } +}