Merge "Add a manual path for setting time zone"
diff --git a/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl b/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl
index 260c7df..df643831 100644
--- a/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl
+++ b/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl
@@ -16,6 +16,7 @@
 
 package android.app.timezonedetector;
 
+import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.PhoneTimeZoneSuggestion;
 
 /**
@@ -32,5 +33,6 @@
  * {@hide}
  */
 interface ITimeZoneDetectorService {
+  void suggestManualTimeZone(in ManualTimeZoneSuggestion timeZoneSuggestion);
   void suggestPhoneTimeZone(in PhoneTimeZoneSuggestion timeZoneSuggestion);
 }
diff --git a/core/java/android/app/timezonedetector/ManualTimeZoneSuggestion.aidl b/core/java/android/app/timezonedetector/ManualTimeZoneSuggestion.aidl
new file mode 100644
index 0000000..d1be86a
--- /dev/null
+++ b/core/java/android/app/timezonedetector/ManualTimeZoneSuggestion.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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.timezonedetector;
+
+parcelable ManualTimeZoneSuggestion;
diff --git a/core/java/android/app/timezonedetector/ManualTimeZoneSuggestion.java b/core/java/android/app/timezonedetector/ManualTimeZoneSuggestion.java
new file mode 100644
index 0000000..a6b953b
--- /dev/null
+++ b/core/java/android/app/timezonedetector/ManualTimeZoneSuggestion.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2019 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.timezonedetector;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+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 manual (user provided) source. The value consists of the number of
+ * milliseconds elapsed since 1/1/1970 00:00:00 UTC and the time according to the elapsed realtime
+ * clock when that number was established. The elapsed realtime clock is considered accurate but
+ * volatile, so time signals must not be persisted across device resets.
+ *
+ * @hide
+ */
+public final class ManualTimeZoneSuggestion implements Parcelable {
+
+    public static final @NonNull Creator<ManualTimeZoneSuggestion> CREATOR =
+            new Creator<ManualTimeZoneSuggestion>() {
+                public ManualTimeZoneSuggestion createFromParcel(Parcel in) {
+                    return ManualTimeZoneSuggestion.createFromParcel(in);
+                }
+
+                public ManualTimeZoneSuggestion[] newArray(int size) {
+                    return new ManualTimeZoneSuggestion[size];
+                }
+            };
+
+    @NonNull
+    private final String mZoneId;
+    @Nullable
+    private ArrayList<String> mDebugInfo;
+
+    public ManualTimeZoneSuggestion(@NonNull String zoneId) {
+        mZoneId = Objects.requireNonNull(zoneId);
+    }
+
+    private static ManualTimeZoneSuggestion createFromParcel(Parcel in) {
+        String zoneId = in.readString();
+        ManualTimeZoneSuggestion suggestion = new ManualTimeZoneSuggestion(zoneId);
+        @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.writeString(mZoneId);
+        dest.writeList(mDebugInfo);
+    }
+
+    @NonNull
+    public String getZoneId() {
+        return mZoneId;
+    }
+
+    @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;
+        }
+        ManualTimeZoneSuggestion
+                that = (ManualTimeZoneSuggestion) o;
+        return Objects.equals(mZoneId, that.mZoneId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mZoneId);
+    }
+
+    @Override
+    public String toString() {
+        return "ManualTimeSuggestion{"
+                + "mZoneId=" + mZoneId
+                + ", mDebugInfo=" + mDebugInfo
+                + '}';
+    }
+}
diff --git a/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java b/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java
new file mode 100644
index 0000000..02ed0ed
--- /dev/null
+++ b/core/tests/coretests/src/android/app/timezonedetector/ManualTimeZoneSuggestionTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2019 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.timezonedetector;
+
+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 org.junit.Test;
+
+public class ManualTimeZoneSuggestionTest {
+
+    private static final String ARBITRARY_ZONE_ID1 = "Europe/London";
+    private static final String ARBITRARY_ZONE_ID2 = "Europe/Paris";
+
+    @Test
+    public void testEquals() {
+        ManualTimeZoneSuggestion one = new ManualTimeZoneSuggestion(ARBITRARY_ZONE_ID1);
+        assertEquals(one, one);
+
+        ManualTimeZoneSuggestion two = new ManualTimeZoneSuggestion(ARBITRARY_ZONE_ID1);
+        assertEquals(one, two);
+        assertEquals(two, one);
+
+        ManualTimeZoneSuggestion three = new ManualTimeZoneSuggestion(ARBITRARY_ZONE_ID2);
+        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() {
+        ManualTimeZoneSuggestion suggestion = new ManualTimeZoneSuggestion(ARBITRARY_ZONE_ID1);
+        assertRoundTripParcelable(suggestion);
+
+        // DebugInfo should also be stored (but is not checked by equals()
+        suggestion.addDebugInfo("This is debug info");
+        ManualTimeZoneSuggestion rtSuggestion = roundTripParcelable(suggestion);
+        assertEquals(suggestion.getDebugInfo(), rtSuggestion.getDebugInfo());
+    }
+}
diff --git a/core/tests/coretests/src/android/app/timezonedetector/ParcelableTestSupport.java b/core/tests/coretests/src/android/app/timezonedetector/ParcelableTestSupport.java
new file mode 100644
index 0000000..0073d86
--- /dev/null
+++ b/core/tests/coretests/src/android/app/timezonedetector/ParcelableTestSupport.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 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.timezonedetector;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.reflect.Field;
+
+/** Utility methods related to {@link Parcelable} objects used in several tests. */
+public final class ParcelableTestSupport {
+
+    private ParcelableTestSupport() {}
+
+    /** Returns the result of parceling and unparceling the argument. */
+    @SuppressWarnings("unchecked")
+    public static <T extends Parcelable> T roundTripParcelable(T parcelable) {
+        Parcel parcel = Parcel.obtain();
+        parcel.writeTypedObject(parcelable, 0);
+        parcel.setDataPosition(0);
+
+        Parcelable.Creator<T> creator;
+        try {
+            Field creatorField = parcelable.getClass().getField("CREATOR");
+            creator = (Parcelable.Creator<T>) creatorField.get(null);
+        } catch (NoSuchFieldException | IllegalAccessException e) {
+            throw new AssertionError(e);
+        }
+        T toReturn = parcel.readTypedObject(creator);
+        parcel.recycle();
+        return toReturn;
+    }
+
+    public static <T extends Parcelable> void assertRoundTripParcelable(T instance) {
+        assertEquals(instance, roundTripParcelable(instance));
+    }
+}
diff --git a/core/tests/coretests/src/android/app/timezonedetector/PhoneTimeZoneSuggestionTest.java b/core/tests/coretests/src/android/app/timezonedetector/PhoneTimeZoneSuggestionTest.java
index ae91edc..0108a0b 100644
--- a/core/tests/coretests/src/android/app/timezonedetector/PhoneTimeZoneSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timezonedetector/PhoneTimeZoneSuggestionTest.java
@@ -16,13 +16,13 @@
 
 package android.app.timezonedetector;
 
+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 static org.junit.Assert.assertTrue;
 
-import android.os.Parcel;
-import android.os.Parcelable;
-
 import org.junit.Test;
 
 public class PhoneTimeZoneSuggestionTest {
@@ -152,19 +152,4 @@
         assertEquals(suggestion1, suggestion1_2);
         assertTrue(suggestion1_2.getDebugInfo().contains(debugString));
     }
-
-    private static void assertRoundTripParcelable(PhoneTimeZoneSuggestion instance) {
-        assertEquals(instance, roundTripParcelable(instance));
-    }
-
-    @SuppressWarnings("unchecked")
-    private static <T extends Parcelable> T roundTripParcelable(T one) {
-        Parcel parcel = Parcel.obtain();
-        parcel.writeTypedObject(one, 0);
-        parcel.setDataPosition(0);
-
-        T toReturn = (T) parcel.readTypedObject(PhoneTimeZoneSuggestion.CREATOR);
-        parcel.recycle();
-        return toReturn;
-    }
 }
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java
index 23746ac..e034ad4 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java
@@ -43,7 +43,7 @@
     }
 
     @Override
