Add a Y2038 check into the time_detector

Add a Y2038 check into the time_detector and add infrastructure to allow
command-line testing to confirm it works.

This is before removing a Y2038 check from NITZ parsing code in the
telephony process. After this change, Androdi will consistently block
>= Y2038 suggestions, not just time signals that come in via telephony.

The initial checks attempt to limit the restriction to devices with
32-bit ABIs, since the main issue we're aware of is that time_t is a
32-bit signed int under bionic, hence there could be issues with that
type in 32-bit processes on 32-bit / mixed 32/64-bit devices.

Bug: 204193177
Test: adb shell cmd time_detector suggest_network_time --reference_time 2480587 --unix_epoch_time 1646473966056
Test: Run with 32-bit + 64-bit only builds, inspect adb shell dumpsys time_detector
Merged-In: Ib9d6472f5ca7a62d59b3224f1845846f99a0b52d
Change-Id: Ib9d6472f5ca7a62d59b3224f1845846f99a0b52d
diff --git a/core/java/android/app/time/ExternalTimeSuggestion.java b/core/java/android/app/time/ExternalTimeSuggestion.java
index a7c0e5c..a7828ab 100644
--- a/core/java/android/app/time/ExternalTimeSuggestion.java
+++ b/core/java/android/app/time/ExternalTimeSuggestion.java
@@ -19,15 +19,14 @@
 import android.annotation.CurrentTimeMillisLong;
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.timedetector.TimeSuggestionHelper;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ShellCommand;
 import android.os.TimestampedValue;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
+import java.io.PrintWriter;
 import java.util.List;
 import java.util.Objects;
 
@@ -75,7 +74,9 @@
     public static final @NonNull Creator<ExternalTimeSuggestion> CREATOR =
             new Creator<ExternalTimeSuggestion>() {
                 public ExternalTimeSuggestion createFromParcel(Parcel in) {
-                    return ExternalTimeSuggestion.createFromParcel(in);
+                    TimeSuggestionHelper helper = TimeSuggestionHelper.handleCreateFromParcel(
+                            ExternalTimeSuggestion.class, in);
+                    return new ExternalTimeSuggestion(helper);
                 }
 
                 public ExternalTimeSuggestion[] newArray(int size) {
@@ -83,10 +84,7 @@
                 }
             };
 
-    @NonNull
-    private final TimestampedValue<Long> mUnixEpochTime;
-    @Nullable
-    private ArrayList<String> mDebugInfo;
+    @NonNull private final TimeSuggestionHelper mTimeSuggestionHelper;
 
     /**
      * Creates a time suggestion cross-referenced to the elapsed realtime clock. See {@link
@@ -98,17 +96,12 @@
      */
     public ExternalTimeSuggestion(@ElapsedRealtimeLong long elapsedRealtimeMillis,
             @CurrentTimeMillisLong long suggestionMillis) {
-        mUnixEpochTime = new TimestampedValue(elapsedRealtimeMillis, suggestionMillis);
+        mTimeSuggestionHelper = new TimeSuggestionHelper(ExternalTimeSuggestion.class,
+                new TimestampedValue<>(elapsedRealtimeMillis, suggestionMillis));
     }
 
-    private static ExternalTimeSuggestion createFromParcel(Parcel in) {
-        TimestampedValue<Long> utcTime = in.readParcelable(null /* classLoader */);
-        ExternalTimeSuggestion suggestion =
-                new ExternalTimeSuggestion(utcTime.getReferenceTimeMillis(), utcTime.getValue());
-        @SuppressWarnings("unchecked")
-        ArrayList<String> debugInfo = (ArrayList<String>) in.readArrayList(null /* classLoader */);
-        suggestion.mDebugInfo = debugInfo;
-        return suggestion;
+    private ExternalTimeSuggestion(@NonNull TimeSuggestionHelper helper) {
+        mTimeSuggestionHelper = Objects.requireNonNull(helper);
     }
 
     @Override
@@ -118,8 +111,7 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeParcelable(mUnixEpochTime, 0);
-        dest.writeList(mDebugInfo);
+        mTimeSuggestionHelper.handleWriteToParcel(dest, flags);
     }
 
     /**
@@ -127,7 +119,7 @@
      */
     @NonNull
     public TimestampedValue<Long> getUnixEpochTime() {
-        return mUnixEpochTime;
+        return mTimeSuggestionHelper.getUnixEpochTime();
     }
 
     /**
@@ -135,9 +127,7 @@
      */
     @NonNull
     public List<String> getDebugInfo() {
-        return mDebugInfo == null
-                ? Collections.emptyList()
-                : Collections.unmodifiableList(mDebugInfo);
+        return mTimeSuggestionHelper.getDebugInfo();
     }
 
     /**
@@ -146,10 +136,7 @@
      * #equals(Object)} and {@link #hashCode()}.
      */
     public void addDebugInfo(@NonNull String... debugInfos) {
-        if (mDebugInfo == null) {
-            mDebugInfo = new ArrayList<>();
-        }
-        mDebugInfo.addAll(Arrays.asList(debugInfos));
+        mTimeSuggestionHelper.addDebugInfo(debugInfos);
     }
 
     @Override
@@ -161,18 +148,29 @@
             return false;
         }
         ExternalTimeSuggestion that = (ExternalTimeSuggestion) o;
-        return Objects.equals(mUnixEpochTime, that.mUnixEpochTime);
+        return mTimeSuggestionHelper.handleEquals(that.mTimeSuggestionHelper);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mUnixEpochTime);
+        return mTimeSuggestionHelper.hashCode();
     }
 
     @Override
     public String toString() {
-        return "ExternalTimeSuggestion{" + "mUnixEpochTime=" + mUnixEpochTime
-                + ", mDebugInfo=" + mDebugInfo
-                + '}';
+        return mTimeSuggestionHelper.handleToString();
+    }
+
+    /** @hide */
+    public static ExternalTimeSuggestion parseCommandLineArg(@NonNull ShellCommand cmd)
+            throws IllegalArgumentException {
+        return new ExternalTimeSuggestion(
+                TimeSuggestionHelper.handleParseCommandLineArg(ExternalTimeSuggestion.class, cmd));
+    }
+
+    /** @hide */
+    public static void printCommandLineOpts(PrintWriter pw) {
+        TimeSuggestionHelper.handlePrintCommandLineOpts(
+                pw, "External", ExternalTimeSuggestion.class);
     }
 }
diff --git a/core/java/android/app/timedetector/GnssTimeSuggestion.java b/core/java/android/app/timedetector/GnssTimeSuggestion.java
index 34f4565..3531b19 100644
--- a/core/java/android/app/timedetector/GnssTimeSuggestion.java
+++ b/core/java/android/app/timedetector/GnssTimeSuggestion.java
@@ -17,30 +17,19 @@
 package android.app.timedetector;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ShellCommand;
 import android.os.TimestampedValue;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
