Add a path for GNSS time suggestions

Add a path for GNSS time suggestions to get to the time detector.

Bug: 157265008
Test: atest services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
Test: atest android.app.timedetector
Change-Id: I5cb12b5545652ed885b72a3170940050ce0628a6
Merged-In: I5cb12b5545652ed885b72a3170940050ce0628a6
diff --git a/core/java/android/app/timedetector/GnssTimeSuggestion.aidl b/core/java/android/app/timedetector/GnssTimeSuggestion.aidl
new file mode 100644
index 0000000..81475ec
--- /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 0000000..6478a2d
--- /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.
+ *
+ * <p>{@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.
+ *
+ * <p>{@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<GnssTimeSuggestion> CREATOR =
+            new Creator<GnssTimeSuggestion>() {
+                public GnssTimeSuggestion createFromParcel(Parcel in) {
+                    return GnssTimeSuggestion.createFromParcel(in);
+                }
+
+                public GnssTimeSuggestion[] newArray(int size) {
+                    return new GnssTimeSuggestion[size];
+                }
+            };
+
+    @NonNull private final TimestampedValue<Long> mUtcTime;
+    @Nullable private ArrayList<String> mDebugInfo;
+
+    public GnssTimeSuggestion(@NonNull TimestampedValue<Long> utcTime) {
+        mUtcTime = Objects.requireNonNull(utcTime);
+        Objects.requireNonNull(utcTime.getValue());
+    }
+
+    private static GnssTimeSuggestion createFromParcel(Parcel in) {
+        TimestampedValue<Long> utcTime = in.readParcelable(null /* classLoader */);
+        GnssTimeSuggestion suggestion = new GnssTimeSuggestion(utcTime);
+        @SuppressWarnings("unchecked")
+        ArrayList<String> debugInfo = (ArrayList<String>) 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<Long> getUtcTime() {
+        return mUtcTime;
+    }
+
+    @NonNull
+    public List<String> 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 7bea5d7..87e7233 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 @@
  * {@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 162e182..52016b6 100644
--- a/core/java/android/app/timedetector/TimeDetector.java
+++ b/core/java/android/app/timedetector/TimeDetector.java
@@ -71,4 +71,12 @@
      */
     @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 ac02c89..b0aa3c8 100644
--- a/core/java/android/app/timedetector/TimeDetectorImpl.java
+++ b/core/java/android/app/timedetector/TimeDetectorImpl.java
@@ -74,4 +74,16 @@
             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 0000000..e248010
--- /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<Long> 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<Long> 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 6dac6b1..84776ea 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 @@
 
 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 @@
         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 @@
                 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 abd4c8c..de6412c 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 @@
 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 @@
  */
 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 @@
     @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 @@
     /** 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 @@
                 return "network";
             case ORIGIN_TELEPHONY:
                 return "telephony";
+            case ORIGIN_GNSS:
+                return "gnss";
             default:
                 throw new IllegalArgumentException("origin=" + origin);
         }
@@ -119,6 +129,8 @@
                 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 b3c8c90..6fe3782 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 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 @@
     private final ReferenceWithHistory<NetworkTimeSuggestion> mLastNetworkSuggestion =
             new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
 
+    @GuardedBy("this")
+    private final ReferenceWithHistory<GnssTimeSuggestion> mLastGnssSuggestion =
+            new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE);
+
     /**
      * The interface used by the strategy to interact with the surrounding service.
      *
@@ -166,6 +171,20 @@
     }
 
     @Override
+    public synchronized void suggestGnssTime(@NonNull GnssTimeSuggestion timeSuggestion) {
+        final TimestampedValue<Long> 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<Long> newUtcTime = suggestion.getUtcTime();
 
@@ -280,6 +299,11 @@
         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 @@
                             + ", 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 @@
         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<Long> 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<Long> time, @NonNull String cause) {
@@ -655,6 +707,16 @@
     }
 
     /**
+     * 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.
      */
     @VisibleForTesting
@@ -672,6 +734,15 @@
         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<Long> 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 22addf9..f0b1be5 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.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 @@
         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 @@
         return new NetworkTimeSuggestion(timeValue);
     }
 
+    private static GnssTimeSuggestion createGnssTimeSuggestion() {
+        TimestampedValue<Long> 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;
 
@@ -242,6 +279,11 @@
         }
 
         @Override
+        public void suggestGnssTime(GnssTimeSuggestion timeSuggestion) {
+            mLastGnssSuggestion = timeSuggestion;
+        }
+
+        @Override
         public void handleAutoTimeConfigChanged() {
             mHandleAutoTimeDetectionChangedCalled = true;
         }
@@ -255,6 +297,7 @@
             mLastTelephonySuggestion = null;
             mLastManualSuggestion = null;
             mLastNetworkSuggestion = null;
+            mLastGnssSuggestion = null;
             mHandleAutoTimeDetectionChangedCalled = false;
             mDumpCalled = false;
         }
@@ -271,6 +314,10 @@
             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 21396fd..b1adb0b 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.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 @@
     }
 
     @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);
@@ -672,22 +720,130 @@
     }
 
     @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 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 @@
     }
 
     @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);
@@ -742,6 +933,20 @@
     }
 
     @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)
                 .pokeAutoTimeDetectionEnabled(false)
@@ -945,6 +1150,11 @@
             return this;
         }
 
+        Script simulateGnssTimeSuggestion(GnssTimeSuggestion timeSuggestion) {
+            mTimeDetectorStrategy.suggestGnssTime(timeSuggestion);
+            return this;
+        }
+
         Script simulateAutoTimeDetectionToggle() {
             mFakeCallback.simulateAutoTimeZoneDetectionToggle();
             mTimeDetectorStrategy.handleAutoTimeConfigChanged();
@@ -995,6 +1205,14 @@
         }
 
         /**
+         * 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.
          */
@@ -1011,6 +1229,14 @@
         }
 
         /**
+         * 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.
          */
@@ -1057,6 +1283,18 @@
         }
 
         /**
+         * Generates a GnssTimeSuggestion using the current elapsed realtime clock for the
+         * reference time.
+         */
+        GnssTimeSuggestion generateGnssTimeSuggestion(Instant suggestedTime) {
+            TimestampedValue<Long> 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.
          */