diff --git a/core/java/android/app/timedetector/GnssTimeSuggestion.aidl b/core/java/android/app/timedetector/GnssTimeSuggestion.aidl new file mode 100644 index 0000000000000..81475ec8156d5 --- /dev/null +++ b/core/java/android/app/timedetector/GnssTimeSuggestion.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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 android.app.timedetector; + +parcelable GnssTimeSuggestion; diff --git a/core/java/android/app/timedetector/GnssTimeSuggestion.java b/core/java/android/app/timedetector/GnssTimeSuggestion.java new file mode 100644 index 0000000000000..6478a2dd2aa9c --- /dev/null +++ b/core/java/android/app/timedetector/GnssTimeSuggestion.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2020 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 android.app.timedetector; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.TimestampedValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * A time signal from a GNSS source. + * + *

{@code utcTime} is the suggested time. The {@code utcTime.value} is the number of milliseconds + * elapsed since 1/1/1970 00:00:00 UTC. The {@code utcTime.referenceTimeMillis} is the value of the + * elapsed realtime clock when the {@code utcTime.value} was established. + * Note that the elapsed realtime clock is considered accurate but it is volatile, so time + * suggestions cannot be persisted across device resets. + * + *

{@code debugInfo} contains debugging metadata associated with the suggestion. This is used to + * record why the suggestion exists and how it was entered. This information exists only to aid in + * debugging and therefore is used by {@link #toString()}, but it is not for use in detection + * logic and is not considered in {@link #hashCode()} or {@link #equals(Object)}. + * + * @hide + */ +public final class GnssTimeSuggestion implements Parcelable { + + public static final @NonNull Creator CREATOR = + new Creator() { + public GnssTimeSuggestion createFromParcel(Parcel in) { + return GnssTimeSuggestion.createFromParcel(in); + } + + public GnssTimeSuggestion[] newArray(int size) { + return new GnssTimeSuggestion[size]; + } + }; + + @NonNull private final TimestampedValue mUtcTime; + @Nullable private ArrayList mDebugInfo; + + public GnssTimeSuggestion(@NonNull TimestampedValue utcTime) { + mUtcTime = Objects.requireNonNull(utcTime); + Objects.requireNonNull(utcTime.getValue()); + } + + private static GnssTimeSuggestion createFromParcel(Parcel in) { + TimestampedValue utcTime = in.readParcelable(null /* classLoader */); + GnssTimeSuggestion suggestion = new GnssTimeSuggestion(utcTime); + @SuppressWarnings("unchecked") + ArrayList debugInfo = (ArrayList) in.readArrayList(null /* classLoader */); + suggestion.mDebugInfo = debugInfo; + return suggestion; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mUtcTime, 0); + dest.writeList(mDebugInfo); + } + + @NonNull + public TimestampedValue getUtcTime() { + return mUtcTime; + } + + @NonNull + public List getDebugInfo() { + return mDebugInfo == null + ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo); + } + + /** + * Associates information with the instance that can be useful for debugging / logging. The + * information is present in {@link #toString()} but is not considered for + * {@link #equals(Object)} and {@link #hashCode()}. + */ + public void addDebugInfo(String... debugInfos) { + if (mDebugInfo == null) { + mDebugInfo = new ArrayList<>(); + } + mDebugInfo.addAll(Arrays.asList(debugInfos)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GnssTimeSuggestion that = (GnssTimeSuggestion) o; + return Objects.equals(mUtcTime, that.mUtcTime); + } + + @Override + public int hashCode() { + return Objects.hash(mUtcTime); + } + + @Override + public String toString() { + return "GnssTimeSuggestion{" + + "mUtcTime=" + mUtcTime + + ", mDebugInfo=" + mDebugInfo + + '}'; + } +} diff --git a/core/java/android/app/timedetector/ITimeDetectorService.aidl b/core/java/android/app/timedetector/ITimeDetectorService.aidl index 7bea5d7d60b12..87e72332274eb 100644 --- a/core/java/android/app/timedetector/ITimeDetectorService.aidl +++ b/core/java/android/app/timedetector/ITimeDetectorService.aidl @@ -16,6 +16,7 @@ package android.app.timedetector; +import android.app.timedetector.GnssTimeSuggestion; import android.app.timedetector.ManualTimeSuggestion; import android.app.timedetector.NetworkTimeSuggestion; import android.app.timedetector.TelephonyTimeSuggestion; @@ -34,6 +35,7 @@ import android.app.timedetector.TelephonyTimeSuggestion; * {@hide} */ interface ITimeDetectorService { + void suggestGnssTime(in GnssTimeSuggestion timeSuggestion); boolean suggestManualTime(in ManualTimeSuggestion timeSuggestion); void suggestNetworkTime(in NetworkTimeSuggestion timeSuggestion); void suggestTelephonyTime(in TelephonyTimeSuggestion timeSuggestion); diff --git a/core/java/android/app/timedetector/TimeDetector.java b/core/java/android/app/timedetector/TimeDetector.java index 162e18215cae2..52016b65688b0 100644 --- a/core/java/android/app/timedetector/TimeDetector.java +++ b/core/java/android/app/timedetector/TimeDetector.java @@ -71,4 +71,12 @@ public interface TimeDetector { */ @RequiresPermission(android.Manifest.permission.SET_TIME) void suggestNetworkTime(NetworkTimeSuggestion timeSuggestion); + + /** + * Suggests the time according to a gnss time source. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.SET_TIME) + void suggestGnssTime(GnssTimeSuggestion timeSuggestion); } diff --git a/core/java/android/app/timedetector/TimeDetectorImpl.java b/core/java/android/app/timedetector/TimeDetectorImpl.java index ac02c8930982f..b0aa3c8d45756 100644 --- a/core/java/android/app/timedetector/TimeDetectorImpl.java +++ b/core/java/android/app/timedetector/TimeDetectorImpl.java @@ -74,4 +74,16 @@ public final class TimeDetectorImpl implements TimeDetector { throw e.rethrowFromSystemServer(); } } + + @Override + public void suggestGnssTime(GnssTimeSuggestion timeSuggestion) { + if (DEBUG) { + Log.d(TAG, "suggestGnssTime called: " + timeSuggestion); + } + try { + mITimeDetectorService.suggestGnssTime(timeSuggestion); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/tests/coretests/src/android/app/timedetector/GnssTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/GnssTimeSuggestionTest.java new file mode 100644 index 0000000000000..e248010319e11 --- /dev/null +++ b/core/tests/coretests/src/android/app/timedetector/GnssTimeSuggestionTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020 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 android.app.timedetector; + +import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable; +import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import android.os.TimestampedValue; + +import org.junit.Test; + +public class GnssTimeSuggestionTest { + + private static final TimestampedValue ARBITRARY_TIME = + new TimestampedValue<>(1111L, 2222L); + + @Test + public void testEquals() { + GnssTimeSuggestion one = new GnssTimeSuggestion(ARBITRARY_TIME); + assertEquals(one, one); + + GnssTimeSuggestion two = new GnssTimeSuggestion(ARBITRARY_TIME); + assertEquals(one, two); + assertEquals(two, one); + + TimestampedValue differentTime = new TimestampedValue<>( + ARBITRARY_TIME.getReferenceTimeMillis() + 1, + ARBITRARY_TIME.getValue()); + GnssTimeSuggestion three = new GnssTimeSuggestion(differentTime); + assertNotEquals(one, three); + assertNotEquals(three, one); + + // DebugInfo must not be considered in equals(). + one.addDebugInfo("Debug info 1"); + two.addDebugInfo("Debug info 2"); + assertEquals(one, two); + } + + @Test + public void testParcelable() { + GnssTimeSuggestion suggestion = new GnssTimeSuggestion(ARBITRARY_TIME); + assertRoundTripParcelable(suggestion); + + // DebugInfo should also be stored (but is not checked by equals() + suggestion.addDebugInfo("This is debug info"); + GnssTimeSuggestion rtSuggestion = roundTripParcelable(suggestion); + assertEquals(suggestion.getDebugInfo(), rtSuggestion.getDebugInfo()); + } +} diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java index 6dac6b1773c47..84776ea072bc8 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorService.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorService.java @@ -18,6 +18,7 @@ package com.android.server.timedetector; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.timedetector.GnssTimeSuggestion; import android.app.timedetector.ITimeDetectorService; import android.app.timedetector.ManualTimeSuggestion; import android.app.timedetector.NetworkTimeSuggestion; @@ -122,6 +123,14 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub { mHandler.post(() -> mTimeDetectorStrategy.suggestNetworkTime(timeSignal)); } + @Override + public void suggestGnssTime(@NonNull GnssTimeSuggestion timeSignal) { + enforceSuggestGnssTimePermission(); + Objects.requireNonNull(timeSignal); + + mHandler.post(() -> mTimeDetectorStrategy.suggestGnssTime(timeSignal)); + } + /** Internal method for handling the auto time setting being changed. */ @VisibleForTesting public void handleAutoTimeDetectionChanged() { @@ -153,4 +162,10 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub { android.Manifest.permission.SET_TIME, "set time"); } + + private void enforceSuggestGnssTimePermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.SET_TIME, + "suggest gnss time"); + } } diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java index abd4c8c63a074..de6412c70a682 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java @@ -19,6 +19,7 @@ package com.android.server.timedetector; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.timedetector.GnssTimeSuggestion; import android.app.timedetector.ManualTimeSuggestion; import android.app.timedetector.NetworkTimeSuggestion; import android.app.timedetector.TelephonyTimeSuggestion; @@ -40,7 +41,7 @@ import java.lang.annotation.RetentionPolicy; */ public interface TimeDetectorStrategy { - @IntDef({ ORIGIN_TELEPHONY, ORIGIN_MANUAL, ORIGIN_NETWORK }) + @IntDef({ ORIGIN_TELEPHONY, ORIGIN_MANUAL, ORIGIN_NETWORK, ORIGIN_GNSS }) @Retention(RetentionPolicy.SOURCE) @interface Origin {} @@ -56,6 +57,10 @@ public interface TimeDetectorStrategy { @Origin int ORIGIN_NETWORK = 3; + /** Used when a time value originated from a gnss signal. */ + @Origin + int ORIGIN_GNSS = 4; + /** Processes the suggested time from telephony sources. */ void suggestTelephonyTime(@NonNull TelephonyTimeSuggestion timeSuggestion); @@ -70,6 +75,9 @@ public interface TimeDetectorStrategy { /** Processes the suggested time from network sources. */ void suggestNetworkTime(@NonNull NetworkTimeSuggestion timeSuggestion); + /** Processes the suggested time from gnss sources. */ + void suggestGnssTime(@NonNull GnssTimeSuggestion timeSuggestion); + /** * Handles the auto-time configuration changing For example, when the auto-time setting is * toggled on or off. @@ -102,6 +110,8 @@ public interface TimeDetectorStrategy { return "network"; case ORIGIN_TELEPHONY: return "telephony"; + case ORIGIN_GNSS: + return "gnss"; default: throw new IllegalArgumentException("origin=" + origin); } @@ -119,6 +129,8 @@ public interface TimeDetectorStrategy { return ORIGIN_NETWORK; case "telephony": return ORIGIN_TELEPHONY; + case "gnss": + return ORIGIN_GNSS; default: throw new IllegalArgumentException("originString=" + originString); } diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java index b3c8c9022701a..6fe37827b916b 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java @@ -23,6 +23,7 @@ import static java.util.stream.Collectors.joining; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AlarmManager; +import android.app.timedetector.GnssTimeSuggestion; import android.app.timedetector.ManualTimeSuggestion; import android.app.timedetector.NetworkTimeSuggestion; import android.app.timedetector.TelephonyTimeSuggestion; @@ -109,6 +110,10 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { private final ReferenceWithHistory mLastNetworkSuggestion = new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE); + @GuardedBy("this") + private final ReferenceWithHistory mLastGnssSuggestion = + new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE); + /** * The interface used by the strategy to interact with the surrounding service. * @@ -165,6 +170,20 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { mCallback = callback; } + @Override + public synchronized void suggestGnssTime(@NonNull GnssTimeSuggestion timeSuggestion) { + final TimestampedValue newUtcTime = timeSuggestion.getUtcTime(); + + if (!validateAutoSuggestionTime(newUtcTime, timeSuggestion)) { + return; + } + + mLastGnssSuggestion.set(timeSuggestion); + + String reason = "GNSS time suggestion received: suggestion=" + timeSuggestion; + doAutoTimeDetection(reason); + } + @Override public synchronized boolean suggestManualTime(@NonNull ManualTimeSuggestion suggestion) { final TimestampedValue newUtcTime = suggestion.getUtcTime(); @@ -280,6 +299,11 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { mLastNetworkSuggestion.dump(ipw); ipw.decreaseIndent(); // level 2 + ipw.println("Gnss suggestion history:"); + ipw.increaseIndent(); // level 2 + mLastGnssSuggestion.dump(ipw); + ipw.decreaseIndent(); // level 2 + ipw.decreaseIndent(); // level 1 ipw.flush(); } @@ -386,6 +410,14 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { + ", networkSuggestion=" + networkSuggestion + ", detectionReason=" + detectionReason; } + } else if (origin == ORIGIN_GNSS) { + GnssTimeSuggestion gnssTimeSuggestion = findLatestValidGnssSuggestion(); + if (gnssTimeSuggestion != null) { + newUtcTime = gnssTimeSuggestion.getUtcTime(); + cause = "Found good gnss suggestion." + + ", gnssTimeSuggestion=" + gnssTimeSuggestion + + ", detectionReason=" + detectionReason; + } } else { Slog.w(LOG_TAG, "Unknown or unsupported origin=" + origin + " in " + Arrays.toString(originPriorities) @@ -527,6 +559,26 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { return networkSuggestion; } + /** Returns the latest, valid, gnss suggestion. Returns {@code null} if there isn't one. */ + @GuardedBy("this") + @Nullable + private GnssTimeSuggestion findLatestValidGnssSuggestion() { + GnssTimeSuggestion gnssTimeSuggestion = mLastGnssSuggestion.get(); + if (gnssTimeSuggestion == null) { + // No gnss suggestions received. This is normal if there's no gnss signal. + return null; + } + + TimestampedValue utcTime = gnssTimeSuggestion.getUtcTime(); + long elapsedRealTimeMillis = mCallback.elapsedRealtimeMillis(); + if (!validateSuggestionUtcTime(elapsedRealTimeMillis, utcTime)) { + // The latest suggestion is not valid, usually due to its age. + return null; + } + + return gnssTimeSuggestion; + } + @GuardedBy("this") private boolean setSystemClockIfRequired( @Origin int origin, @NonNull TimestampedValue time, @NonNull String cause) { @@ -654,6 +706,16 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { return findLatestValidNetworkSuggestion(); } + /** + * Returns the latest valid gnss suggestion. Not intended for general use: it is used during + * tests to check strategy behavior. + */ + @VisibleForTesting + @Nullable + public synchronized GnssTimeSuggestion findLatestValidGnssSuggestionForTests() { + return findLatestValidGnssSuggestion(); + } + /** * A method used to inspect state during tests. Not intended for general use. */ @@ -672,6 +734,15 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { return mLastNetworkSuggestion.get(); } + /** + * A method used to inspect state during tests. Not intended for general use. + */ + @VisibleForTesting + @Nullable + public synchronized GnssTimeSuggestion getLatestGnssSuggestion() { + return mLastGnssSuggestion.get(); + } + private static boolean validateSuggestionUtcTime( long elapsedRealtimeMillis, TimestampedValue utcTime) { long referenceTimeMillis = utcTime.getReferenceTimeMillis(); diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java index 22addf948a53b..f0b1be594909d 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.timedetector.GnssTimeSuggestion; import android.app.timedetector.ManualTimeSuggestion; import android.app.timedetector.NetworkTimeSuggestion; import android.app.timedetector.TelephonyTimeSuggestion; @@ -172,6 +173,36 @@ public class TimeDetectorServiceTest { mStubbedTimeDetectorStrategy.verifySuggestNetworkTimeCalled(NetworkTimeSuggestion); } + @Test(expected = SecurityException.class) + public void testSuggestGnssTime_withoutPermission() { + doThrow(new SecurityException("Mock")) + .when(mMockContext).enforceCallingOrSelfPermission(anyString(), any()); + GnssTimeSuggestion gnssTimeSuggestion = createGnssTimeSuggestion(); + + try { + mTimeDetectorService.suggestGnssTime(gnssTimeSuggestion); + fail(); + } finally { + verify(mMockContext).enforceCallingOrSelfPermission( + eq(android.Manifest.permission.SET_TIME), anyString()); + } + } + + @Test + public void testSuggestGnssTime() throws Exception { + doNothing().when(mMockContext).enforceCallingOrSelfPermission(anyString(), any()); + + GnssTimeSuggestion gnssTimeSuggestion = createGnssTimeSuggestion(); + mTimeDetectorService.suggestGnssTime(gnssTimeSuggestion); + mTestHandler.assertTotalMessagesEnqueued(1); + + verify(mMockContext).enforceCallingOrSelfPermission( + eq(android.Manifest.permission.SET_TIME), anyString()); + + mTestHandler.waitForMessagesToBeProcessed(); + mStubbedTimeDetectorStrategy.verifySuggestGnssTimeCalled(gnssTimeSuggestion); + } + @Test public void testDump() { when(mMockContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)) @@ -216,12 +247,18 @@ public class TimeDetectorServiceTest { return new NetworkTimeSuggestion(timeValue); } + private static GnssTimeSuggestion createGnssTimeSuggestion() { + TimestampedValue timeValue = new TimestampedValue<>(100L, 1_000_000L); + return new GnssTimeSuggestion(timeValue); + } + private static class StubbedTimeDetectorStrategy implements TimeDetectorStrategy { // Call tracking. private TelephonyTimeSuggestion mLastTelephonySuggestion; private ManualTimeSuggestion mLastManualSuggestion; private NetworkTimeSuggestion mLastNetworkSuggestion; + private GnssTimeSuggestion mLastGnssSuggestion; private boolean mHandleAutoTimeDetectionChangedCalled; private boolean mDumpCalled; @@ -241,6 +278,11 @@ public class TimeDetectorServiceTest { mLastNetworkSuggestion = timeSuggestion; } + @Override + public void suggestGnssTime(GnssTimeSuggestion timeSuggestion) { + mLastGnssSuggestion = timeSuggestion; + } + @Override public void handleAutoTimeConfigChanged() { mHandleAutoTimeDetectionChangedCalled = true; @@ -255,6 +297,7 @@ public class TimeDetectorServiceTest { mLastTelephonySuggestion = null; mLastManualSuggestion = null; mLastNetworkSuggestion = null; + mLastGnssSuggestion = null; mHandleAutoTimeDetectionChangedCalled = false; mDumpCalled = false; } @@ -271,6 +314,10 @@ public class TimeDetectorServiceTest { assertEquals(expectedSuggestion, mLastNetworkSuggestion); } + void verifySuggestGnssTimeCalled(GnssTimeSuggestion expectedSuggestion) { + assertEquals(expectedSuggestion, mLastGnssSuggestion); + } + void verifyHandleAutoTimeDetectionChangedCalled() { assertTrue(mHandleAutoTimeDetectionChangedCalled); } diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java index 21396fd0516e5..b1adb0b4ce60b 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java @@ -16,6 +16,7 @@ package com.android.server.timedetector; +import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_GNSS; import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_NETWORK; import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_TELEPHONY; @@ -25,6 +26,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import android.app.timedetector.GnssTimeSuggestion; import android.app.timedetector.ManualTimeSuggestion; import android.app.timedetector.NetworkTimeSuggestion; import android.app.timedetector.TelephonyTimeSuggestion; @@ -569,7 +571,53 @@ public class TimeDetectorStrategyImplTest { } @Test - public void highPrioritySuggestionsShouldBeatLowerPrioritySuggestions() { + public void testSuggestGnssTime_autoTimeEnabled() { + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoOriginPriorities(ORIGIN_GNSS) + .pokeAutoTimeDetectionEnabled(true); + + GnssTimeSuggestion timeSuggestion = + mScript.generateGnssTimeSuggestion(ARBITRARY_TEST_TIME); + + mScript.simulateTimePassing(); + + long expectedSystemClockMillis = + mScript.calculateTimeInMillisForNow(timeSuggestion.getUtcTime()); + mScript.simulateGnssTimeSuggestion(timeSuggestion) + .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); + } + + @Test + public void testSuggestGnssTime_autoTimeDisabled() { + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoOriginPriorities(ORIGIN_GNSS) + .pokeAutoTimeDetectionEnabled(false); + + GnssTimeSuggestion timeSuggestion = + mScript.generateGnssTimeSuggestion(ARBITRARY_TEST_TIME); + + mScript.simulateTimePassing() + .simulateGnssTimeSuggestion(timeSuggestion) + .verifySystemClockWasNotSetAndResetCallTracking(); + } + + @Test + public void gnssTimeSuggestion_ignoredWhenReferencedTimeIsInThePast() { + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoOriginPriorities(ORIGIN_GNSS) + .pokeAutoTimeDetectionEnabled(true); + + Instant suggestedTime = TIME_LOWER_BOUND.minus(Duration.ofDays(1)); + GnssTimeSuggestion timeSuggestion = mScript + .generateGnssTimeSuggestion(suggestedTime); + + mScript.simulateGnssTimeSuggestion(timeSuggestion) + .verifySystemClockWasNotSetAndResetCallTracking() + .assertLatestGnssSuggestion(null); + } + + @Test + public void highPrioritySuggestionsBeatLowerPrioritySuggestions_telephonyNetworkOrigins() { mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) .pokeAutoTimeDetectionEnabled(true) .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK); @@ -671,23 +719,131 @@ public class TimeDetectorStrategyImplTest { mScript.peekBestTelephonySuggestion()); } + @Test + public void highPrioritySuggestionsBeatLowerPrioritySuggestions_networkGnssOrigins() { + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(true) + .pokeAutoOriginPriorities(ORIGIN_NETWORK, ORIGIN_GNSS); + + // Three obviously different times that could not be mistaken for each other. + Instant gnssTime1 = ARBITRARY_TEST_TIME; + Instant gnssTime2 = ARBITRARY_TEST_TIME.plus(Duration.ofDays(30)); + Instant networkTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(60)); + // A small increment used to simulate the passage of time, but not enough to interfere with + // macro-level time changes associated with suggestion age. + final long smallTimeIncrementMillis = 101; + + // A gnss suggestion is made. It should be used because there is no network suggestion. + GnssTimeSuggestion gnssTimeSuggestion1 = + mScript.generateGnssTimeSuggestion(gnssTime1); + mScript.simulateTimePassing(smallTimeIncrementMillis) + .simulateGnssTimeSuggestion(gnssTimeSuggestion1) + .verifySystemClockWasSetAndResetCallTracking( + mScript.calculateTimeInMillisForNow(gnssTimeSuggestion1.getUtcTime())); + + // Check internal state. + mScript.assertLatestNetworkSuggestion(null) + .assertLatestGnssSuggestion(gnssTimeSuggestion1); + assertEquals(gnssTimeSuggestion1, mScript.peekLatestValidGnssSuggestion()); + assertNull("No network suggestions were made:", mScript.peekLatestValidNetworkSuggestion()); + + // Simulate a little time passing. + mScript.simulateTimePassing(smallTimeIncrementMillis) + .verifySystemClockWasNotSetAndResetCallTracking(); + + // Now a network suggestion is made. Network suggestions are prioritized over gnss + // suggestions so it should "win". + NetworkTimeSuggestion networkTimeSuggestion = + mScript.generateNetworkTimeSuggestion(networkTime); + mScript.simulateTimePassing(smallTimeIncrementMillis) + .simulateNetworkTimeSuggestion(networkTimeSuggestion) + .verifySystemClockWasSetAndResetCallTracking( + mScript.calculateTimeInMillisForNow(networkTimeSuggestion.getUtcTime())); + + // Check internal state. + mScript.assertLatestNetworkSuggestion(networkTimeSuggestion) + .assertLatestGnssSuggestion(gnssTimeSuggestion1); + assertEquals(gnssTimeSuggestion1, mScript.peekLatestValidGnssSuggestion()); + assertEquals(networkTimeSuggestion, mScript.peekLatestValidNetworkSuggestion()); + + // Simulate some significant time passing: half the time allowed before a time signal + // becomes "too old to use". + mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_UTC_TIME_AGE_MILLIS / 2) + .verifySystemClockWasNotSetAndResetCallTracking(); + + // Now another gnss suggestion is made. Network suggestions are prioritized over + // gnss suggestions so the latest network suggestion should still "win". + GnssTimeSuggestion gnssTimeSuggestion2 = + mScript.generateGnssTimeSuggestion(gnssTime2); + mScript.simulateTimePassing(smallTimeIncrementMillis) + .simulateGnssTimeSuggestion(gnssTimeSuggestion2) + .verifySystemClockWasNotSetAndResetCallTracking(); + + // Check internal state. + mScript.assertLatestNetworkSuggestion(networkTimeSuggestion) + .assertLatestGnssSuggestion(gnssTimeSuggestion2); + assertEquals(gnssTimeSuggestion2, mScript.peekLatestValidGnssSuggestion()); + assertEquals(networkTimeSuggestion, mScript.peekLatestValidNetworkSuggestion()); + + // Simulate some significant time passing: half the time allowed before a time signal + // becomes "too old to use". This should mean that telephonyTimeSuggestion is now too old to + // be used but networkTimeSuggestion2 is not. + mScript.simulateTimePassing(TimeDetectorStrategyImpl.MAX_UTC_TIME_AGE_MILLIS / 2); + + // NOTE: The TimeDetectorStrategyImpl doesn't set an alarm for the point when the last + // suggestion it used becomes too old: it requires a new suggestion or an auto-time toggle + // to re-run the detection logic. This may change in future but until then we rely on a + // steady stream of suggestions to re-evaluate. + mScript.verifySystemClockWasNotSetAndResetCallTracking(); + + // Check internal state. + mScript.assertLatestNetworkSuggestion(networkTimeSuggestion) + .assertLatestGnssSuggestion(gnssTimeSuggestion2); + assertEquals(gnssTimeSuggestion2, mScript.peekLatestValidGnssSuggestion()); + assertNull( + "Network suggestion should be expired:", + mScript.peekLatestValidNetworkSuggestion()); + + // Toggle auto-time off and on to force the detection logic to run. + mScript.simulateAutoTimeDetectionToggle() + .simulateTimePassing(smallTimeIncrementMillis) + .simulateAutoTimeDetectionToggle(); + + // Verify the latest gnss time now wins. + mScript.verifySystemClockWasSetAndResetCallTracking( + mScript.calculateTimeInMillisForNow(gnssTimeSuggestion2.getUtcTime())); + + // Check internal state. + mScript.assertLatestNetworkSuggestion(networkTimeSuggestion) + .assertLatestGnssSuggestion(gnssTimeSuggestion2); + assertEquals(gnssTimeSuggestion2, mScript.peekLatestValidGnssSuggestion()); + assertNull( + "Network suggestion should still be expired:", + mScript.peekLatestValidNetworkSuggestion()); + } + @Test public void whenAllTimeSuggestionsAreAvailable_higherPriorityWins_lowerPriorityComesFirst() { mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) .pokeAutoTimeDetectionEnabled(true) - .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK); + .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK, ORIGIN_GNSS); Instant networkTime = ARBITRARY_TEST_TIME; - Instant telephonyTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(30)); + Instant gnssTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(30)); + Instant telephonyTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(60)); NetworkTimeSuggestion networkTimeSuggestion = mScript.generateNetworkTimeSuggestion(networkTime); + GnssTimeSuggestion gnssTimeSuggestion = + mScript.generateGnssTimeSuggestion(gnssTime); TelephonyTimeSuggestion telephonyTimeSuggestion = mScript.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, telephonyTime); mScript.simulateNetworkTimeSuggestion(networkTimeSuggestion) + .simulateGnssTimeSuggestion(gnssTimeSuggestion) .simulateTelephonyTimeSuggestion(telephonyTimeSuggestion) .assertLatestNetworkSuggestion(networkTimeSuggestion) + .assertLatestGnssSuggestion(gnssTimeSuggestion) .assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, telephonyTimeSuggestion) .verifySystemClockWasSetAndResetCallTracking(telephonyTime.toEpochMilli()); } @@ -696,20 +852,25 @@ public class TimeDetectorStrategyImplTest { public void whenAllTimeSuggestionsAreAvailable_higherPriorityWins_higherPriorityComesFirst() { mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) .pokeAutoTimeDetectionEnabled(true) - .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK); + .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK, ORIGIN_GNSS); Instant networkTime = ARBITRARY_TEST_TIME; Instant telephonyTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(30)); + Instant gnssTime = ARBITRARY_TEST_TIME.plus(Duration.ofDays(60)); NetworkTimeSuggestion networkTimeSuggestion = mScript.generateNetworkTimeSuggestion(networkTime); TelephonyTimeSuggestion telephonyTimeSuggestion = mScript.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, telephonyTime); + GnssTimeSuggestion gnssTimeSuggestion = + mScript.generateGnssTimeSuggestion(gnssTime); mScript.simulateTelephonyTimeSuggestion(telephonyTimeSuggestion) .simulateNetworkTimeSuggestion(networkTimeSuggestion) + .simulateGnssTimeSuggestion(gnssTimeSuggestion) .assertLatestNetworkSuggestion(networkTimeSuggestion) .assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, telephonyTimeSuggestion) + .assertLatestGnssSuggestion(gnssTimeSuggestion) .verifySystemClockWasSetAndResetCallTracking(telephonyTime.toEpochMilli()); } @@ -728,7 +889,37 @@ public class TimeDetectorStrategyImplTest { } @Test - public void suggestionsFromSourceNotListedInPrioritiesList_areIgnored() { + public void whenHigherPrioritySuggestionsAreNotAvailable_fallbacksToNext() { + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(true) + .pokeAutoOriginPriorities(ORIGIN_TELEPHONY, ORIGIN_NETWORK, ORIGIN_GNSS); + + GnssTimeSuggestion timeSuggestion = + mScript.generateGnssTimeSuggestion(ARBITRARY_TEST_TIME); + + mScript.simulateGnssTimeSuggestion(timeSuggestion) + .assertLatestGnssSuggestion(timeSuggestion) + .verifySystemClockWasSetAndResetCallTracking(ARBITRARY_TEST_TIME.toEpochMilli()); + } + + @Test + public void suggestionsFromTelephonyOriginNotInPriorityList_areIgnored() { + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(true) + .pokeAutoOriginPriorities(ORIGIN_NETWORK); + + int slotIndex = ARBITRARY_SLOT_INDEX; + Instant testTime = ARBITRARY_TEST_TIME; + TelephonyTimeSuggestion timeSuggestion = + mScript.generateTelephonyTimeSuggestion(slotIndex, testTime); + + mScript.simulateTelephonyTimeSuggestion(timeSuggestion) + .assertLatestTelephonySuggestion(ARBITRARY_SLOT_INDEX, timeSuggestion) + .verifySystemClockWasNotSetAndResetCallTracking(); + } + + @Test + public void suggestionsFromNetworkOriginNotInPriorityList_areIgnored() { mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) .pokeAutoTimeDetectionEnabled(true) .pokeAutoOriginPriorities(ORIGIN_TELEPHONY); @@ -741,6 +932,20 @@ public class TimeDetectorStrategyImplTest { .verifySystemClockWasNotSetAndResetCallTracking(); } + @Test + public void suggestionsFromGnssOriginNotInPriorityList_areIgnored() { + mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) + .pokeAutoTimeDetectionEnabled(true) + .pokeAutoOriginPriorities(ORIGIN_TELEPHONY); + + GnssTimeSuggestion timeSuggestion = mScript.generateGnssTimeSuggestion( + ARBITRARY_TEST_TIME); + + mScript.simulateGnssTimeSuggestion(timeSuggestion) + .assertLatestGnssSuggestion(timeSuggestion) + .verifySystemClockWasNotSetAndResetCallTracking(); + } + @Test public void autoOriginPrioritiesList_doesNotAffectManualSuggestion() { mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO) @@ -945,6 +1150,11 @@ public class TimeDetectorStrategyImplTest { return this; } + Script simulateGnssTimeSuggestion(GnssTimeSuggestion timeSuggestion) { + mTimeDetectorStrategy.suggestGnssTime(timeSuggestion); + return this; + } + Script simulateAutoTimeDetectionToggle() { mFakeCallback.simulateAutoTimeZoneDetectionToggle(); mTimeDetectorStrategy.handleAutoTimeConfigChanged(); @@ -994,6 +1204,14 @@ public class TimeDetectorStrategyImplTest { return this; } + /** + * White box test info: Asserts the latest gnss suggestion is as expected. + */ + Script assertLatestGnssSuggestion(GnssTimeSuggestion expected) { + assertEquals(expected, mTimeDetectorStrategy.getLatestGnssSuggestion()); + return this; + } + /** * White box test info: Returns the telephony suggestion that would be used, if any, given * the current elapsed real time clock and regardless of origin prioritization. @@ -1010,6 +1228,14 @@ public class TimeDetectorStrategyImplTest { return mTimeDetectorStrategy.findLatestValidNetworkSuggestionForTests(); } + /** + * White box test info: Returns the gnss suggestion that would be used, if any, given the + * current elapsed real time clock and regardless of origin prioritization. + */ + GnssTimeSuggestion peekLatestValidGnssSuggestion() { + return mTimeDetectorStrategy.findLatestValidGnssSuggestionForTests(); + } + /** * Generates a ManualTimeSuggestion using the current elapsed realtime clock for the * reference time. @@ -1056,6 +1282,18 @@ public class TimeDetectorStrategyImplTest { return new NetworkTimeSuggestion(utcTime); } + /** + * Generates a GnssTimeSuggestion using the current elapsed realtime clock for the + * reference time. + */ + GnssTimeSuggestion generateGnssTimeSuggestion(Instant suggestedTime) { + TimestampedValue utcTime = + new TimestampedValue<>( + mFakeCallback.peekElapsedRealtimeMillis(), + suggestedTime.toEpochMilli()); + return new GnssTimeSuggestion(utcTime); + } + /** * Calculates what the supplied time would be when adjusted for the movement of the fake * elapsed realtime clock.