+import java.io.PrintWriter;
 import java.util.List;
 import java.util.Objects;
 
 /**
  * A time signal from a GNSS source.
  *
- * <p>{@code unixEpochTime} is the suggested time. The {@code unixEpochTime.value} is the number of
- * milliseconds elapsed since 1/1/1970 00:00:00 UTC according to the Unix time system. The {@code
- * unixEpochTime.referenceTimeMillis} is the value of the elapsed realtime clock when the {@code
- * unixEpochTime.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)}.
+ * <p>See {@link TimeSuggestionHelper} for property information.
  *
  * @hide
  */
@@ -49,7 +38,9 @@
     public static final @NonNull Creator<GnssTimeSuggestion> CREATOR =
             new Creator<GnssTimeSuggestion>() {
                 public GnssTimeSuggestion createFromParcel(Parcel in) {
-                    return GnssTimeSuggestion.createFromParcel(in);
+                    TimeSuggestionHelper helper = TimeSuggestionHelper.handleCreateFromParcel(
+                            GnssTimeSuggestion.class, in);
+                    return new GnssTimeSuggestion(helper);
                 }
 
                 public GnssTimeSuggestion[] newArray(int size) {
@@ -57,21 +48,14 @@
                 }
             };
 
-    @NonNull private final TimestampedValue<Long> mUnixEpochTime;
-    @Nullable private ArrayList<String> mDebugInfo;
+    @NonNull private final TimeSuggestionHelper mTimeSuggestionHelper;
 
     public GnssTimeSuggestion(@NonNull TimestampedValue<Long> unixEpochTime) {
-        mUnixEpochTime = Objects.requireNonNull(unixEpochTime);
-        Objects.requireNonNull(unixEpochTime.getValue());
+        mTimeSuggestionHelper = new TimeSuggestionHelper(GnssTimeSuggestion.class, unixEpochTime);
     }
 
-    private static GnssTimeSuggestion createFromParcel(Parcel in) {
-        TimestampedValue<Long> unixEpochTime = in.readParcelable(null /* classLoader */);
-        GnssTimeSuggestion suggestion = new GnssTimeSuggestion(unixEpochTime);
-        @SuppressWarnings("unchecked")
-        ArrayList<String> debugInfo = (ArrayList<String>) in.readArrayList(null /* classLoader */);
-        suggestion.mDebugInfo = debugInfo;
-        return suggestion;
+    private GnssTimeSuggestion(@NonNull TimeSuggestionHelper helper) {
+        mTimeSuggestionHelper = Objects.requireNonNull(helper);
     }
 
     @Override
@@ -81,19 +65,17 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeParcelable(mUnixEpochTime, 0);
-        dest.writeList(mDebugInfo);
+        mTimeSuggestionHelper.handleWriteToParcel(dest, flags);
     }
 
     @NonNull
     public TimestampedValue<Long> getUnixEpochTime() {
-        return mUnixEpochTime;
+        return mTimeSuggestionHelper.getUnixEpochTime();
     }
 
     @NonNull
     public List<String> getDebugInfo() {
-        return mDebugInfo == null
-                ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
+        return mTimeSuggestionHelper.getDebugInfo();
     }
 
     /**
@@ -102,10 +84,7 @@
      * {@link #equals(Object)} and {@link #hashCode()}.
      */
     public void addDebugInfo(String... debugInfos) {
-        if (mDebugInfo == null) {
-            mDebugInfo = new ArrayList<>();
-        }
-        mDebugInfo.addAll(Arrays.asList(debugInfos));
+        mTimeSuggestionHelper.addDebugInfo(debugInfos);
     }
 
     @Override
@@ -117,19 +96,29 @@
             return false;
         }
         GnssTimeSuggestion that = (GnssTimeSuggestion) o;
-        return Objects.equals(mUnixEpochTime, that.mUnixEpochTime);
+        return mTimeSuggestionHelper.handleEquals(that.mTimeSuggestionHelper);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mUnixEpochTime);
+        return mTimeSuggestionHelper.hashCode();
     }
 
     @Override
     public String toString() {
-        return "GnssTimeSuggestion{"
-                + "mUnixEpochTime=" + mUnixEpochTime
-                + ", mDebugInfo=" + mDebugInfo
-                + '}';
+        return mTimeSuggestionHelper.handleToString();
+    }
+
+    /** Parses command line args to create a {@link GnssTimeSuggestion}. */
+    public static GnssTimeSuggestion parseCommandLineArg(@NonNull ShellCommand cmd)
+            throws IllegalArgumentException {
+        TimeSuggestionHelper suggestionHelper =
+                TimeSuggestionHelper.handleParseCommandLineArg(GnssTimeSuggestion.class, cmd);
+        return new GnssTimeSuggestion(suggestionHelper);
+    }
+
+    /** Prints the command line args needed to create a {@link GnssTimeSuggestion}. */
+    public static void printCommandLineOpts(PrintWriter pw) {
+        TimeSuggestionHelper.handlePrintCommandLineOpts(pw, "GNSS", GnssTimeSuggestion.class);
     }
 }
diff --git a/core/java/android/app/timedetector/ManualTimeSuggestion.java b/core/java/android/app/timedetector/ManualTimeSuggestion.java
index 76db33b..b447799 100644
--- a/core/java/android/app/timedetector/ManualTimeSuggestion.java
+++ b/core/java/android/app/timedetector/ManualTimeSuggestion.java
@@ -20,27 +20,17 @@
 import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ShellCommand;
 import android.os.TimestampedValue;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
+import java.io.PrintWriter;
 import java.util.List;
 import java.util.Objects;
 
 /**
  * A time signal from a manual (user provided) source.
  *
- * <p>{@code unixEpochTime} is the suggested time. The {@code unixEpochTime.value} is the number of
- * milliseconds elapsed since 1/1/1970 00:00:00 UTC. The {@code unixEpochTime.referenceTimeMillis}
- * is the value of the elapsed realtime clock when the {@code unixEpochTime.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)}.
+ * <p>See {@link TimeSuggestionHelper} for property information.
  *
  * @hide
  */
@@ -49,7 +39,9 @@
     public static final @NonNull Creator<ManualTimeSuggestion> CREATOR =
             new Creator<ManualTimeSuggestion>() {
                 public ManualTimeSuggestion createFromParcel(Parcel in) {
-                    return ManualTimeSuggestion.createFromParcel(in);
+                    TimeSuggestionHelper helper = TimeSuggestionHelper.handleCreateFromParcel(
+                            ManualTimeSuggestion.class, in);
+                    return new ManualTimeSuggestion(helper);
                 }
 
                 public ManualTimeSuggestion[] newArray(int size) {
@@ -57,21 +49,14 @@
                 }
             };
 