-    public boolean isTimeZoneDetectionEnabled() {
+    public boolean isAutoTimeZoneDetectionEnabled() {
         return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE, 1 /* default */) > 0;
     }
 
@@ -66,14 +66,16 @@
     }
 
     @Override
-    public void setDeviceTimeZone(String zoneId) {
+    public void setDeviceTimeZone(String zoneId, boolean sendNetworkBroadcast) {
         AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
         alarmManager.setTimeZone(zoneId);
 
-        // TODO Nothing in the platform appears to listen for this. Remove it.
-        Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE);
-        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
-        intent.putExtra("time-zone", zoneId);
-        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+        if (sendNetworkBroadcast) {
+            // TODO Nothing in the platform appears to listen for this. Remove it.
+            Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE);
+            intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+            intent.putExtra("time-zone", zoneId);
+            mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
index 558aa9e..9a1fe65 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.timezonedetector.ITimeZoneDetectorService;
+import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.PhoneTimeZoneSuggestion;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -28,7 +29,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DumpUtils;
-import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.FgThread;
 import com.android.server.SystemService;
 
@@ -75,7 +75,7 @@
                 Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
                 new ContentObserver(handler) {
                     public void onChange(boolean selfChange) {
-                        timeZoneDetectorStrategy.handleTimeZoneDetectionChange();
+                        timeZoneDetectorStrategy.handleAutoTimeZoneDetectionChange();
                     }
                 });
 
@@ -91,8 +91,16 @@
     }
 
     @Override
+    public void suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion timeZoneSuggestion) {
+        enforceSuggestManualTimeZonePermission();
+        Objects.requireNonNull(timeZoneSuggestion);
+
+        mHandler.post(() -> mTimeZoneDetectorStrategy.suggestManualTimeZone(timeZoneSuggestion));
+    }
+
+    @Override
     public void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion timeZoneSuggestion) {
-        enforceSetTimeZonePermission();
+        enforceSuggestPhoneTimeZonePermission();
         Objects.requireNonNull(timeZoneSuggestion);
 
         mHandler.post(() -> mTimeZoneDetectorStrategy.suggestPhoneTimeZone(timeZoneSuggestion));
@@ -103,13 +111,17 @@
             @Nullable String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
 
-        mTimeZoneDetectorStrategy.dumpState(pw);
-        mTimeZoneDetectorStrategy.dumpLogs(new IndentingPrintWriter(pw, " "));
+        mTimeZoneDetectorStrategy.dumpState(pw, args);
     }
 
-    private void enforceSetTimeZonePermission() {
+    private void enforceSuggestPhoneTimeZonePermission() {
         mContext.enforceCallingPermission(
                 android.Manifest.permission.SET_TIME_ZONE, "set time zone");
     }
+
+    private void enforceSuggestManualTimeZonePermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.SET_TIME_ZONE, "set time zone");
+    }
 }
 
diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
index e24c089..5db12c7 100644
--- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
+++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java
@@ -21,8 +21,10 @@
 import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
 import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.PhoneTimeZoneSuggestion;
 import android.content.Context;
 import android.util.ArrayMap;
