Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 270ae737 authored by Eva Chen's avatar Eva Chen
Browse files

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
parent a10381aa
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -1570,6 +1570,10 @@
        <item>network</item>
    </string-array>

    <!-- Enables the GnssTimeUpdate service. This is the global switch for enabling Gnss time based
         suggestions to TimeDetector service. See also config_autoTimeSourcesPriority. -->
    <bool name="config_enableGnssTimeUpdateService">false</bool>

    <!-- Enables the TimeZoneRuleManager service. This is the global switch for the updateable time
         zone update mechanism. -->
    <bool name="config_enableUpdateableTimeZoneRules">false</bool>
+1 −0
Original line number Diff line number Diff line
@@ -2175,6 +2175,7 @@
  <java-symbol type="string" name="config_persistentDataPackageName" />
  <java-symbol type="string" name="config_deviceConfiguratorPackageName" />
  <java-symbol type="array" name="config_autoTimeSourcesPriority" />
  <java-symbol type="bool" name="config_enableGnssTimeUpdateService" />
  <java-symbol type="bool" name="config_enableGeolocationTimeZoneDetection" />
  <java-symbol type="bool" name="config_enablePrimaryLocationTimeZoneProvider" />
  <java-symbol type="bool" name="config_enablePrimaryLocationTimeZoneOverlay" />
+203 −0
Original line number Diff line number Diff line
/*
 * 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.
 *
 * <p>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<Long> 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<Long> 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");
        }
    }
}
+12 −0
Original line number Diff line number Diff line
@@ -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 {
+162 −0
Original line number Diff line number Diff line
/*
 * 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<Long> 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<LocationListener> 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<LocationListener> 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());
    }
}