-    @NonNull private final TimestampedValue<Long> mUnixEpochTime;
-    @Nullable private ArrayList<String> mDebugInfo;
+    @NonNull private final TimeSuggestionHelper mTimeSuggestionHelper;
 
     public ManualTimeSuggestion(@NonNull TimestampedValue<Long> unixEpochTime) {
-        mUnixEpochTime = Objects.requireNonNull(unixEpochTime);
-        Objects.requireNonNull(unixEpochTime.getValue());
+        mTimeSuggestionHelper = new TimeSuggestionHelper(ManualTimeSuggestion.class, unixEpochTime);
     }
 
-    private static ManualTimeSuggestion createFromParcel(Parcel in) {
-        TimestampedValue<Long> unixEpochTime = in.readParcelable(null /* classLoader */);
-        ManualTimeSuggestion suggestion = new ManualTimeSuggestion(unixEpochTime);
-        @SuppressWarnings("unchecked")
-        ArrayList<String> debugInfo = (ArrayList<String>) in.readArrayList(null /* classLoader */);
-        suggestion.mDebugInfo = debugInfo;
-        return suggestion;
+    private ManualTimeSuggestion(@NonNull TimeSuggestionHelper helper) {
+        mTimeSuggestionHelper = Objects.requireNonNull(helper);
     }
 
     @Override
@@ -81,19 +66,17 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeParcelable(mUnixEpochTime, 0);
-        dest.writeList(mDebugInfo);
+        mTimeSuggestionHelper.handleWriteToParcel(dest, flags);
     }
 
     @NonNull
     public TimestampedValue<Long> getUnixEpochTime() {
-        return mUnixEpochTime;
+        return mTimeSuggestionHelper.getUnixEpochTime();
     }
 
     @NonNull
     public List<String> getDebugInfo() {
-        return mDebugInfo == null
-                ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
+        return mTimeSuggestionHelper.getDebugInfo();
     }
 
     /**
@@ -102,10 +85,7 @@
      * {@link #equals(Object)} and {@link #hashCode()}.
      */
     public void addDebugInfo(String... debugInfos) {
-        if (mDebugInfo == null) {
-            mDebugInfo = new ArrayList<>();
-        }
-        mDebugInfo.addAll(Arrays.asList(debugInfos));
+        mTimeSuggestionHelper.addDebugInfo(debugInfos);
     }
 
     @Override
@@ -117,19 +97,28 @@
             return false;
         }
         ManualTimeSuggestion that = (ManualTimeSuggestion) o;
-        return Objects.equals(mUnixEpochTime, that.mUnixEpochTime);
+        return mTimeSuggestionHelper.handleEquals(that.mTimeSuggestionHelper);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mUnixEpochTime);
+        return mTimeSuggestionHelper.hashCode();
     }
 
     @Override
     public String toString() {
-        return "ManualTimeSuggestion{"
-                + "mUnixEpochTime=" + mUnixEpochTime
-                + ", mDebugInfo=" + mDebugInfo
-                + '}';
+        return mTimeSuggestionHelper.handleToString();
+    }
+
+    /** @hide */
+    public static ManualTimeSuggestion parseCommandLineArg(@NonNull ShellCommand cmd)
+            throws IllegalArgumentException {
+        return new ManualTimeSuggestion(
+                TimeSuggestionHelper.handleParseCommandLineArg(ManualTimeSuggestion.class, cmd));
+    }
+
+    /** @hide */
+    public static void printCommandLineOpts(PrintWriter pw) {
+        TimeSuggestionHelper.handlePrintCommandLineOpts(pw, "Manual", ManualTimeSuggestion.class);
     }
 }
diff --git a/core/java/android/app/timedetector/NetworkTimeSuggestion.java b/core/java/android/app/timedetector/NetworkTimeSuggestion.java
index e22f1d6e..e93c75c 100644
--- a/core/java/android/app/timedetector/NetworkTimeSuggestion.java
+++ b/core/java/android/app/timedetector/NetworkTimeSuggestion.java
@@ -17,31 +17,19 @@
 package android.app.timedetector;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ShellCommand;
 import android.os.TimestampedValue;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
+import java.io.PrintWriter;
 import java.util.List;
 import java.util.Objects;
 
 /**
  * A time signal from a network time source like NTP.
  *
- * <p>{@code unixEpochTime} contains the suggested time. The {@code unixEpochTime.value} is the
- * number of milliseconds elapsed since 1/1/1970 00:00:00 UTC according to the Unix time system.
- * The {@code unixEpochTime.referenceTimeMillis} is the value of the elapsed realtime clock when
- * the {@code unixEpochTime.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 determined. 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)}.
+ * <p>See {@link TimeSuggestionHelper} for property information.
  *
  * @hide
  */
@@ -50,7 +38,9 @@
     public static final @NonNull Creator<NetworkTimeSuggestion> CREATOR =
             new Creator<NetworkTimeSuggestion>() {
                 public NetworkTimeSuggestion createFromParcel(Parcel in) {
-                    return NetworkTimeSuggestion.createFromParcel(in);
+                    TimeSuggestionHelper helper = TimeSuggestionHelper.handleCreateFromParcel(
+                            NetworkTimeSuggestion.class, in);
+                    return new NetworkTimeSuggestion(helper);
                 }
 
                 public NetworkTimeSuggestion[] newArray(int size) {
@@ -58,21 +48,15 @@
                 }
             };
 
-    @NonNull private final TimestampedValue<Long> mUnixEpochTime;
-    @Nullable private ArrayList<String> mDebugInfo;
+    @NonNull private final TimeSuggestionHelper mTimeSuggestionHelper;
 
     public NetworkTimeSuggestion(@NonNull TimestampedValue<Long> unixEpochTime) {
-        mUnixEpochTime = Objects.requireNonNull(unixEpochTime);
-        Objects.requireNonNull(unixEpochTime.getValue());
+        mTimeSuggestionHelper = new TimeSuggestionHelper(
+                NetworkTimeSuggestion.class, unixEpochTime);
     }
 