@@ -34,22 +36,34 @@
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.LinkedList;
 import java.util.Map;
 import java.util.Objects;
 
 /**
- * A singleton, stateful time zone detection strategy that is aware of multiple phone devices. It
- * keeps track of the most recent suggestion from each phone and it uses the best based on a scoring
- * algorithm. If several phones provide the same score then the phone with the lowest numeric ID
- * "wins". If the situation changes and it is no longer possible to be confident about the time
- * zone, phones must submit an empty suggestion in order to "withdraw" their previous suggestion.
+ * A singleton, stateful time zone detection strategy that is aware of user (manual) suggestions and
+ * suggestions from multiple phone devices. Suggestions are acted on or ignored as needed, dependent
+ * on the current "auto time zone detection" setting.
+ *
+ * <p>For automatic detection it keeps track of the most recent suggestion from each phone it uses
+ * the best suggestion based on a scoring algorithm. If several phones provide the same score then
+ * the phone with the lowest numeric ID "wins". If the situation changes and it is no longer
+ * possible to be confident about the time zone, phones must submit an empty suggestion in order to
+ * "withdraw" their previous suggestion.
  */
 public class TimeZoneDetectorStrategy {
 
     /**
      * Used by {@link TimeZoneDetectorStrategy} to interact with the surrounding service. It can be
      * faked for tests.
+     *
+     * <p>Note: Because the system properties-derived values like
+     * {@link #isAutoTimeZoneDetectionEnabled()}, {@link #isAutoTimeZoneDetectionEnabled()},
+     * {@link #getDeviceTimeZone()} can be modified independently and from different threads (and
+     * processes!), their use are prone to race conditions. That will be true until the
+     * responsibility for setting their values is moved to {@link TimeZoneDetectorStrategy}.
      */
     @VisibleForTesting
     public interface Callback {
@@ -57,7 +71,7 @@
         /**
          * Returns true if automatic time zone detection is enabled in settings.
          */
-        boolean isTimeZoneDetectionEnabled();
+        boolean isAutoTimeZoneDetectionEnabled();
 
         /**
          * Returns true if the device has had an explicit time zone set.
@@ -72,22 +86,34 @@
         /**
          * Sets the device's time zone.
          */
-        void setDeviceTimeZone(@NonNull String zoneId);
+        void setDeviceTimeZone(@NonNull String zoneId, boolean sendNetworkBroadcast);
     }
 
-    static final String LOG_TAG = "TimeZoneDetectorStrategy";
-    static final boolean DBG = false;
+    private static final String LOG_TAG = "TimeZoneDetectorStrategy";
+    private static final boolean DBG = false;
+
+    @IntDef({ ORIGIN_PHONE, ORIGIN_MANUAL })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Origin {}
+
+    /** Used when a time value originated from a telephony signal. */
+    @Origin
+    private static final int ORIGIN_PHONE = 1;
+
+    /** Used when a time value originated from a user / manual settings. */
+    @Origin
+    private static final int ORIGIN_MANUAL = 2;
 
     /**
-     * The abstract score for an empty or invalid suggestion.
+     * The abstract score for an empty or invalid phone suggestion.
      *
-     * Used to score suggestions where there is no zone.
+     * Used to score phone suggestions where there is no zone.
      */
     @VisibleForTesting
-    public static final int SCORE_NONE = 0;
+    public static final int PHONE_SCORE_NONE = 0;
 
     /**
-     * The abstract score for a low quality suggestion.
+     * The abstract score for a low quality phone suggestion.
      *
      * Used to score suggestions where:
      * The suggested zone ID is one of several possibilities, and the possibilities have different
@@ -96,10 +122,10 @@
      * You would have to be quite desperate to want to use this choice.
      */
     @VisibleForTesting
-    public static final int SCORE_LOW = 1;
+    public static final int PHONE_SCORE_LOW = 1;
 
     /**
-     * The abstract score for a medium quality suggestion.
+     * The abstract score for a medium quality phone suggestion.
      *
      * Used for:
      * The suggested zone ID is one of several possibilities but at least the possibilities have the
@@ -107,33 +133,36 @@
      * switch to DST at the wrong time and (for example) their calendar events.
      */
     @VisibleForTesting
-    public static final int SCORE_MEDIUM = 2;
+    public static final int PHONE_SCORE_MEDIUM = 2;
 
     /**
-     * The abstract score for a high quality suggestion.
+     * The abstract score for a high quality phone suggestion.
      *
      * Used for:
      * The suggestion was for one zone ID and the answer was unambiguous and likely correct given
      * the info available.
      */
     @VisibleForTesting
-    public static final int SCORE_HIGH = 3;
+    public static final int PHONE_SCORE_HIGH = 3;
 
     /**
-     * The abstract score for a highest quality suggestion.
+     * The abstract score for a highest quality phone suggestion.
      *
      * Used for:
      * Suggestions that must "win" because they constitute test or emulator zone ID.
      */
     @VisibleForTesting
-    public static final int SCORE_HIGHEST = 4;
+    public static final int PHONE_SCORE_HIGHEST = 4;
 
-    /** The threshold at which suggestions are good enough to use to set the device's time zone. */
+    /**
+     * The threshold at which phone suggestions are good enough to use to set the device's time
+     * zone.
+     */
     @VisibleForTesting
-    public static final int SCORE_USAGE_THRESHOLD = SCORE_MEDIUM;
+    public static final int PHONE_SCORE_USAGE_THRESHOLD = PHONE_SCORE_MEDIUM;
 
     /** The number of previous phone suggestions to keep for each ID (for use during debugging). */
-    private static final int KEEP_SUGGESTION_HISTORY_SIZE = 30;
+    private static final int KEEP_PHONE_SUGGESTION_HISTORY_SIZE = 30;
 
     @NonNull
     private final Callback mCallback;
@@ -146,24 +175,16 @@
     private final LocalLog mTimeZoneChangesLog = new LocalLog(30);
 
     /**
-     * A mapping from phoneId to a linked list of time zone suggestions (the head being the latest).
-     * We typically expect one or two entries in this Map: devices will have a small number
+     * A mapping from phoneId to a linked list of phone time zone suggestions (the head being the
+     * latest). We typically expect one or two entries in this Map: devices will have a small number
      * of telephony devices and phoneIds are assumed to be stable. The LinkedList associated with
-     * the ID will not exceed {@link #KEEP_SUGGESTION_HISTORY_SIZE} in size.
+     * the ID will not exceed {@link #KEEP_PHONE_SUGGESTION_HISTORY_SIZE} in size.
      */
     @GuardedBy("this")
     private ArrayMap<Integer, LinkedList<QualifiedPhoneTimeZoneSuggestion>> mSuggestionByPhoneId =
             new ArrayMap<>();
 
     /**
-     * The most recent best guess of time zone from all phones. Can be {@code null} to indicate
-     * there would be no current suggestion.
-     */
-    @GuardedBy("this")
-    @Nullable
-    private QualifiedPhoneTimeZoneSuggestion mCurrentSuggestion;
-
-    /**
      * Creates a new instance of {@link TimeZoneDetectorStrategy}.
      */
     public static TimeZoneDetectorStrategy create(Context context) {
@@ -176,56 +197,68 @@
         mCallback = Objects.requireNonNull(callback);
     }
 
+    /** Process the suggested manually- / user-entered time zone. */
+    public synchronized void suggestManualTimeZone(@NonNull ManualTimeZoneSuggestion suggestion) {
+        Objects.requireNonNull(suggestion);
+
+        String timeZoneId = suggestion.getZoneId();
+        String cause = "Manual time suggestion received: suggestion=" + suggestion;
+        setDeviceTimeZoneIfRequired(ORIGIN_MANUAL, timeZoneId, cause);
+    }
+
     /**
      * Suggests a time zone for the device, or withdraws a previous suggestion if
      * {@link PhoneTimeZoneSuggestion#getZoneId()} is {@code null}. The suggestion is scoped to a
      * specific {@link PhoneTimeZoneSuggestion#getPhoneId() phone}.
      * See {@link PhoneTimeZoneSuggestion} for an explanation of the metadata associated with a
-     * suggestion. The service uses suggestions to decide whether to modify the device's time zone
+     * suggestion. The strategy uses suggestions to decide whether to modify the device's time zone
      * setting and what to set it to.
      */
-    public synchronized void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion newSuggestion) {
+    public synchronized void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion) {
         if (DBG) {
-            Slog.d(LOG_TAG, "suggestPhoneTimeZone: newSuggestion=" + newSuggestion);
+            Slog.d(LOG_TAG, "Phone suggestion received. newSuggestion=" + suggestion);
         }
-        Objects.requireNonNull(newSuggestion);
+        Objects.requireNonNull(suggestion);
 
-        int score = scoreSuggestion(newSuggestion);
+        // Score the suggestion.
+        int score = scorePhoneSuggestion(suggestion);
         QualifiedPhoneTimeZoneSuggestion scoredSuggestion =
-                new QualifiedPhoneTimeZoneSuggestion(newSuggestion, score);
+                new QualifiedPhoneTimeZoneSuggestion(suggestion, score);
 
-        // Record the suggestion against the correct phoneId.
+        // Store the suggestion against the correct phoneId.
         LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions =
-                mSuggestionByPhoneId.get(newSuggestion.getPhoneId());
+                mSuggestionByPhoneId.get(suggestion.getPhoneId());
         if (suggestions == null) {
             suggestions = new LinkedList<>();
-            mSuggestionByPhoneId.put(newSuggestion.getPhoneId(), suggestions);
+            mSuggestionByPhoneId.put(suggestion.getPhoneId(), suggestions);
         }
         suggestions.addFirst(scoredSuggestion);
-        if (suggestions.size() > KEEP_SUGGESTION_HISTORY_SIZE) {
+        if (suggestions.size() > KEEP_PHONE_SUGGESTION_HISTORY_SIZE) {
             suggestions.removeLast();
         }
 
-        // Now run the competition between the phones' suggestions.
-        doTimeZoneDetection();
+        // Now perform auto time zone detection. The new suggestion may be used to modify the time
+        // zone setting.
+        String reason = "New phone time suggested. suggestion=" + suggestion;
+        doAutoTimeZoneDetection(reason);
     }
 
-    private static int scoreSuggestion(@NonNull PhoneTimeZoneSuggestion suggestion) {
+    private static int scorePhoneSuggestion(@NonNull PhoneTimeZoneSuggestion suggestion) {
         int score;
         if (suggestion.getZoneId() == null) {
-            score = SCORE_NONE;
+            score = PHONE_SCORE_NONE;
         } else if (suggestion.getMatchType() == MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY
                 || suggestion.getMatchType() == MATCH_TYPE_EMULATOR_ZONE_ID) {
             // Handle emulator / test cases : These suggestions should always just be used.
-            score = SCORE_HIGHEST;
+            score = PHONE_SCORE_HIGHEST;
         } else if (suggestion.getQuality() == QUALITY_SINGLE_ZONE) {
-            score = SCORE_HIGH;
+            score = PHONE_SCORE_HIGH;
         } else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET) {
             // The suggestion may be wrong, but at least the offset should be correct.
-            score = SCORE_MEDIUM;
+            score = PHONE_SCORE_MEDIUM;
         } else if (suggestion.getQuality() == QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS) {
             // The suggestion has a good chance of being wrong.
-            score = SCORE_LOW;
+            score = PHONE_SCORE_LOW;
         } else {
             throw new AssertionError();
         }
@@ -235,47 +268,46 @@
     /**
      * Finds the best available time zone suggestion from all phones. If it is high-enough quality
      * and automatic time zone detection is enabled then it will be set on the device. The outcome
-     * can be that this service becomes / remains un-opinionated and nothing is set.
+     * can be that this strategy becomes / remains un-opinionated and nothing is set.
      */
     @GuardedBy("this")
-    private void doTimeZoneDetection() {
-        QualifiedPhoneTimeZoneSuggestion bestSuggestion = findBestSuggestion();
-        boolean timeZoneDetectionEnabled = mCallback.isTimeZoneDetectionEnabled();
+    private void doAutoTimeZoneDetection(@NonNull String detectionReason) {
+        if (!mCallback.isAutoTimeZoneDetectionEnabled()) {
+            // Avoid doing unnecessary work with this (race-prone) check.
+            return;
+        }
+
+        QualifiedPhoneTimeZoneSuggestion bestPhoneSuggestion = findBestPhoneSuggestion();
 
         // Work out what to do with the best suggestion.
-        if (bestSuggestion == null) {
-            // There is no suggestion. Become un-opinionated.
+        if (bestPhoneSuggestion == null) {
+            // There is no phone suggestion available at all. Become un-opinionated.
             if (DBG) {
-                Slog.d(LOG_TAG, "doTimeZoneDetection: No good suggestion."
-                        + " bestSuggestion=null"
-                        + ", timeZoneDetectionEnabled=" + timeZoneDetectionEnabled);
+                Slog.d(LOG_TAG, "Could not determine time zone: No best phone suggestion."
+                        + " detectionReason=" + detectionReason);
             }
-            mCurrentSuggestion = null;
             return;
         }
 
         // Special case handling for uninitialized devices. This should only happen once.
-        String newZoneId = bestSuggestion.suggestion.getZoneId();
+        String newZoneId = bestPhoneSuggestion.suggestion.getZoneId();
         if (newZoneId != null && !mCallback.isDeviceTimeZoneInitialized()) {
-            Slog.i(LOG_TAG, "doTimeZoneDetection: Device has no time zone set so might set the"
-                    + " device to the best available suggestion."
-                    + " bestSuggestion=" + bestSuggestion
-                    + ", timeZoneDetectionEnabled=" + timeZoneDetectionEnabled);
-
-            mCurrentSuggestion = bestSuggestion;
-            if (timeZoneDetectionEnabled) {
-                setDeviceTimeZone(bestSuggestion.suggestion);
-            }
+            String cause = "Device has no time zone set. Attempting to set the device to the best"
+                    + " available suggestion."
+                    + " bestPhoneSuggestion=" + bestPhoneSuggestion
+                    + ", detectionReason=" + detectionReason;
+            Slog.i(LOG_TAG, cause);
+            setDeviceTimeZoneIfRequired(ORIGIN_PHONE, newZoneId, cause);
             return;
         }
 
-        boolean suggestionGoodEnough = bestSuggestion.score >= SCORE_USAGE_THRESHOLD;
+        boolean suggestionGoodEnough = bestPhoneSuggestion.score >= PHONE_SCORE_USAGE_THRESHOLD;
         if (!suggestionGoodEnough) {
             if (DBG) {
-                Slog.d(LOG_TAG, "doTimeZoneDetection: Suggestion not good enough."
-                        + " bestSuggestion=" + bestSuggestion);
+                Slog.d(LOG_TAG, "Best suggestion not good enough."
+                        + " bestPhoneSuggestion=" + bestPhoneSuggestion
+                        + ", detectionReason=" + detectionReason);
             }
-            mCurrentSuggestion = null;
             return;
         }
 
@@ -283,63 +315,84 @@
         // zone ID.
         if (newZoneId == null) {
             Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:"
-                    + " bestSuggestion=" + bestSuggestion);
-            mCurrentSuggestion = null;
+                    + " bestPhoneSuggestion=" + bestPhoneSuggestion
+                    + " detectionReason=" + detectionReason);
             return;
         }
 
-        // There is a good suggestion. Store the suggestion and set the device time zone if
-        // settings allow.
-        mCurrentSuggestion = bestSuggestion;
-
-        // Only set the device time zone if time zone detection is enabled.
-        if (!timeZoneDetectionEnabled) {
-            if (DBG) {
-                Slog.d(LOG_TAG, "doTimeZoneDetection: Not setting the time zone because time zone"
-                        + " detection is disabled."
-                        + " bestSuggestion=" + bestSuggestion);
-            }
-            return;
-        }
-        PhoneTimeZoneSuggestion suggestion = bestSuggestion.suggestion;
-        setDeviceTimeZone(suggestion);
+        String zoneId = bestPhoneSuggestion.suggestion.getZoneId();
+        String cause = "Found good suggestion."
+                + ", bestPhoneSuggestion=" + bestPhoneSuggestion
+                + ", detectionReason=" + detectionReason;
+        setDeviceTimeZoneIfRequired(ORIGIN_PHONE, zoneId, cause);
     }
 
-    private void setDeviceTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion) {
-        String currentZoneId = mCallback.getDeviceTimeZone();
-        String newZoneId = suggestion.getZoneId();
+    @GuardedBy("this")
+    private void setDeviceTimeZoneIfRequired(
+            @Origin int origin, @NonNull String newZoneId, @NonNull String cause) {
+        Objects.requireNonNull(newZoneId);
+        Objects.requireNonNull(cause);
 
-        // Paranoia: This should never happen.
-        if (newZoneId == null) {
-            Slog.w(LOG_TAG, "setDeviceTimeZone: Suggested zone is null."
-                    + " timeZoneSuggestion=" + suggestion);
-            return;
+        boolean sendNetworkBroadcast = (origin == ORIGIN_PHONE);
+        boolean isOriginAutomatic = isOriginAutomatic(origin);
+        if (isOriginAutomatic) {
+            if (!mCallback.isAutoTimeZoneDetectionEnabled()) {
+                if (DBG) {
+                    Slog.d(LOG_TAG, "Auto time zone detection is not enabled."
+                            + " origin=" + origin
+                            + ", newZoneId=" + newZoneId
+                            + ", cause=" + cause);
+                }
+                return;
+            }
+        } else {
+            if (mCallback.isAutoTimeZoneDetectionEnabled()) {
+                if (DBG) {
+                    Slog.d(LOG_TAG, "Auto time zone detection is enabled."
+                            + " origin=" + origin
+                            + ", newZoneId=" + newZoneId
+                            + ", cause=" + cause);
+                }
+                return;
+            }
         }
 
+        String currentZoneId = mCallback.getDeviceTimeZone();
+
         // Avoid unnecessary changes / intents.
         if (newZoneId.equals(currentZoneId)) {
             // No need to set the device time zone - the setting is already what we would be
             // suggesting.
             if (DBG) {
-                Slog.d(LOG_TAG, "setDeviceTimeZone: No need to change the time zone;"
+                Slog.d(LOG_TAG, "No need to change the time zone;"
                         + " device is already set to the suggested zone."
-                        + " timeZoneSuggestion=" + suggestion);
+                        + " origin=" + origin
+                        + ", newZoneId=" + newZoneId
+                        + ", cause=" + cause);
             }
             return;
         }
 
-        String msg = "Changing device time zone. currentZoneId=" + currentZoneId
-                + ", timeZoneSuggestion=" + suggestion;
+        mCallback.setDeviceTimeZone(newZoneId, sendNetworkBroadcast);
+        String msg = "Set device time zone."
+                + " origin=" + origin
+                + ", currentZoneId=" + currentZoneId
+                + ", newZoneId=" + newZoneId
+                + ", sendNetworkBroadcast" + sendNetworkBroadcast
+                + ", cause=" + cause;
         if (DBG) {
             Slog.d(LOG_TAG, msg);
         }
         mTimeZoneChangesLog.log(msg);
-        mCallback.setDeviceTimeZone(newZoneId);
+    }
+
+    private static boolean isOriginAutomatic(@Origin int origin) {
+        return origin == ORIGIN_PHONE;
     }
 
     @GuardedBy("this")
     @Nullable
-    private QualifiedPhoneTimeZoneSuggestion findBestSuggestion() {
+    private QualifiedPhoneTimeZoneSuggestion findBestPhoneSuggestion() {
         QualifiedPhoneTimeZoneSuggestion bestSuggestion = null;
 
         // Iterate over the latest QualifiedPhoneTimeZoneSuggestion objects received for each phone
@@ -376,38 +429,44 @@
     }
 
     /**
-     * Returns the current best suggestion. Not intended for general use: it is used during tests
-     * to check service behavior.
+     * Returns the current best phone suggestion. Not intended for general use: it is used during
+     * tests to check strategy behavior.
      */
     @VisibleForTesting
     @Nullable
-    public synchronized QualifiedPhoneTimeZoneSuggestion findBestSuggestionForTests() {
-        return findBestSuggestion();
+    public synchronized QualifiedPhoneTimeZoneSuggestion findBestPhoneSuggestionForTests() {
+        return findBestPhoneSuggestion();
     }
 
     /**
-     * Called when the has been a change to the automatic time zone detection setting.
+     * Called when there has been a change to the automatic time zone detection setting.
      */
     @VisibleForTesting
-    public synchronized void handleTimeZoneDetectionChange() {
+    public synchronized void handleAutoTimeZoneDetectionChange() {
         if (DBG) {
             Slog.d(LOG_TAG, "handleTimeZoneDetectionChange() called");
         }
-        if (mCallback.isTimeZoneDetectionEnabled()) {
+        if (mCallback.isAutoTimeZoneDetectionEnabled()) {
             // When the user enabled time zone detection, run the time zone detection and change the
             // device time zone if possible.
-            doTimeZoneDetection();
+            String reason = "Auto time zone detection setting enabled.";
+            doAutoTimeZoneDetection(reason);
         }
     }
 
     /**
-     * Dumps any logs held to the supplied writer.
+     * Dumps internal state such as field values.
      */
-    public synchronized void dumpLogs(IndentingPrintWriter ipw) {
-        ipw.println("TimeZoneDetectorStrategy:");
+    public synchronized void dumpState(PrintWriter pw, String[] args) {
+        pw.println("TimeZoneDetectorStrategy:");
+        pw.println("mCallback.isTimeZoneDetectionEnabled()="
+                + mCallback.isAutoTimeZoneDetectionEnabled());
+        pw.println("mCallback.isDeviceTimeZoneInitialized()="
+                + mCallback.isDeviceTimeZoneInitialized());
+        pw.println("mCallback.getDeviceTimeZone()="
+                + mCallback.getDeviceTimeZone());
 
-        ipw.increaseIndent(); // level 1
-
+        IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
         ipw.println("Time zone change log:");
         ipw.increaseIndent(); // level 2
         mTimeZoneChangesLog.dump(ipw);
@@ -427,24 +486,13 @@
         }
         ipw.decreaseIndent(); // level 2
         ipw.decreaseIndent(); // level 1
-    }
+        ipw.flush();
 
-    /**
-     * Dumps internal state such as field values.
-     */
-    public synchronized void dumpState(PrintWriter pw) {
-        pw.println("mCurrentSuggestion=" + mCurrentSuggestion);
-        pw.println("mCallback.isTimeZoneDetectionEnabled()="
-                + mCallback.isTimeZoneDetectionEnabled());
-        pw.println("mCallback.isDeviceTimeZoneInitialized()="
-                + mCallback.isDeviceTimeZoneInitialized());
-        pw.println("mCallback.getDeviceTimeZone()="
-                + mCallback.getDeviceTimeZone());
         pw.flush();
     }
 
     /**
-     * A method used to inspect service state during tests. Not intended for general use.
+     * A method used to inspect strategy state during tests. Not intended for general use.
      */
     @VisibleForTesting
     public synchronized QualifiedPhoneTimeZoneSuggestion getLatestPhoneSuggestion(int phoneId) {
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyTest.java
index f9f23c3..270436d 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyTest.java
@@ -24,18 +24,19 @@
 import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET;
 import static android.app.timezonedetector.PhoneTimeZoneSuggestion.QUALITY_SINGLE_ZONE;
 
-import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_HIGH;
-import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_HIGHEST;
-import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_LOW;
-import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_MEDIUM;
-import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_NONE;
-import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.SCORE_USAGE_THRESHOLD;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_HIGH;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_HIGHEST;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_LOW;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_MEDIUM;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_NONE;
+import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.PHONE_SCORE_USAGE_THRESHOLD;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import android.app.timezonedetector.ManualTimeZoneSuggestion;
 import android.app.timezonedetector.PhoneTimeZoneSuggestion;
 import android.app.timezonedetector.PhoneTimeZoneSuggestion.MatchType;
 import android.app.timezonedetector.PhoneTimeZoneSuggestion.Quality;
@@ -49,6 +50,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedList;
+import java.util.Objects;
 
 /**
  * White-box unit tests for {@link TimeZoneDetectorStrategy}.
@@ -64,16 +66,17 @@
     // than the previous.
     private static final SuggestionTestCase[] TEST_CASES = new SuggestionTestCase[] {
             newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
-                    QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, SCORE_LOW),
+                    QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, PHONE_SCORE_LOW),
             newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET,
-                    SCORE_MEDIUM),
+                    PHONE_SCORE_MEDIUM),
             newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET,
-                    QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, SCORE_MEDIUM),
-            newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_SINGLE_ZONE, SCORE_HIGH),
-            newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE, SCORE_HIGH),
+                    QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, PHONE_SCORE_MEDIUM),
+            newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_SINGLE_ZONE, PHONE_SCORE_HIGH),
+            newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE,
+                    PHONE_SCORE_HIGH),
             newTestCase(MATCH_TYPE_TEST_NETWORK_OFFSET_ONLY,
-                    QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, SCORE_HIGHEST),
-            newTestCase(MATCH_TYPE_EMULATOR_ZONE_ID, QUALITY_SINGLE_ZONE, SCORE_HIGHEST),
+                    QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET, PHONE_SCORE_HIGHEST),
+            newTestCase(MATCH_TYPE_EMULATOR_ZONE_ID, QUALITY_SINGLE_ZONE, PHONE_SCORE_HIGHEST),
     };
 
     private TimeZoneDetectorStrategy mTimeZoneDetectorStrategy;
@@ -87,11 +90,11 @@
     }
 
     @Test
-    public void testEmptySuggestions() {
+    public void testEmptyPhoneSuggestions() {
         PhoneTimeZoneSuggestion phone1TimeZoneSuggestion = createEmptyPhone1Suggestion();
         PhoneTimeZoneSuggestion phone2TimeZoneSuggestion = createEmptyPhone2Suggestion();
         Script script = new Script()
-                .initializeTimeZoneDetectionEnabled(true)
+                .initializeAutoTimeZoneDetection(true)
                 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID);
 
         script.suggestPhoneTimeZone(phone1TimeZoneSuggestion)
@@ -99,38 +102,38 @@
 
         // Assert internal service state.
         QualifiedPhoneTimeZoneSuggestion expectedPhone1ScoredSuggestion =
-                new QualifiedPhoneTimeZoneSuggestion(phone1TimeZoneSuggestion, SCORE_NONE);
+                new QualifiedPhoneTimeZoneSuggestion(phone1TimeZoneSuggestion, PHONE_SCORE_NONE);
         assertEquals(expectedPhone1ScoredSuggestion,
                 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
         assertNull(mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
         assertEquals(expectedPhone1ScoredSuggestion,
-                mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
 
         script.suggestPhoneTimeZone(phone2TimeZoneSuggestion)
                 .verifyTimeZoneNotSet();
 
         // Assert internal service state.
         QualifiedPhoneTimeZoneSuggestion expectedPhone2ScoredSuggestion =
-                new QualifiedPhoneTimeZoneSuggestion(phone2TimeZoneSuggestion, SCORE_NONE);
+                new QualifiedPhoneTimeZoneSuggestion(phone2TimeZoneSuggestion, PHONE_SCORE_NONE);
         assertEquals(expectedPhone1ScoredSuggestion,
                 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
         assertEquals(expectedPhone2ScoredSuggestion,
                 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
         // Phone 1 should always beat phone 2, all other things being equal.
         assertEquals(expectedPhone1ScoredSuggestion,
-                mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
     }
 
     @Test
-    public void testFirstPlausibleSuggestionAcceptedWhenTimeZoneUninitialized() {
+    public void testFirstPlausiblePhoneSuggestionAcceptedWhenTimeZoneUninitialized() {
         SuggestionTestCase testCase = newTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY,
-                QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, SCORE_LOW);
+                QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, PHONE_SCORE_LOW);
         PhoneTimeZoneSuggestion lowQualitySuggestion =
                 testCase.createSuggestion(PHONE1_ID, "America/New_York");
 
         // The device time zone setting is left uninitialized.
         Script script = new Script()
-                .initializeTimeZoneDetectionEnabled(true);
+                .initializeAutoTimeZoneDetection(true);
 
         // The very first suggestion will be taken.
         script.suggestPhoneTimeZone(lowQualitySuggestion)
@@ -142,7 +145,7 @@
         assertEquals(expectedScoredSuggestion,
                 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
         assertEquals(expectedScoredSuggestion,
-                mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
 
         // Another low quality suggestion will be ignored now that the setting is initialized.
         PhoneTimeZoneSuggestion lowQualitySuggestion2 =
@@ -156,7 +159,7 @@
         assertEquals(expectedScoredSuggestion2,
                 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
         assertEquals(expectedScoredSuggestion2,
-                mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
     }
 
     /**
@@ -164,12 +167,12 @@
      * the strategy is "opinionated".
      */
     @Test
-    public void testTogglingTimeZoneDetection() {
+    public void testTogglingAutoTimeZoneDetection() {
         Script script = new Script();
 
         for (SuggestionTestCase testCase : TEST_CASES) {
             // Start with the device in a known state.
-            script.initializeTimeZoneDetectionEnabled(false)
+            script.initializeAutoTimeZoneDetection(false)
                     .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID);
 
             PhoneTimeZoneSuggestion suggestion =
@@ -186,14 +189,14 @@
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
             assertEquals(expectedScoredSuggestion,
-                    mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                    mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
 
             // Toggling the time zone setting on should cause the device setting to be set.
-            script.timeZoneDetectionEnabled(true);
+            script.autoTimeZoneDetectionEnabled(true);
 
             // When time zone detection is already enabled the suggestion (if it scores highly
             // enough) should be set immediately.
-            if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
+            if (testCase.expectedScore >= PHONE_SCORE_USAGE_THRESHOLD) {
                 script.verifyTimeZoneSetAndReset(suggestion);
             } else {
                 script.verifyTimeZoneNotSet();
@@ -203,24 +206,24 @@
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
             assertEquals(expectedScoredSuggestion,
-                    mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                    mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
 
             // Toggling the time zone setting should off should do nothing.
-            script.timeZoneDetectionEnabled(false)
+            script.autoTimeZoneDetectionEnabled(false)
                     .verifyTimeZoneNotSet();
 
             // Assert internal service state.
             assertEquals(expectedScoredSuggestion,
                     mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
             assertEquals(expectedScoredSuggestion,
-                    mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                    mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
         }
     }
 
     @Test
-    public void testSuggestionsSinglePhone() {
+    public void testPhoneSuggestionsSinglePhone() {
         Script script = new Script()
-                .initializeTimeZoneDetectionEnabled(true)
+                .initializeAutoTimeZoneDetection(true)
                 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID);
 
         for (SuggestionTestCase testCase : TEST_CASES) {
@@ -254,7 +257,7 @@
                 new QualifiedPhoneTimeZoneSuggestion(zonePhone1Suggestion, testCase.expectedScore);
 
         script.suggestPhoneTimeZone(zonePhone1Suggestion);
-        if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
+        if (testCase.expectedScore >= PHONE_SCORE_USAGE_THRESHOLD) {
             script.verifyTimeZoneSetAndReset(zonePhone1Suggestion);
         } else {
             script.verifyTimeZoneNotSet();
@@ -264,7 +267,7 @@
         assertEquals(expectedZonePhone1ScoredSuggestion,
                 mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE1_ID));
         assertEquals(expectedZonePhone1ScoredSuggestion,
-                mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
     }
 
     /**
@@ -278,12 +281,12 @@
         PhoneTimeZoneSuggestion emptyPhone1Suggestion = createEmptyPhone1Suggestion();
         PhoneTimeZoneSuggestion emptyPhone2Suggestion = createEmptyPhone2Suggestion();
         QualifiedPhoneTimeZoneSuggestion expectedEmptyPhone1ScoredSuggestion =
-                new QualifiedPhoneTimeZoneSuggestion(emptyPhone1Suggestion, SCORE_NONE);
+                new QualifiedPhoneTimeZoneSuggestion(emptyPhone1Suggestion, PHONE_SCORE_NONE);
         QualifiedPhoneTimeZoneSuggestion expectedEmptyPhone2ScoredSuggestion =
-                new QualifiedPhoneTimeZoneSuggestion(emptyPhone2Suggestion, SCORE_NONE);
+                new QualifiedPhoneTimeZoneSuggestion(emptyPhone2Suggestion, PHONE_SCORE_NONE);
 
         Script script = new Script()
-                .initializeTimeZoneDetectionEnabled(true)
+                .initializeAutoTimeZoneDetection(true)
                 .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
                 // Initialize the latest suggestions as empty so we don't need to worry about nulls
                 // below for the first loop.
@@ -305,7 +308,7 @@
 
             // Start the test by making a suggestion for phone 1.
             script.suggestPhoneTimeZone(zonePhone1Suggestion);
-            if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
+            if (testCase.expectedScore >= PHONE_SCORE_USAGE_THRESHOLD) {
                 script.verifyTimeZoneSetAndReset(zonePhone1Suggestion);
             } else {
                 script.verifyTimeZoneNotSet();
@@ -317,7 +320,7 @@
             assertEquals(expectedEmptyPhone2ScoredSuggestion,
                     mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
             assertEquals(expectedZonePhone1ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                    mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
 
             // Phone 2 then makes an alternative suggestion with an identical score. Phone 1's
             // suggestion should still "win" if it is above the required threshold.
@@ -331,13 +334,13 @@
                     mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
             // Phone 1 should always beat phone 2, all other things being equal.
             assertEquals(expectedZonePhone1ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                    mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
 
             // Withdrawing phone 1's suggestion should leave phone 2 as the new winner. Since the
             // zoneId is different, the time zone setting should be updated if the score is high
             // enough.
             script.suggestPhoneTimeZone(emptyPhone1Suggestion);
-            if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
+            if (testCase.expectedScore >= PHONE_SCORE_USAGE_THRESHOLD) {
                 script.verifyTimeZoneSetAndReset(zonePhone2Suggestion);
             } else {
                 script.verifyTimeZoneNotSet();
@@ -349,7 +352,7 @@
             assertEquals(expectedZonePhone2ScoredSuggestion,
                     mTimeZoneDetectorStrategy.getLatestPhoneSuggestion(PHONE2_ID));
             assertEquals(expectedZonePhone2ScoredSuggestion,
-                    mTimeZoneDetectorStrategy.findBestSuggestionForTests());
+                    mTimeZoneDetectorStrategy.findBestPhoneSuggestionForTests());
 
             // Reset the state for the next loop.
             script.suggestPhoneTimeZone(emptyPhone2Suggestion)
@@ -369,10 +372,11 @@
     @Test
     public void testTimeZoneDetectorStrategyDoesNotAssumeCurrentSetting() {
         Script script = new Script()
-                .initializeTimeZoneDetectionEnabled(true);
+                .initializeAutoTimeZoneDetection(true);
 
         SuggestionTestCase testCase =
-                newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE, SCORE_HIGH);
+                newTestCase(MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE,
+                        PHONE_SCORE_HIGH);
         PhoneTimeZoneSuggestion losAngelesSuggestion =
                 testCase.createSuggestion(PHONE1_ID, "America/Los_Angeles");
         PhoneTimeZoneSuggestion newYorkSuggestion =
@@ -387,21 +391,49 @@
 
         // Toggling time zone detection should set the device time zone only if the current setting
         // value is different from the most recent phone suggestion.
-        script.timeZoneDetectionEnabled(false)
+        script.autoTimeZoneDetectionEnabled(false)
                 .verifyTimeZoneNotSet()
-                .timeZoneDetectionEnabled(true)
+                .autoTimeZoneDetectionEnabled(true)
                 .verifyTimeZoneNotSet();
 
         // Simulate a user turning auto detection off, a new suggestion being made while auto
         // detection is off, and the user turning it on again.
-        script.timeZoneDetectionEnabled(false)
+        script.autoTimeZoneDetectionEnabled(false)
                 .suggestPhoneTimeZone(newYorkSuggestion)
                 .verifyTimeZoneNotSet();
         // Latest suggestion should be used.
-        script.timeZoneDetectionEnabled(true)
+        script.autoTimeZoneDetectionEnabled(true)
                 .verifyTimeZoneSetAndReset(newYorkSuggestion);
     }
 
+    @Test
+    public void testManualSuggestion_autoTimeZoneDetectionEnabled() {
+        Script script = new Script()
+                .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
+                .initializeAutoTimeZoneDetection(true);
+
+        // Auto time zone detection is enabled so the manual suggestion should be ignored.
+        script.suggestManualTimeZone(createManualSuggestion("Europe/Paris"))
+            .verifyTimeZoneNotSet();
+    }
+
+
+    @Test
+    public void testManualSuggestion_autoTimeZoneDetectionDisabled() {
+        Script script = new Script()
+                .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID)
+                .initializeAutoTimeZoneDetection(false);
+
+        // Auto time zone detection is disabled so the manual suggestion should be used.
+        ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris");
+        script.suggestManualTimeZone(manualSuggestion)
+            .verifyTimeZoneSetAndReset(manualSuggestion);
+    }
+
+    private ManualTimeZoneSuggestion createManualSuggestion(String zoneId) {
+        return new ManualTimeZoneSuggestion(zoneId);
+    }
+
     private static PhoneTimeZoneSuggestion createEmptyPhone1Suggestion() {
         return new PhoneTimeZoneSuggestion.Builder(PHONE1_ID).build();
     }
@@ -410,55 +442,86 @@
         return new PhoneTimeZoneSuggestion.Builder(PHONE2_ID).build();
     }
 
-    class FakeTimeZoneDetectorStrategyCallback implements TimeZoneDetectorStrategy.Callback {
+    static class FakeTimeZoneDetectorStrategyCallback implements TimeZoneDetectorStrategy.Callback {
 
-        private boolean mTimeZoneDetectionEnabled;
-        private TestState<String> mTimeZoneId = new TestState<>();
+        private boolean mAutoTimeZoneDetectionEnabled;
+        private TestState<TimeZoneChange> mTimeZoneChanges = new TestState<>();
+        private String mTimeZoneId;
 
         @Override
-        public boolean isTimeZoneDetectionEnabled() {
-            return mTimeZoneDetectionEnabled;
+        public boolean isAutoTimeZoneDetectionEnabled() {
+            return mAutoTimeZoneDetectionEnabled;
         }
 
         @Override
         public boolean isDeviceTimeZoneInitialized() {
-            return mTimeZoneId.getLatest() != null;
+            return mTimeZoneId != null;
         }
 
         @Override
         public String getDeviceTimeZone() {
-            return mTimeZoneId.getLatest();
+            return mTimeZoneId;
         }
 
         @Override
-        public void setDeviceTimeZone(String zoneId) {
-            mTimeZoneId.set(zoneId);
+        public void setDeviceTimeZone(String zoneId, boolean withNetworkBroadcast) {
+            mTimeZoneId = zoneId;
+            mTimeZoneChanges.set(new TimeZoneChange(zoneId, withNetworkBroadcast));
         }
 
-        void initializeTimeZoneDetectionEnabled(boolean enabled) {
-            mTimeZoneDetectionEnabled = enabled;
+        void initializeAutoTimeZoneDetection(boolean enabled) {
+            mAutoTimeZoneDetectionEnabled = enabled;
         }
 
         void initializeTimeZone(String zoneId) {
-            mTimeZoneId.init(zoneId);
+            mTimeZoneId = zoneId;
         }
 
-        void setTimeZoneDetectionEnabled(boolean enabled) {
-            mTimeZoneDetectionEnabled = enabled;
+        void setAutoTimeZoneDetectionEnabled(boolean enabled) {
+            mAutoTimeZoneDetectionEnabled = enabled;
         }
 
         void assertTimeZoneNotSet() {
-            mTimeZoneId.assertHasNotBeenSet();
+            mTimeZoneChanges.assertHasNotBeenSet();
         }
 
-        void assertTimeZoneSet(String timeZoneId) {
-            mTimeZoneId.assertHasBeenSet();
-            mTimeZoneId.assertChangeCount(1);
-            mTimeZoneId.assertLatestEquals(timeZoneId);
+        void assertTimeZoneSet(String timeZoneId, boolean withNetworkBroadcast) {
+            mTimeZoneChanges.assertHasBeenSet();
+            mTimeZoneChanges.assertChangeCount(1);
+            TimeZoneChange expectedChange = new TimeZoneChange(timeZoneId, withNetworkBroadcast);
+            mTimeZoneChanges.assertLatestEquals(expectedChange);
         }
 
         void commitAllChanges() {
-            mTimeZoneId.commitLatest();
+            mTimeZoneChanges.commitLatest();
+        }
+    }
+
+    private static class TimeZoneChange {
+        private final String mTimeZoneId;
+        private final boolean mWithNetworkBroadcast;
+
+        private TimeZoneChange(String timeZoneId, boolean withNetworkBroadcast) {
+            mTimeZoneId = timeZoneId;
+            mWithNetworkBroadcast = withNetworkBroadcast;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            TimeZoneChange that = (TimeZoneChange) o;
+            return mWithNetworkBroadcast == that.mWithNetworkBroadcast
+                    && mTimeZoneId.equals(that.mTimeZoneId);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mTimeZoneId, mWithNetworkBroadcast);
         }
     }
 
@@ -517,8 +580,8 @@
      */
     private class Script {
 
-        Script initializeTimeZoneDetectionEnabled(boolean enabled) {
-            mFakeTimeZoneDetectorStrategyCallback.initializeTimeZoneDetectionEnabled(enabled);
+        Script initializeAutoTimeZoneDetection(boolean enabled) {
+            mFakeTimeZoneDetectorStrategyCallback.initializeAutoTimeZoneDetection(enabled);
             return this;
         }
 
@@ -527,24 +590,21 @@
             return this;
         }
 
-        Script timeZoneDetectionEnabled(boolean enabled) {
-            mFakeTimeZoneDetectorStrategyCallback.setTimeZoneDetectionEnabled(enabled);
-            mTimeZoneDetectorStrategy.handleTimeZoneDetectionChange();
+        Script autoTimeZoneDetectionEnabled(boolean enabled) {
+            mFakeTimeZoneDetectorStrategyCallback.setAutoTimeZoneDetectionEnabled(enabled);
+            mTimeZoneDetectorStrategy.handleAutoTimeZoneDetectionChange();
             return this;
         }
 
-        /** Simulates the time zone detection service receiving a phone-originated suggestion. */
+        /** Simulates the time zone detection strategy receiving a phone-originated suggestion. */
         Script suggestPhoneTimeZone(PhoneTimeZoneSuggestion phoneTimeZoneSuggestion) {
             mTimeZoneDetectorStrategy.suggestPhoneTimeZone(phoneTimeZoneSuggestion);
             return this;
         }
 
-        /** Simulates the user manually setting the time zone. */
-        Script manuallySetTimeZone(String timeZoneId) {
-            // Assert the test code is correct to call this method.
-            assertFalse(mFakeTimeZoneDetectorStrategyCallback.isTimeZoneDetectionEnabled());
-
-            mFakeTimeZoneDetectorStrategyCallback.initializeTimeZone(timeZoneId);
+        /** Simulates the time zone detection strategy receiving a user-originated suggestion. */
+        Script suggestManualTimeZone(ManualTimeZoneSuggestion manualTimeZoneSuggestion) {
+            mTimeZoneDetectorStrategy.suggestManualTimeZone(manualTimeZoneSuggestion);
             return this;
         }
 
@@ -553,8 +613,22 @@
             return this;
         }
 
-        Script verifyTimeZoneSetAndReset(PhoneTimeZoneSuggestion timeZoneSuggestion) {
-            mFakeTimeZoneDetectorStrategyCallback.assertTimeZoneSet(timeZoneSuggestion.getZoneId());
+        Script verifyTimeZoneSetAndReset(PhoneTimeZoneSuggestion suggestion) {
+            // Phone suggestions should cause a TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE
+            // broadcast.
+            boolean withNetworkBroadcast = true;
+            mFakeTimeZoneDetectorStrategyCallback.assertTimeZoneSet(
+                    suggestion.getZoneId(), withNetworkBroadcast);
+            mFakeTimeZoneDetectorStrategyCallback.commitAllChanges();
+            return this;
+        }
+
+        Script verifyTimeZoneSetAndReset(ManualTimeZoneSuggestion suggestion) {
+            // Manual suggestions should not cause a TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE
+            // broadcast.
+            boolean withNetworkBroadcast = false;
+            mFakeTimeZoneDetectorStrategyCallback.assertTimeZoneSet(
+                    suggestion.getZoneId(), withNetworkBroadcast);
             mFakeTimeZoneDetectorStrategyCallback.commitAllChanges();
             return this;
         }