-    private static NetworkTimeSuggestion createFromParcel(Parcel in) {
-        TimestampedValue<Long> unixEpochTime = in.readParcelable(null /* classLoader */);
-        NetworkTimeSuggestion suggestion = new NetworkTimeSuggestion(unixEpochTime);
-        @SuppressWarnings("unchecked")
-        ArrayList<String> debugInfo = (ArrayList<String>) in.readArrayList(null /* classLoader */);
-        suggestion.mDebugInfo = debugInfo;
-        return suggestion;
+    private NetworkTimeSuggestion(@NonNull TimeSuggestionHelper helper) {
+        mTimeSuggestionHelper = Objects.requireNonNull(helper);
     }
 
     @Override
@@ -82,35 +66,30 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
-        dest.writeParcelable(mUnixEpochTime, 0);
-        dest.writeList(mDebugInfo);
+        mTimeSuggestionHelper.handleWriteToParcel(dest, flags);
     }
 
     @NonNull
     public TimestampedValue<Long> getUnixEpochTime() {
-        return mUnixEpochTime;
+        return mTimeSuggestionHelper.getUnixEpochTime();
     }
 
     @NonNull
     public List<String> getDebugInfo() {
-        return mDebugInfo == null
-                ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
+        return mTimeSuggestionHelper.getDebugInfo();
     }
 
     /**
      * 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()}.
+     * 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));
+        mTimeSuggestionHelper.addDebugInfo(debugInfos);
     }
 
     @Override
-    public boolean equals(@Nullable Object o) {
+    public boolean equals(Object o) {
         if (this == o) {
             return true;
         }
@@ -118,19 +97,28 @@
             return false;
         }
         NetworkTimeSuggestion that = (NetworkTimeSuggestion) o;
-        return Objects.equals(mUnixEpochTime, that.mUnixEpochTime);
+        return mTimeSuggestionHelper.handleEquals(that.mTimeSuggestionHelper);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mUnixEpochTime);
+        return mTimeSuggestionHelper.hashCode();
     }
 
     @Override
     public String toString() {
-        return "NetworkTimeSuggestion{"
-                + "mUnixEpochTime=" + mUnixEpochTime
-                + ", mDebugInfo=" + mDebugInfo
-                + '}';
+        return mTimeSuggestionHelper.handleToString();
+    }
+
+    /** @hide */
+    public static NetworkTimeSuggestion parseCommandLineArg(@NonNull ShellCommand cmd)
+            throws IllegalArgumentException {
+        return new NetworkTimeSuggestion(
+                TimeSuggestionHelper.handleParseCommandLineArg(NetworkTimeSuggestion.class, cmd));
+    }
+
+    /** @hide */
+    public static void printCommandLineOpts(PrintWriter pw) {
+        TimeSuggestionHelper.handlePrintCommandLineOpts(pw, "Network", NetworkTimeSuggestion.class);
     }
 }
diff --git a/core/java/android/app/timedetector/TelephonyTimeSuggestion.java b/core/java/android/app/timedetector/TelephonyTimeSuggestion.java
index 4ff7517..6f204d6 100644
--- a/core/java/android/app/timedetector/TelephonyTimeSuggestion.java
+++ b/core/java/android/app/timedetector/TelephonyTimeSuggestion.java
@@ -20,8 +20,10 @@
 import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.ShellCommand;
 import android.os.TimestampedValue;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -88,6 +90,61 @@
         return suggestion;
     }
 
+    /** @hide */
+    public static TelephonyTimeSuggestion parseCommandLineArg(@NonNull ShellCommand cmd)
+            throws IllegalArgumentException {
+        Integer slotIndex = null;
+        Long referenceTimeMillis = null;
+        Long unixEpochTimeMillis = null;
+        String opt;
+        while ((opt = cmd.getNextArg()) != null) {
+            switch (opt) {
+                case "--slot_index": {
+                    slotIndex = Integer.parseInt(cmd.getNextArgRequired());
+                    break;
+                }
+                case "--reference_time": {
+                    referenceTimeMillis = Long.parseLong(cmd.getNextArgRequired());
+                    break;
+                }
+                case "--unix_epoch_time": {
+                    unixEpochTimeMillis = Long.parseLong(cmd.getNextArgRequired());
+                    break;
+                }
+                default: {
+                    throw new IllegalArgumentException("Unknown option: " + opt);
+                }
+            }
+        }
+
+        if (slotIndex == null) {
+            throw new IllegalArgumentException("No slotIndex specified.");
+        }
+        if (referenceTimeMillis == null) {
+            throw new IllegalArgumentException("No referenceTimeMillis specified.");
+        }
+        if (unixEpochTimeMillis == null) {
+            throw new IllegalArgumentException("No unixEpochTimeMillis specified.");
+        }
+
+        TimestampedValue<Long> timeSignal =
+                new TimestampedValue<>(referenceTimeMillis, unixEpochTimeMillis);
+        Builder builder = new Builder(slotIndex)
+                .setUnixEpochTime(timeSignal)
+                .addDebugInfo("Command line injection");
+        return builder.build();
+    }
+
+    /** @hide */
+    public static void printCommandLineOpts(PrintWriter pw) {
+        pw.println("Telephony suggestion options:");
+        pw.println("  --slot_index <number>");
+        pw.println("  --reference_time <elapsed realtime millis>");
+        pw.println("  --unix_epoch_time <Unix epoch time millis>");
+        pw.println();
+        pw.println("See " + TelephonyTimeSuggestion.class.getName() + " for more information");
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/core/java/android/app/timedetector/TimeDetector.java b/core/java/android/app/timedetector/TimeDetector.java
index a356230..f0d77761 100644
--- a/core/java/android/app/timedetector/TimeDetector.java
+++ b/core/java/android/app/timedetector/TimeDetector.java
@@ -44,6 +44,36 @@
     String SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED = "is_auto_detection_enabled";
 
     /**
+     * A shell command that injects a manual time suggestion.
+     * @hide
+     */
+    String SHELL_COMMAND_SUGGEST_MANUAL_TIME = "suggest_manual_time";
+
+    /**
+     * A shell command that injects a telephony time suggestion.
+     * @hide
+     */
+    String SHELL_COMMAND_SUGGEST_TELEPHONY_TIME = "suggest_telephony_time";
+
+    /**
+     * A shell command that injects a network time suggestion.
+     * @hide
+     */
+    String SHELL_COMMAND_SUGGEST_NETWORK_TIME = "suggest_network_time";
+
+    /**
+     * A shell command that injects a GNSS time suggestion.
+     * @hide
+     */
+    String SHELL_COMMAND_SUGGEST_GNSS_TIME = "suggest_gnss_time";
+
+    /**
+     * A shell command that injects a external time suggestion.
+     * @hide
+     */
+    String SHELL_COMMAND_SUGGEST_EXTERNAL_TIME = "suggest_external_time";
+
+    /**
      * A shared utility method to create a {@link ManualTimeSuggestion}.
      *
      * @hide
diff --git a/core/java/android/app/timedetector/TimeSuggestionHelper.java b/core/java/android/app/timedetector/TimeSuggestionHelper.java
new file mode 100644
index 0000000..9b99be6
--- /dev/null
+++ b/core/java/android/app/timedetector/TimeSuggestionHelper.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2022 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.ShellCommand;
+import android.os.TimestampedValue;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A delegate class to support time suggestion classes that could diverge in the future. This class
+ * exists purely for code re-use and provides support methods. It avoids class inheritance
+ * deliberately to allow each suggestion to evolve in different directions later without affecting
+ * SDK APIs.
+ *
+ * <p>{@code unixEpochTime} is the suggested time. The {@code unixEpochTime.value} is the number of
+ * milliseconds elapsed since 1/1/1970 00:00:00 UTC according to the Unix time system. The {@code
+ * unixEpochTime.referenceTimeMillis} is the value of the elapsed realtime clock when the {@code
+ * unixEpochTime.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 TimeSuggestionHelper {
+
+    @NonNull private final Class<?> mHelpedClass;
+    @NonNull private final TimestampedValue<Long> mUnixEpochTime;
+    @Nullable private ArrayList<String> mDebugInfo;
+
+    /** Creates a helper for the specified class, containing the supplied properties. */
+    public TimeSuggestionHelper(@NonNull Class<?> helpedClass,
+            @NonNull TimestampedValue<Long> unixEpochTime) {
+        mHelpedClass = Objects.requireNonNull(helpedClass);
+        mUnixEpochTime = Objects.requireNonNull(unixEpochTime);
+        Objects.requireNonNull(unixEpochTime.getValue());
+    }
+
+    /** See {@link TimeSuggestionHelper} for property details. */
+    @NonNull
+    public TimestampedValue<Long> getUnixEpochTime() {
+        return mUnixEpochTime;
+    }
+
+    /** See {@link TimeSuggestionHelper} for information about {@code debugInfo}. */
+    @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.
+     *
+     * <p>See {@link TimeSuggestionHelper} for more information about {@code debugInfo}.
+     */
+    public void addDebugInfo(@NonNull String debugInfo) {
+        if (mDebugInfo == null) {
+            mDebugInfo = new ArrayList<>();
+        }
+        mDebugInfo.add(debugInfo);
+    }
+
+    /**
+     * 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) {
+        addDebugInfo(Arrays.asList(debugInfos));
+    }
+
+    /**
+     * Associates information with the instance that can be useful for debugging / logging.
+     *
+     * <p>See {@link TimeSuggestionHelper} for more information about {@code debugInfo}.
+     */
+    public void addDebugInfo(@NonNull List<String> debugInfo) {
+        if (mDebugInfo == null) {
+            mDebugInfo = new ArrayList<>(debugInfo.size());
+        }
+        mDebugInfo.addAll(debugInfo);
+    }
+
+    /**
+     * Implemented in case users call this insteam of {@link #handleEquals(TimeSuggestionHelper)}.
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        TimeSuggestionHelper that = (TimeSuggestionHelper) o;
+        return handleEquals(that);
+    }
+
+    /** Used to implement {@link Object#equals(Object)}. */
+    public boolean handleEquals(TimeSuggestionHelper o) {
+        return Objects.equals(mHelpedClass, o.mHelpedClass)
+                && Objects.equals(mUnixEpochTime, o.mUnixEpochTime);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mUnixEpochTime);
+    }
+
+    /** Used to implement {@link Object#toString()}. */
+    public String handleToString() {
+        return mHelpedClass.getSimpleName() + "{"
+                + "mUnixEpochTime=" + mUnixEpochTime
+                + ", mDebugInfo=" + mDebugInfo
+                + '}';
+    }
+
+    /** Constructs a helper with suggestion state from a Parcel. */
+    public static TimeSuggestionHelper handleCreateFromParcel(@NonNull Class<?> helpedClass,
+            @NonNull Parcel in) {
+        @SuppressWarnings("unchecked")
+        TimestampedValue<Long> unixEpochTime = in.readParcelable(
+                null /* classLoader */, TimestampedValue.class);
+        TimeSuggestionHelper suggestionHelper =
+                new TimeSuggestionHelper(helpedClass, unixEpochTime);
+        suggestionHelper.mDebugInfo = in.readArrayList(null /* classLoader */, String.class);
+        return suggestionHelper;
+    }
+
+    /** Writes the helper suggestion state to a Parcel. */
+    public void handleWriteToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mUnixEpochTime, 0);
+        dest.writeList(mDebugInfo);
+    }
+
+    /** Parses command line args to create a {@link TimeSuggestionHelper}. */
+    public static TimeSuggestionHelper handleParseCommandLineArg(
+            @NonNull Class<?> helpedClass, @NonNull ShellCommand cmd)
+            throws IllegalArgumentException {
+        Long referenceTimeMillis = null;
+        Long unixEpochTimeMillis = null;
+        String opt;
+        while ((opt = cmd.getNextArg()) != null) {
+            switch (opt) {
+                case "--reference_time": {
+                    referenceTimeMillis = Long.parseLong(cmd.getNextArgRequired());
+                    break;
+                }
+                case "--unix_epoch_time": {
+                    unixEpochTimeMillis = Long.parseLong(cmd.getNextArgRequired());
+                    break;
+                }
+                default: {
+                    throw new IllegalArgumentException("Unknown option: " + opt);
+                }
+            }
+        }
+
+        if (referenceTimeMillis == null) {
+            throw new IllegalArgumentException("No referenceTimeMillis specified.");
+        }
+        if (unixEpochTimeMillis == null) {
+            throw new IllegalArgumentException("No unixEpochTimeMillis specified.");
+        }
+
+        TimestampedValue<Long> timeSignal =
+                new TimestampedValue<>(referenceTimeMillis, unixEpochTimeMillis);
+        TimeSuggestionHelper suggestionHelper = new TimeSuggestionHelper(helpedClass, timeSignal);
+        suggestionHelper.addDebugInfo("Command line injection");
+        return suggestionHelper;
+    }
+
+    /** Prints the command line args needed to create a {@link TimeSuggestionHelper}. */
+    public static void handlePrintCommandLineOpts(
+            @NonNull PrintWriter pw, @NonNull String typeName, @NonNull Class<?> clazz) {
+        pw.printf("%s suggestion options:\n", typeName);
+        pw.println("  --reference_time <elapsed realtime millis>");
+        pw.println("  --unix_epoch_time <Unix epoch time millis>");
+        pw.println();
+        pw.println("See " + clazz.getName() + " for more information");
+    }
+}
diff --git a/core/tests/coretests/src/android/app/time/ExternalTimeSuggestionTest.java b/core/tests/coretests/src/android/app/time/ExternalTimeSuggestionTest.java
new file mode 100644
index 0000000..90b3305
--- /dev/null
+++ b/core/tests/coretests/src/android/app/time/ExternalTimeSuggestionTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 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.time;
+
+import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.ShellCommand;
+
+import org.junit.Test;
+
+/**
+ * Tests for non-SDK methods on {@link ExternalTimeSuggestion}.
+ * Also see {@link android.app.time.cts.ExternalTimeSuggestionTest}
+ */
+public class ExternalTimeSuggestionTest {
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noReferenceTime() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--unix_epoch_time 12345");
+        ExternalTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noUnixEpochTime() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321");
+        ExternalTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test
+    public void testParseCommandLineArg_validSuggestion() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321 --unix_epoch_time 12345");
+        ExternalTimeSuggestion expectedSuggestion = new ExternalTimeSuggestion(54321L, 12345L);
+        ExternalTimeSuggestion actualSuggestion =
+                ExternalTimeSuggestion.parseCommandLineArg(testShellCommand);
+        assertEquals(expectedSuggestion, actualSuggestion);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_unknownArgument() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321 --unix_epoch_time 12345 --bad_arg 0");
+        ExternalTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+}
diff --git a/core/tests/coretests/src/android/app/timedetector/GnssTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/GnssTimeSuggestionTest.java
index e248010..af403a2 100644
--- a/core/tests/coretests/src/android/app/timedetector/GnssTimeSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timedetector/GnssTimeSuggestionTest.java
@@ -18,10 +18,12 @@
 
 import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
 import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
+import android.os.ShellCommand;
 import android.os.TimestampedValue;
 
 import org.junit.Test;
@@ -63,4 +65,36 @@
         GnssTimeSuggestion rtSuggestion = roundTripParcelable(suggestion);
         assertEquals(suggestion.getDebugInfo(), rtSuggestion.getDebugInfo());
     }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noReferenceTime() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--unix_epoch_time 12345");
+        GnssTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noUnixEpochTime() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321");
+        GnssTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test
+    public void testParseCommandLineArg_validSuggestion() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321 --unix_epoch_time 12345");
+        TimestampedValue<Long> timeSignal = new TimestampedValue<>(54321L, 12345L);
+        GnssTimeSuggestion expectedSuggestion = new GnssTimeSuggestion(timeSignal);
+        GnssTimeSuggestion actualSuggestion =
+                GnssTimeSuggestion.parseCommandLineArg(testShellCommand);
+        assertEquals(expectedSuggestion, actualSuggestion);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_unknownArgument() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321 --unix_epoch_time 12345 --bad_arg 0");
+        GnssTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
 }
diff --git a/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java
index 750ffa1..94218cd 100644
--- a/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java
@@ -18,10 +18,12 @@
 
 import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
 import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
+import android.os.ShellCommand;
 import android.os.TimestampedValue;
 
 import org.junit.Test;
@@ -63,4 +65,36 @@
         ManualTimeSuggestion rtSuggestion = roundTripParcelable(suggestion);
         assertEquals(suggestion.getDebugInfo(), rtSuggestion.getDebugInfo());
     }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noReferenceTime() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--unix_epoch_time 12345");
+        ManualTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noUnixEpochTime() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321");
+        ManualTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test
+    public void testParseCommandLineArg_validSuggestion() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321 --unix_epoch_time 12345");
+        TimestampedValue<Long> timeSignal = new TimestampedValue<>(54321L, 12345L);
+        ManualTimeSuggestion expectedSuggestion = new ManualTimeSuggestion(timeSignal);
+        ManualTimeSuggestion actualSuggestion =
+                ManualTimeSuggestion.parseCommandLineArg(testShellCommand);
+        assertEquals(expectedSuggestion, actualSuggestion);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_unknownArgument() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321 --unix_epoch_time 12345 --bad_arg 0");
+        ManualTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
 }
diff --git a/core/tests/coretests/src/android/app/timedetector/NetworkTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/NetworkTimeSuggestionTest.java
index b88c36f..0e09dd3 100644
--- a/core/tests/coretests/src/android/app/timedetector/NetworkTimeSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timedetector/NetworkTimeSuggestionTest.java
@@ -18,10 +18,12 @@
 
 import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
 import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
+import android.os.ShellCommand;
 import android.os.TimestampedValue;
 
 import org.junit.Test;
@@ -63,4 +65,36 @@
         NetworkTimeSuggestion rtSuggestion = roundTripParcelable(suggestion);
         assertEquals(suggestion.getDebugInfo(), rtSuggestion.getDebugInfo());
     }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noReferenceTime() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--unix_epoch_time 12345");
+        NetworkTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noUnixEpochTime() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321");
+        NetworkTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test
+    public void testParseCommandLineArg_validSuggestion() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321 --unix_epoch_time 12345");
+        TimestampedValue<Long> timeSignal = new TimestampedValue<>(54321L, 12345L);
+        NetworkTimeSuggestion expectedSuggestion = new NetworkTimeSuggestion(timeSignal);
+        NetworkTimeSuggestion actualSuggestion =
+                NetworkTimeSuggestion.parseCommandLineArg(testShellCommand);
+        assertEquals(expectedSuggestion, actualSuggestion);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_unknownArgument() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321 --unix_epoch_time 12345 --bad_arg 0");
+        NetworkTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
 }
diff --git a/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java
index cc75579..bb995a8 100644
--- a/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timedetector/TelephonyTimeSuggestionTest.java
@@ -18,10 +18,12 @@
 
 import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
 import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+import static android.app.timezonedetector.ShellCommandTestSupport.createShellCommandWithArgsAndOptions;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
+import android.os.ShellCommand;
 import android.os.TimestampedValue;
 
 import org.junit.Test;
@@ -95,4 +97,45 @@
             assertEquals(suggestion1.getDebugInfo(), rtSuggestion1.getDebugInfo());
         }
     }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noSlotIndex() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--reference_time 54321 --unix_epoch_time 12345");
+        TelephonyTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noReferenceTime() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--slot_index 0 --unix_epoch_time 12345");
+        TelephonyTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_noUnixEpochTime() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--slot_index 0 --reference_time 54321");
+        TelephonyTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
+
+    @Test
+    public void testParseCommandLineArg_validSuggestion() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--slot_index 0 --reference_time 54321 --unix_epoch_time 12345");
+        TelephonyTimeSuggestion expectedSuggestion =
+                new TelephonyTimeSuggestion.Builder(0)
+                        .setUnixEpochTime(new TimestampedValue<>(54321L, 12345L))
+                        .build();
+        TelephonyTimeSuggestion actualSuggestion =
+                TelephonyTimeSuggestion.parseCommandLineArg(testShellCommand);
+        assertEquals(expectedSuggestion, actualSuggestion);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testParseCommandLineArg_unknownArgument() {
+        ShellCommand testShellCommand = createShellCommandWithArgsAndOptions(
+                "--slot_index 0 --reference_time 54321 --unix_epoch_time 12345 --bad_arg 0");
+        TelephonyTimeSuggestion.parseCommandLineArg(testShellCommand);
+    }
 }
diff --git a/core/tests/coretests/src/android/app/timezonedetector/ShellCommandTestSupport.java b/core/tests/coretests/src/android/app/timezonedetector/ShellCommandTestSupport.java
index 8d8290c..4efaed1 100644
--- a/core/tests/coretests/src/android/app/timezonedetector/ShellCommandTestSupport.java
+++ b/core/tests/coretests/src/android/app/timezonedetector/ShellCommandTestSupport.java
@@ -26,14 +26,14 @@
 import java.util.List;
 
 /** Utility methods related to {@link ShellCommand} objects used in several tests. */
-final class ShellCommandTestSupport {
+public final class ShellCommandTestSupport {
     private ShellCommandTestSupport() {}
 
-    static ShellCommand createShellCommandWithArgsAndOptions(String argsWithSpaces) {
+    public static ShellCommand createShellCommandWithArgsAndOptions(String argsWithSpaces) {
         return createShellCommandWithArgsAndOptions(Arrays.asList(argsWithSpaces.split(" ")));
     }
 
-    static ShellCommand createShellCommandWithArgsAndOptions(List<String> args) {
+    public static ShellCommand createShellCommandWithArgsAndOptions(List<String> args) {
         ShellCommand command = mock(ShellCommand.class);
         class ArgProvider {
             private int mCount;
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 8b27522..c7d2d79 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -473,6 +473,12 @@
     <uses-permission android:name="android.permission.MANAGE_TIME_AND_ZONE_DETECTION" />
     <uses-permission android:name="android.permission.SUGGEST_EXTERNAL_TIME" />
 
+    <!-- Permissions used for manual testing of time detection behavior. -->
+    <uses-permission android:name="android.permission.SUGGEST_MANUAL_TIME" />
+    <uses-permission android:name="android.permission.SUGGEST_TELEPHONY_TIME" />
+    <uses-permission android:name="android.permission.SUGGEST_NETWORK_TIME" />
+    <uses-permission android:name="android.permission.SUGGEST_GNSS_TIME" />
+
     <!-- Permission required for CTS test - android.server.biometrics -->
     <uses-permission android:name="android.permission.USE_BIOMETRIC" />
 
diff --git a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
index 7649958..9d26351 100644
--- a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
+++ b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java
@@ -22,6 +22,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
+import android.os.Build;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.SystemClock;
@@ -166,6 +167,11 @@
         mWakeLock.release();
     }
 
+    @Override
+    public boolean deviceHasY2038Issue() {
+        return Build.SUPPORTED_32_BIT_ABIS.length > 0;
+    }
+
     private void checkWakeLockHeld() {
         if (!mWakeLock.isHeld()) {
             Slog.wtf(LOG_TAG, "WakeLock " + mWakeLock + " not held");
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java b/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java
index 721986b..cc5e6fe 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java
@@ -17,14 +17,26 @@
 
 import static android.app.timedetector.TimeDetector.SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED;
 import static android.app.timedetector.TimeDetector.SHELL_COMMAND_SERVICE_NAME;
+import static android.app.timedetector.TimeDetector.SHELL_COMMAND_SUGGEST_EXTERNAL_TIME;
+import static android.app.timedetector.TimeDetector.SHELL_COMMAND_SUGGEST_GNSS_TIME;
+import static android.app.timedetector.TimeDetector.SHELL_COMMAND_SUGGEST_MANUAL_TIME;
+import static android.app.timedetector.TimeDetector.SHELL_COMMAND_SUGGEST_NETWORK_TIME;
+import static android.app.timedetector.TimeDetector.SHELL_COMMAND_SUGGEST_TELEPHONY_TIME;
 import static android.provider.DeviceConfig.NAMESPACE_SYSTEM_TIME;
 
 import static com.android.server.timedetector.ServerFlags.KEY_TIME_DETECTOR_LOWER_BOUND_MILLIS_OVERRIDE;
 import static com.android.server.timedetector.ServerFlags.KEY_TIME_DETECTOR_ORIGIN_PRIORITIES_OVERRIDE;
 
+import android.app.time.ExternalTimeSuggestion;
+import android.app.timedetector.GnssTimeSuggestion;
+import android.app.timedetector.ManualTimeSuggestion;
+import android.app.timedetector.NetworkTimeSuggestion;
+import android.app.timedetector.TelephonyTimeSuggestion;
 import android.os.ShellCommand;
 
 import java.io.PrintWriter;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
 
 /** Implements the shell command interface for {@link TimeDetectorService}. */
 class TimeDetectorShellCommand extends ShellCommand {
@@ -44,6 +56,16 @@
         switch (cmd) {
             case SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED:
                 return runIsAutoDetectionEnabled();
+            case SHELL_COMMAND_SUGGEST_MANUAL_TIME:
+                return runSuggestManualTime();
+            case SHELL_COMMAND_SUGGEST_TELEPHONY_TIME:
+                return runSuggestTelephonyTime();
+            case SHELL_COMMAND_SUGGEST_NETWORK_TIME:
+                return runSuggestNetworkTime();
+            case SHELL_COMMAND_SUGGEST_GNSS_TIME:
+                return runSuggestGnssTime();
+            case SHELL_COMMAND_SUGGEST_EXTERNAL_TIME:
+                return runSuggestExternalTime();
             default: {
                 return handleDefaultCommands(cmd);
             }
@@ -59,6 +81,53 @@
         return 0;
     }
 
+    private int runSuggestManualTime() {
+        return runSuggestTime(
+                () -> ManualTimeSuggestion.parseCommandLineArg(this),
+                mInterface::suggestManualTime);
+    }
+
+    private int runSuggestTelephonyTime() {
+        return runSuggestTime(
+                () -> TelephonyTimeSuggestion.parseCommandLineArg(this),
+                mInterface::suggestTelephonyTime);
+    }
+
+    private int runSuggestNetworkTime() {
+        return runSuggestTime(
+                () -> NetworkTimeSuggestion.parseCommandLineArg(this),
+                mInterface::suggestNetworkTime);
+    }
+
+    private int runSuggestGnssTime() {
+        return runSuggestTime(
+                () -> GnssTimeSuggestion.parseCommandLineArg(this),
+                mInterface::suggestGnssTime);
+    }
+
+    private int runSuggestExternalTime() {
+        return runSuggestTime(
+                () -> ExternalTimeSuggestion.parseCommandLineArg(this),
+                mInterface::suggestExternalTime);
+    }
+
+    private <T> int runSuggestTime(Supplier<T> suggestionParser, Consumer<T> invoker) {
+        final PrintWriter pw = getOutPrintWriter();
+        try {
+            T suggestion = suggestionParser.get();
+            if (suggestion == null) {
+                pw.println("Error: suggestion not specified");
+                return 1;
+            }
+            invoker.accept(suggestion);
+            pw.println("Suggestion " + suggestion + " injected.");
+            return 0;
+        } catch (RuntimeException e) {
+            pw.println(e);
+            return 1;
+        }
+    }
+
     @Override
     public void onHelp() {
         final PrintWriter pw = getOutPrintWriter();
@@ -68,6 +137,22 @@
         pw.printf("  %s\n", SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED);
         pw.printf("    Prints true/false according to the automatic time detection setting.\n");
         pw.println();
+        pw.printf("  %s <manual suggestion opts>\n", SHELL_COMMAND_SUGGEST_MANUAL_TIME);
+        pw.printf("  %s <telephony suggestion opts>\n", SHELL_COMMAND_SUGGEST_TELEPHONY_TIME);
+        pw.printf("  %s <network suggestion opts>\n", SHELL_COMMAND_SUGGEST_NETWORK_TIME);
+        pw.printf("  %s <gnss suggestion opts>\n", SHELL_COMMAND_SUGGEST_GNSS_TIME);
+        pw.printf("  %s <external suggestion opts>\n", SHELL_COMMAND_SUGGEST_EXTERNAL_TIME);
+        pw.println();
+        ManualTimeSuggestion.printCommandLineOpts(pw);
+        pw.println();
+        TelephonyTimeSuggestion.printCommandLineOpts(pw);
+        pw.println();
+        NetworkTimeSuggestion.printCommandLineOpts(pw);
+        pw.println();
+        GnssTimeSuggestion.printCommandLineOpts(pw);
+        pw.println();
+        ExternalTimeSuggestion.printCommandLineOpts(pw);
+        pw.println();
         pw.printf("This service is also affected by the following device_config flags in the"
                 + " %s namespace:\n", NAMESPACE_SYSTEM_TIME);
         pw.printf("  %s\n", KEY_TIME_DETECTOR_LOWER_BOUND_MILLIS_OVERRIDE);
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
index ae4d46c..33ab104 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java
@@ -85,6 +85,9 @@
      */
     private static final int KEEP_SUGGESTION_HISTORY_SIZE = 10;
 
+    /** The value in Unix epoch milliseconds of the Y2038 issue. */
+    private static final long Y2038_LIMIT_IN_MILLIS = 1000L * Integer.MAX_VALUE;
+
     /**
      * A log that records the decisions / decision metadata that affected the device's system clock
      * time. This is logged in bug reports to assist with debugging issues with detection.
@@ -185,6 +188,12 @@
 
         /** Release the wake lock acquired by a call to {@link #acquireWakeLock()}. */
         void releaseWakeLock();
+
+        /**
+         * Returns {@code true} if the device may be at risk of time_t overflow (because bionic
+         * defines time_t as a 32-bit signed integer for 32-bit processes).
+         */
+        boolean deviceHasY2038Issue();
     }
 
     static TimeDetectorStrategy create(
@@ -333,6 +342,7 @@
                         .mapToObj(TimeDetectorStrategy::originToString)
                         .collect(joining(",", "[", "]"));
         ipw.println("mEnvironment.autoOriginPriorities()=" + priorities);
+        ipw.println("mEnvironment.deviceHasY2038Issue()=" + mEnvironment.deviceHasY2038Issue());
 
         ipw.println("Time change log:");
         ipw.increaseIndent(); // level 2
@@ -413,6 +423,16 @@
                     + ", suggestion=" + suggestion);
             return false;
         }
+
+        if (newUnixEpochTime.getValue() > Y2038_LIMIT_IN_MILLIS
+                && mEnvironment.deviceHasY2038Issue()) {
+            // This check won't prevent a device's system clock exceeding Integer.MAX_VALUE Unix
+            // seconds through the normal passage of time, but it will stop it jumping above 2038
+            // because of a "bad" suggestion. b/204193177
+            Slog.w(LOG_TAG, "Suggested value is above max time supported by this device."
+                    + " suggestion=" + suggestion);
+            return false;
+        }
         return true;
     }
 
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 2d9903f..2248ddb 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java
@@ -1131,6 +1131,49 @@
                 .verifySystemClockWasSetAndResetCallTracking(ARBITRARY_TEST_TIME.toEpochMilli());
     }
 
+    @Test
+    public void manualY2038SuggestionsAreRejectedOnAffectedDevices() {
+        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
+                .pokeAutoTimeDetectionEnabled(false)
+                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY)
+                .pokeDeviceHasY2038Issues(true);
+
+        Instant y2038IssueTime = Instant.ofEpochMilli((1L + Integer.MAX_VALUE) * 1000L);
+        ManualTimeSuggestion timeSuggestion = mScript.generateManualTimeSuggestion(y2038IssueTime);
+        mScript.simulateManualTimeSuggestion(timeSuggestion, false /* expectedResult */)
+                .verifySystemClockWasNotSetAndResetCallTracking();
+    }
+
+    @Test
+    public void telephonyY2038SuggestionsAreRejectedOnAffectedDevices() {
+        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
+                .pokeAutoTimeDetectionEnabled(true)
+                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY)
+                .pokeDeviceHasY2038Issues(true);
+
+        final int slotIndex = 0;
+        Instant y2038IssueTime = Instant.ofEpochMilli((1L + Integer.MAX_VALUE) * 1000L);
+        TelephonyTimeSuggestion timeSuggestion =
+                mScript.generateTelephonyTimeSuggestion(slotIndex, y2038IssueTime);
+        mScript.simulateTelephonyTimeSuggestion(timeSuggestion)
+                .verifySystemClockWasNotSetAndResetCallTracking();
+    }
+
+    @Test
+    public void telephonyY2038SuggestionsAreNotRejectedOnUnaffectedDevices() {
+        mScript.pokeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO)
+                .pokeAutoTimeDetectionEnabled(true)
+                .pokeAutoOriginPriorities(ORIGIN_TELEPHONY)
+                .pokeDeviceHasY2038Issues(false);
+
+        final int slotIndex = 0;
+        Instant y2038IssueTime = Instant.ofEpochMilli((1L + Integer.MAX_VALUE) * 1000L);
+        TelephonyTimeSuggestion timeSuggestion =
+                mScript.generateTelephonyTimeSuggestion(slotIndex, y2038IssueTime);
+        mScript.simulateTelephonyTimeSuggestion(timeSuggestion)
+                .verifySystemClockWasSetAndResetCallTracking(y2038IssueTime.toEpochMilli());
+    }
+
     /**
      * A fake implementation of {@link TimeDetectorStrategyImpl.Environment}. Besides tracking
      * changes and behaving like the real thing should, it also asserts preconditions.
@@ -1143,6 +1186,7 @@
         private int mSystemClockUpdateThresholdMillis = 2000;
         private int[] mAutoOriginPriorities = PROVIDERS_PRIORITY;
         private ConfigurationChangeListener mConfigChangeListener;
+        private boolean mDeviceHas2038Issues = false;
 
         // Tracking operations.
         private boolean mSystemClockWasSet;
@@ -1208,6 +1252,15 @@
             mWakeLockAcquired = false;
         }
 
+        public void setDeviceHas2038Issues(boolean hasIssues) {
+            mDeviceHas2038Issues = hasIssues;
+        }
+
+        @Override
+        public boolean deviceHasY2038Issue() {
+            return mDeviceHas2038Issues;
+        }
+
         // Methods below are for managing the fake's behavior.
 
         void pokeSystemClockUpdateThreshold(int thresholdMillis) {
@@ -1304,6 +1357,11 @@
             return this;
         }
 
+        Script pokeDeviceHasY2038Issues(boolean hasIssues) {
+            mFakeEnvironment.setDeviceHas2038Issues(hasIssues);
+            return this;
+        }
+
         long peekElapsedRealtimeMillis() {
             return mFakeEnvironment.peekElapsedRealtimeMillis();
         }