From 8164e67726c6ae438c85a1c60cced8c1e3c15f40 Mon Sep 17 00:00:00 2001 From: Neil Fuller Date: Tue, 28 Jul 2020 11:25:17 +0100 Subject: Change TimeZoneDetectorStrategyImpl to allow geo Change TimeZoneDetectorStrategyImpl to allow a new "geo only" mode. The strategy can now exist in one of two modes: 1) Telephony signals only 2) Geolocation signals only They may later joined by a third "hybrid" mode where we can experiment with how to blend the two (if that seems like it will provide better results), or perform a fallback from one to the other. Note that nothing is actually making Geolocation-based suggestions yet. The shell command line can be used to simulate geolocation suggestions in the meantime. This commit doesn't contain real settings logic: the settings storage and logic will be added in a follow-up commit. Bug: 149014708 Test: atest com.android.server.timezonedetector Test: atest core/tests/coretests/src/android/app/timezonedetector/ Change-Id: I83f8f64d40255567c035a67741b18ecaf6f0561d --- .../app/timezonedetector/TimeZoneCapabilities.java | 33 +- .../timezonedetector/TimeZoneConfiguration.java | 35 +- .../timezonedetector/TimeZoneCapabilitiesTest.java | 20 + .../TimeZoneConfigurationTest.java | 49 +- .../TimeZoneDetectorCallbackImpl.java | 20 +- .../timezonedetector/TimeZoneDetectorInternal.java | 3 +- .../TimeZoneDetectorInternalImpl.java | 1 + .../timezonedetector/TimeZoneDetectorService.java | 7 +- .../timezonedetector/TimeZoneDetectorStrategy.java | 6 +- .../TimeZoneDetectorStrategyImpl.java | 134 +++++- .../FakeTimeZoneDetectorStrategy.java | 10 +- .../TimeZoneDetectorInternalImplTest.java | 10 + .../TimeZoneDetectorServiceTest.java | 41 +- .../TimeZoneDetectorStrategyImplTest.java | 512 +++++++++++++++++---- 14 files changed, 754 insertions(+), 127 deletions(-) diff --git a/core/java/android/app/timezonedetector/TimeZoneCapabilities.java b/core/java/android/app/timezonedetector/TimeZoneCapabilities.java index 236b0064763e..cc0af3f97e49 100644 --- a/core/java/android/app/timezonedetector/TimeZoneCapabilities.java +++ b/core/java/android/app/timezonedetector/TimeZoneCapabilities.java @@ -91,11 +91,13 @@ public final class TimeZoneCapabilities implements Parcelable { private final @UserIdInt int mUserId; private final @CapabilityState int mConfigureAutoDetectionEnabled; + private final @CapabilityState int mConfigureGeoDetectionEnabled; private final @CapabilityState int mSuggestManualTimeZone; private TimeZoneCapabilities(@NonNull Builder builder) { this.mUserId = builder.mUserId; this.mConfigureAutoDetectionEnabled = builder.mConfigureAutoDetectionEnabled; + this.mConfigureGeoDetectionEnabled = builder.mConfigureGeoDetectionEnabled; this.mSuggestManualTimeZone = builder.mSuggestManualTimeZone; } @@ -103,6 +105,7 @@ public final class TimeZoneCapabilities implements Parcelable { private static TimeZoneCapabilities createFromParcel(Parcel in) { return new TimeZoneCapabilities.Builder(in.readInt()) .setConfigureAutoDetectionEnabled(in.readInt()) + .setConfigureGeoDetectionEnabled(in.readInt()) .setSuggestManualTimeZone(in.readInt()) .build(); } @@ -111,6 +114,7 @@ public final class TimeZoneCapabilities implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mUserId); dest.writeInt(mConfigureAutoDetectionEnabled); + dest.writeInt(mConfigureGeoDetectionEnabled); dest.writeInt(mSuggestManualTimeZone); } @@ -120,8 +124,8 @@ public final class TimeZoneCapabilities implements Parcelable { } /** - * Returns the user's capability state for controlling automatic time zone detection via - * {@link TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)} and {@link + * Returns the user's capability state for controlling whether automatic time zone detection is + * enabled via {@link TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)} and {@link * TimeZoneConfiguration#isAutoDetectionEnabled()}. */ @CapabilityState @@ -129,6 +133,16 @@ public final class TimeZoneCapabilities implements Parcelable { return mConfigureAutoDetectionEnabled; } + /** + * Returns the user's capability state for controlling whether geolocation can be used to detect + * time zone via {@link TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)} and {@link + * TimeZoneConfiguration#isGeoDetectionEnabled()}. + */ + @CapabilityState + public int getConfigureGeoDetectionEnabled() { + return mConfigureGeoDetectionEnabled; + } + /** * Returns the user's capability state for manually setting the time zone on a device via * {@link TimeZoneDetector#suggestManualTimeZone(ManualTimeZoneSuggestion)}. @@ -157,12 +171,16 @@ public final class TimeZoneCapabilities implements Parcelable { TimeZoneCapabilities that = (TimeZoneCapabilities) o; return mUserId == that.mUserId && mConfigureAutoDetectionEnabled == that.mConfigureAutoDetectionEnabled + && mConfigureGeoDetectionEnabled == that.mConfigureGeoDetectionEnabled && mSuggestManualTimeZone == that.mSuggestManualTimeZone; } @Override public int hashCode() { - return Objects.hash(mUserId, mConfigureAutoDetectionEnabled, mSuggestManualTimeZone); + return Objects.hash(mUserId, + mConfigureAutoDetectionEnabled, + mConfigureGeoDetectionEnabled, + mSuggestManualTimeZone); } @Override @@ -170,6 +188,7 @@ public final class TimeZoneCapabilities implements Parcelable { return "TimeZoneDetectorCapabilities{" + "mUserId=" + mUserId + ", mConfigureAutomaticDetectionEnabled=" + mConfigureAutoDetectionEnabled + + ", mConfigureGeoDetectionEnabled=" + mConfigureGeoDetectionEnabled + ", mSuggestManualTimeZone=" + mSuggestManualTimeZone + '}'; } @@ -179,6 +198,7 @@ public final class TimeZoneCapabilities implements Parcelable { private final @UserIdInt int mUserId; private @CapabilityState int mConfigureAutoDetectionEnabled; + private @CapabilityState int mConfigureGeoDetectionEnabled; private @CapabilityState int mSuggestManualTimeZone; /** @@ -194,6 +214,12 @@ public final class TimeZoneCapabilities implements Parcelable { return this; } + /** Sets the state for the geolocation time zone detection enabled config. */ + public Builder setConfigureGeoDetectionEnabled(@CapabilityState int value) { + this.mConfigureGeoDetectionEnabled = value; + return this; + } + /** Sets the state for the suggestManualTimeZone action. */ public Builder setSuggestManualTimeZone(@CapabilityState int value) { this.mSuggestManualTimeZone = value; @@ -204,6 +230,7 @@ public final class TimeZoneCapabilities implements Parcelable { @NonNull public TimeZoneCapabilities build() { verifyCapabilitySet(mConfigureAutoDetectionEnabled, "configureAutoDetectionEnabled"); + verifyCapabilitySet(mConfigureGeoDetectionEnabled, "configureGeoDetectionEnabled"); verifyCapabilitySet(mSuggestManualTimeZone, "suggestManualTimeZone"); return new TimeZoneCapabilities(this); } diff --git a/core/java/android/app/timezonedetector/TimeZoneConfiguration.java b/core/java/android/app/timezonedetector/TimeZoneConfiguration.java index 047d3493d5dd..6f84ee22a985 100644 --- a/core/java/android/app/timezonedetector/TimeZoneConfiguration.java +++ b/core/java/android/app/timezonedetector/TimeZoneConfiguration.java @@ -67,6 +67,10 @@ public final class TimeZoneConfiguration implements Parcelable { @Property public static final String PROPERTY_AUTO_DETECTION_ENABLED = "autoDetectionEnabled"; + /** See {@link TimeZoneConfiguration#isGeoDetectionEnabled()} for details. */ + @Property + public static final String PROPERTY_GEO_DETECTION_ENABLED = "geoDetectionEnabled"; + private final Bundle mBundle; private TimeZoneConfiguration(Builder builder) { @@ -86,7 +90,8 @@ public final class TimeZoneConfiguration implements Parcelable { /** Returns {@code true} if all known properties are set. */ public boolean isComplete() { - return hasProperty(PROPERTY_AUTO_DETECTION_ENABLED); + return hasProperty(PROPERTY_AUTO_DETECTION_ENABLED) + && hasProperty(PROPERTY_GEO_DETECTION_ENABLED); } /** Returns true if the specified property is set. */ @@ -108,6 +113,28 @@ public final class TimeZoneConfiguration implements Parcelable { return mBundle.getBoolean(PROPERTY_AUTO_DETECTION_ENABLED); } + /** + * Returns the value of the {@link #PROPERTY_GEO_DETECTION_ENABLED} property. This + * controls whether a device can use location to determine time zone. Only used when + * {@link #isAutoDetectionEnabled()} is true. + * + * @throws IllegalStateException if the field has not been set + */ + public boolean isGeoDetectionEnabled() { + if (!mBundle.containsKey(PROPERTY_GEO_DETECTION_ENABLED)) { + throw new IllegalStateException(PROPERTY_GEO_DETECTION_ENABLED + " is not set"); + } + return mBundle.getBoolean(PROPERTY_GEO_DETECTION_ENABLED); + } + + /** + * Convenience method to merge this with another. The argument configuration properties have + * precedence. + */ + public TimeZoneConfiguration with(TimeZoneConfiguration other) { + return new Builder(this).mergeProperties(other).build(); + } + @Override public int describeContents() { return 0; @@ -174,6 +201,12 @@ public final class TimeZoneConfiguration implements Parcelable { return this; } + /** Sets the desired state of the geolocation time zone detection enabled property. */ + public Builder setGeoDetectionEnabled(boolean enabled) { + this.mBundle.putBoolean(PROPERTY_GEO_DETECTION_ENABLED, enabled); + return this; + } + /** Returns the {@link TimeZoneConfiguration}. */ @NonNull public TimeZoneConfiguration build() { diff --git a/core/tests/coretests/src/android/app/timezonedetector/TimeZoneCapabilitiesTest.java b/core/tests/coretests/src/android/app/timezonedetector/TimeZoneCapabilitiesTest.java index 8a36f1dc057b..72391f4d7dec 100644 --- a/core/tests/coretests/src/android/app/timezonedetector/TimeZoneCapabilitiesTest.java +++ b/core/tests/coretests/src/android/app/timezonedetector/TimeZoneCapabilitiesTest.java @@ -33,9 +33,11 @@ public class TimeZoneCapabilitiesTest { public void testEquals() { TimeZoneCapabilities.Builder builder1 = new TimeZoneCapabilities.Builder(ARBITRARY_USER_ID) .setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED) + .setConfigureGeoDetectionEnabled(CAPABILITY_POSSESSED) .setSuggestManualTimeZone(CAPABILITY_POSSESSED); TimeZoneCapabilities.Builder builder2 = new TimeZoneCapabilities.Builder(ARBITRARY_USER_ID) .setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED) + .setConfigureGeoDetectionEnabled(CAPABILITY_POSSESSED) .setSuggestManualTimeZone(CAPABILITY_POSSESSED); { TimeZoneCapabilities one = builder1.build(); @@ -57,6 +59,20 @@ public class TimeZoneCapabilitiesTest { assertEquals(one, two); } + builder2.setConfigureGeoDetectionEnabled(CAPABILITY_NOT_ALLOWED); + { + TimeZoneCapabilities one = builder1.build(); + TimeZoneCapabilities two = builder2.build(); + assertNotEquals(one, two); + } + + builder1.setConfigureGeoDetectionEnabled(CAPABILITY_NOT_ALLOWED); + { + TimeZoneCapabilities one = builder1.build(); + TimeZoneCapabilities two = builder2.build(); + assertEquals(one, two); + } + builder2.setSuggestManualTimeZone(CAPABILITY_NOT_ALLOWED); { TimeZoneCapabilities one = builder1.build(); @@ -76,12 +92,16 @@ public class TimeZoneCapabilitiesTest { public void testParcelable() { TimeZoneCapabilities.Builder builder = new TimeZoneCapabilities.Builder(ARBITRARY_USER_ID) .setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED) + .setConfigureGeoDetectionEnabled(CAPABILITY_POSSESSED) .setSuggestManualTimeZone(CAPABILITY_POSSESSED); assertRoundTripParcelable(builder.build()); builder.setConfigureAutoDetectionEnabled(CAPABILITY_NOT_ALLOWED); assertRoundTripParcelable(builder.build()); + builder.setConfigureGeoDetectionEnabled(CAPABILITY_NOT_ALLOWED); + assertRoundTripParcelable(builder.build()); + builder.setSuggestManualTimeZone(CAPABILITY_NOT_ALLOWED); assertRoundTripParcelable(builder.build()); } diff --git a/core/tests/coretests/src/android/app/timezonedetector/TimeZoneConfigurationTest.java b/core/tests/coretests/src/android/app/timezonedetector/TimeZoneConfigurationTest.java index ac7e9c437b63..00dc73ed269f 100644 --- a/core/tests/coretests/src/android/app/timezonedetector/TimeZoneConfigurationTest.java +++ b/core/tests/coretests/src/android/app/timezonedetector/TimeZoneConfigurationTest.java @@ -29,8 +29,9 @@ public class TimeZoneConfigurationTest { @Test public void testBuilder_copyConstructor() { - TimeZoneConfiguration.Builder builder1 = - new TimeZoneConfiguration.Builder().setAutoDetectionEnabled(true); + TimeZoneConfiguration.Builder builder1 = new TimeZoneConfiguration.Builder() + .setAutoDetectionEnabled(true) + .setGeoDetectionEnabled(true); TimeZoneConfiguration configuration1 = builder1.build(); TimeZoneConfiguration configuration2 = @@ -41,16 +42,15 @@ public class TimeZoneConfigurationTest { @Test public void testIsComplete() { - TimeZoneConfiguration incompleteConfiguration = - new TimeZoneConfiguration.Builder() - .build(); - assertFalse(incompleteConfiguration.isComplete()); + TimeZoneConfiguration.Builder builder = + new TimeZoneConfiguration.Builder(); + assertFalse(builder.build().isComplete()); - TimeZoneConfiguration completeConfiguration = - new TimeZoneConfiguration.Builder() - .setAutoDetectionEnabled(true) - .build(); - assertTrue(completeConfiguration.isComplete()); + builder.setAutoDetectionEnabled(true); + assertFalse(builder.build().isComplete()); + + builder.setGeoDetectionEnabled(true); + assertTrue(builder.build().isComplete()); } @Test @@ -122,6 +122,27 @@ public class TimeZoneConfigurationTest { TimeZoneConfiguration two = builder2.build(); assertEquals(one, two); } + + builder1.setGeoDetectionEnabled(true); + { + TimeZoneConfiguration one = builder1.build(); + TimeZoneConfiguration two = builder2.build(); + assertNotEquals(one, two); + } + + builder2.setGeoDetectionEnabled(false); + { + TimeZoneConfiguration one = builder1.build(); + TimeZoneConfiguration two = builder2.build(); + assertNotEquals(one, two); + } + + builder1.setGeoDetectionEnabled(false); + { + TimeZoneConfiguration one = builder1.build(); + TimeZoneConfiguration two = builder2.build(); + assertEquals(one, two); + } } @Test @@ -135,5 +156,11 @@ public class TimeZoneConfigurationTest { builder.setAutoDetectionEnabled(false); assertRoundTripParcelable(builder.build()); + + builder.setGeoDetectionEnabled(false); + assertRoundTripParcelable(builder.build()); + + builder.setGeoDetectionEnabled(true); + assertRoundTripParcelable(builder.build()); } } diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java index c2b0d1067e38..0ca36e0fc258 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java @@ -72,6 +72,10 @@ public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrat builder.setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED); } + // TODO(b/149014708) Replace this with real logic when the settings storage is fully + // implemented. + builder.setConfigureGeoDetectionEnabled(CAPABILITY_NOT_SUPPORTED); + // The ability to make manual time zone suggestions can also be restricted by policy. With // the current logic above, this could lead to a situation where a device hardware does not // support auto detection, the device has been forced into "auto" mode by an admin and the @@ -90,6 +94,7 @@ public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrat public TimeZoneConfiguration getConfiguration(@UserIdInt int userId) { return new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(isAutoDetectionEnabled()) + .setGeoDetectionEnabled(isGeoDetectionEnabled()) .build(); } @@ -105,8 +110,11 @@ public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrat // detection: if we wrote it down then we'd set the default explicitly. That might influence // what happens on later releases that do support auto detection on the same hardware. if (isAutoDetectionSupported()) { - final int value = configuration.isAutoDetectionEnabled() ? 1 : 0; - Settings.Global.putInt(mCr, Settings.Global.AUTO_TIME_ZONE, value); + final int autoEnabledValue = configuration.isAutoDetectionEnabled() ? 1 : 0; + Settings.Global.putInt(mCr, Settings.Global.AUTO_TIME_ZONE, autoEnabledValue); + + final boolean geoTzDetectionEnabledValue = configuration.isGeoDetectionEnabled(); + // TODO(b/149014708) Write this down to user-scoped settings once implemented. } } @@ -125,6 +133,14 @@ public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrat return false; } + @Override + public boolean isGeoDetectionEnabled() { + // TODO(b/149014708) Read this from user-scoped settings once implemented. The user's + // location toggle will act as an override for this setting, i.e. so that the setting will + // return false if the location toggle is disabled. + return false; + } + @Override public boolean isDeviceTimeZoneInitialized() { // timezone.equals("GMT") will be true and only true if the time zone was diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java index 3d9ec6475a8b..fb7a73d12632 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; * The internal (in-process) system server API for the {@link * com.android.server.timezonedetector.TimeZoneDetectorService}. * + *

The methods on this class can be called from any thread. * @hide */ public interface TimeZoneDetectorInternal extends Dumpable.Container { @@ -29,7 +30,7 @@ public interface TimeZoneDetectorInternal extends Dumpable.Container { /** * Suggests the current time zone, determined using geolocation, to the detector. The * detector may ignore the signal based on system settings, whether better information is - * available, and so on. + * available, and so on. This method may be implemented asynchronously. */ void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion); } diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java index 4464f7d136e3..15412a0d14a1 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java @@ -58,6 +58,7 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter @NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) { Objects.requireNonNull(timeZoneSuggestion); + // All strategy calls must take place on the mHandler thread. mHandler.post( () -> mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(timeZoneSuggestion)); } diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java index 7467439905eb..d81f949742bd 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java @@ -130,6 +130,10 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub service.handleAutoTimeZoneConfigChanged(); } }); + // TODO(b/149014708) Listen for changes to geolocation time zone detection enabled config. + // This should also include listening to the current user and the current user's location + // toggle since the config is user-scoped and the location toggle overrides the geolocation + // time zone enabled setting. return service; } @@ -280,7 +284,7 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub void handleConfigurationChanged() { // Note: we could trigger an async time zone detection operation here via a call to - // handleAutoTimeZoneDetectionChanged(), but that is triggered in response to the underlying + // handleAutoTimeZoneConfigChanged(), but that is triggered in response to the underlying // setting value changing so it is currently unnecessary. If we get to a point where all // configuration changes are guaranteed to happen in response to an updateConfiguration() // call, then we can remove that path and call it here instead. @@ -288,7 +292,6 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub // Configuration has changed, but each user may have a different view of the configuration. // It's possible that this will cause unnecessary notifications but that shouldn't be a // problem. - synchronized (mConfigurationListeners) { final int userCount = mConfigurationListeners.size(); for (int userIndex = 0; userIndex < userCount; userIndex++) { diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java index f947a6554412..c5b7e39f4fef 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java @@ -30,9 +30,9 @@ import android.util.IndentingPrintWriter; *

The strategy uses suggestions to decide whether to modify the device's time zone setting * and what to set it to. * - *

Most calls will be handled by a single thread but that is not true for all calls. For example - * {@link #dump(IndentingPrintWriter, String[])}) may be called on a different thread so - * implementations mustvhandle thread safety. + *

Most calls will be handled by a single thread, but that is not true for all calls. For example + * {@link #dump(IndentingPrintWriter, String[])}) may be called on a different thread concurrently + * with other operations so implementations must still handle thread safety. * * @hide */ diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java index 9c36c3921e3e..d1369a289428 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java @@ -23,6 +23,7 @@ import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_S import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_APPLICABLE; import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_POSSESSED; import static android.app.timezonedetector.TimeZoneConfiguration.PROPERTY_AUTO_DETECTION_ENABLED; +import static android.app.timezonedetector.TimeZoneConfiguration.PROPERTY_GEO_DETECTION_ENABLED; import android.annotation.NonNull; import android.annotation.Nullable; @@ -97,6 +98,12 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat */ boolean isAutoDetectionEnabled(); + /** + * Returns whether geolocation can be used for time zone detection when {@link + * #isAutoDetectionEnabled()} returns {@code true}. + */ + boolean isGeoDetectionEnabled(); + /** * Returns true if the device has had an explicit time zone set. */ @@ -200,7 +207,15 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat */ @GuardedBy("this") private ArrayMapWithHistory - mSuggestionBySlotIndex = new ArrayMapWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE); + mTelephonySuggestionsBySlotIndex = + new ArrayMapWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE); + + /** + * The latest geolocation suggestion received. + */ + @GuardedBy("this") + private ReferenceWithHistory mLatestGeoLocationSuggestion = + new ReferenceWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE); @GuardedBy("this") private final List mDumpables = new ArrayList<>(); @@ -284,17 +299,26 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat private static boolean containsAutoTimeDetectionProperties( @NonNull TimeZoneConfiguration configuration) { - return configuration.hasProperty(PROPERTY_AUTO_DETECTION_ENABLED); + return configuration.hasProperty(PROPERTY_AUTO_DETECTION_ENABLED) + || configuration.hasProperty(PROPERTY_GEO_DETECTION_ENABLED); } @Override public synchronized void suggestGeolocationTimeZone( @NonNull GeolocationTimeZoneSuggestion suggestion) { + if (DBG) { + Slog.d(LOG_TAG, "Geolocation suggestion received. newSuggestion=" + suggestion); + } + Objects.requireNonNull(suggestion); + mLatestGeoLocationSuggestion.set(suggestion); - // TODO Implement this. - throw new UnsupportedOperationException( - "Geo-location time zone detection is not currently implemented"); + // Now perform auto time zone detection. The new suggestion may be used to modify the time + // zone setting. + if (mCallback.isGeoDetectionEnabled()) { + String reason = "New geolocation time zone suggested. suggestion=" + suggestion; + doAutoTimeZoneDetection(reason); + } } @Override @@ -332,12 +356,14 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat new QualifiedTelephonyTimeZoneSuggestion(suggestion, score); // Store the suggestion against the correct slotIndex. - mSuggestionBySlotIndex.put(suggestion.getSlotIndex(), scoredSuggestion); + mTelephonySuggestionsBySlotIndex.put(suggestion.getSlotIndex(), scoredSuggestion); // Now perform auto time zone detection. The new suggestion may be used to modify the time // zone setting. - String reason = "New telephony time suggested. suggestion=" + suggestion; - doAutoTimeZoneDetection(reason); + if (!mCallback.isGeoDetectionEnabled()) { + String reason = "New telephony time zone suggested. suggestion=" + suggestion; + doAutoTimeZoneDetection(reason); + } } private static int scoreTelephonySuggestion(@NonNull TelephonyTimeZoneSuggestion suggestion) { @@ -363,9 +389,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat } /** - * Finds the best available time zone suggestion from all slotIndexes. 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 strategy becomes / remains un-opinionated and nothing is set. + * Performs automatic time zone detection. */ @GuardedBy("this") private void doAutoTimeZoneDetection(@NonNull String detectionReason) { @@ -374,6 +398,62 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat return; } + // Use the right suggestions based on the current configuration. This check is potentially + // race-prone until this value is set via a call to TimeZoneDetectorStrategy. + if (mCallback.isGeoDetectionEnabled()) { + doGeolocationTimeZoneDetection(detectionReason); + } else { + doTelephonyTimeZoneDetection(detectionReason); + } + } + + /** + * Detects the time zone using the latest available geolocation time zone suggestion, if one is + * available. The outcome can be that this strategy becomes / remains un-opinionated and nothing + * is set. + */ + @GuardedBy("this") + private void doGeolocationTimeZoneDetection(@NonNull String detectionReason) { + GeolocationTimeZoneSuggestion latestGeolocationSuggestion = + mLatestGeoLocationSuggestion.get(); + if (latestGeolocationSuggestion == null) { + return; + } + + List zoneIds = latestGeolocationSuggestion.getZoneIds(); + if (zoneIds == null || zoneIds.isEmpty()) { + // This means the client has become uncertain about the time zone or it is certain there + // is no known zone. In either case we must leave the existing time zone setting as it + // is. + return; + } + + // GeolocationTimeZoneSuggestion has no measure of quality. We assume all suggestions are + // reliable. + String zoneId; + + // Introduce bias towards the device's current zone when there are multiple zone suggested. + String deviceTimeZone = mCallback.getDeviceTimeZone(); + if (zoneIds.contains(deviceTimeZone)) { + if (DBG) { + Slog.d(LOG_TAG, + "Geo tz suggestion contains current device time zone. Applying bias."); + } + zoneId = deviceTimeZone; + } else { + zoneId = zoneIds.get(0); + } + setDeviceTimeZoneIfRequired(zoneId, detectionReason); + } + + /** + * Detects the time zone using the latest available telephony time zone suggestions. + * Finds the best available time zone suggestion from all slotIndexes. 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 strategy becomes / remains un-opinionated and nothing is set. + */ + @GuardedBy("this") + private void doTelephonyTimeZoneDetection(@NonNull String detectionReason) { QualifiedTelephonyTimeZoneSuggestion bestTelephonySuggestion = findBestTelephonySuggestion(); @@ -468,9 +548,9 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // slotIndex and find the best. Note that we deliberately do not look at age: the caller can // rate-limit so age is not a strong indicator of confidence. Instead, the callers are // expected to withdraw suggestions they no longer have confidence in. - for (int i = 0; i < mSuggestionBySlotIndex.size(); i++) { + for (int i = 0; i < mTelephonySuggestionsBySlotIndex.size(); i++) { QualifiedTelephonyTimeZoneSuggestion candidateSuggestion = - mSuggestionBySlotIndex.valueAt(i); + mTelephonySuggestionsBySlotIndex.valueAt(i); if (candidateSuggestion == null) { // Unexpected continue; @@ -505,14 +585,10 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat @Override public synchronized void handleAutoTimeZoneConfigChanged() { if (DBG) { - Slog.d(LOG_TAG, "handleTimeZoneDetectionChange() called"); - } - if (mCallback.isAutoDetectionEnabled()) { - // When the user enabled time zone detection, run the time zone detection and change the - // device time zone if possible. - String reason = "Auto time zone detection setting enabled."; - doAutoTimeZoneDetection(reason); + Slog.d(LOG_TAG, "handleAutoTimeZoneConfigChanged()"); } + + doAutoTimeZoneDetection("handleAutoTimeZoneConfigChanged()"); } @Override @@ -532,15 +608,21 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat ipw.println("mCallback.isDeviceTimeZoneInitialized()=" + mCallback.isDeviceTimeZoneInitialized()); ipw.println("mCallback.getDeviceTimeZone()=" + mCallback.getDeviceTimeZone()); + ipw.println("mCallback.isGeoDetectionEnabled()=" + mCallback.isGeoDetectionEnabled()); ipw.println("Time zone change log:"); ipw.increaseIndent(); // level 2 mTimeZoneChangesLog.dump(ipw); ipw.decreaseIndent(); // level 2 + ipw.println("Geolocation suggestion history:"); + ipw.increaseIndent(); // level 2 + mLatestGeoLocationSuggestion.dump(ipw); + ipw.decreaseIndent(); // level 2 + ipw.println("Telephony suggestion history:"); ipw.increaseIndent(); // level 2 - mSuggestionBySlotIndex.dump(ipw); + mTelephonySuggestionsBySlotIndex.dump(ipw); ipw.decreaseIndent(); // level 2 ipw.decreaseIndent(); // level 1 @@ -555,7 +637,15 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat @VisibleForTesting public synchronized QualifiedTelephonyTimeZoneSuggestion getLatestTelephonySuggestion( int slotIndex) { - return mSuggestionBySlotIndex.get(slotIndex); + return mTelephonySuggestionsBySlotIndex.get(slotIndex); + } + + /** + * A method used to inspect strategy state during tests. Not intended for general use. + */ + @VisibleForTesting + public GeolocationTimeZoneSuggestion getLatestGeolocationSuggestion() { + return mLatestGeoLocationSuggestion.get(); } /** diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java index dcf319058ca2..e5e931115c05 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java @@ -27,6 +27,9 @@ import android.app.timezonedetector.TimeZoneCapabilities; import android.app.timezonedetector.TimeZoneConfiguration; import android.util.IndentingPrintWriter; +import java.util.ArrayList; +import java.util.List; + class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { private StrategyListener mListener; @@ -41,6 +44,7 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { private TelephonyTimeZoneSuggestion mLastTelephonySuggestion; private boolean mHandleAutoTimeZoneConfigChangedCalled; private boolean mDumpCalled; + private final List mDumpables = new ArrayList<>(); @Override public void setStrategyListener(@NonNull StrategyListener listener) { @@ -105,7 +109,7 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { @Override public void addDumpable(Dumpable dumpable) { - // Stubbed + mDumpables.add(dumpable); } @Override @@ -149,4 +153,8 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { void verifyDumpCalled() { assertTrue(mDumpCalled); } + + void verifyHasDumpable(Dumpable expected) { + assertTrue(mDumpables.contains(expected)); + } } diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java index 0e2c22756097..e9d57e52ce69 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java @@ -75,6 +75,16 @@ public class TimeZoneDetectorInternalImplTest { mFakeTimeZoneDetectorStrategy.verifySuggestGeolocationTimeZoneCalled(timeZoneSuggestion); } + @Test + public void testAddDumpable() throws Exception { + Dumpable stubbedDumpable = mock(Dumpable.class); + + mTimeZoneDetectorInternal.addDumpable(stubbedDumpable); + mTestHandler.assertTotalMessagesEnqueued(0); + + mFakeTimeZoneDetectorStrategy.verifyHasDumpable(stubbedDumpable); + } + private static GeolocationTimeZoneSuggestion createGeolocationTimeZoneSuggestion() { return new GeolocationTimeZoneSuggestion(ARBITRARY_ZONE_IDS); } diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java index 8034cacc6923..3a1ec4f90d7a 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java @@ -52,6 +52,7 @@ import org.junit.runner.RunWith; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Arrays; @RunWith(AndroidJUnit4.class) public class TimeZoneDetectorServiceTest { @@ -252,6 +253,39 @@ public class TimeZoneDetectorServiceTest { } } + @Test(expected = SecurityException.class) + public void testSuggestGeolocationTimeZone_withoutPermission() { + doThrow(new SecurityException("Mock")) + .when(mMockContext).enforceCallingOrSelfPermission(anyString(), any()); + GeolocationTimeZoneSuggestion timeZoneSuggestion = createGeolocationTimeZoneSuggestion(); + + try { + mTimeZoneDetectorService.suggestGeolocationTimeZone(timeZoneSuggestion); + fail(); + } finally { + verify(mMockContext).enforceCallingOrSelfPermission( + eq(android.Manifest.permission.SET_TIME_ZONE), + anyString()); + } + } + + @Test + public void testSuggestGeolocationTimeZone() throws Exception { + doNothing().when(mMockContext).enforceCallingOrSelfPermission(anyString(), any()); + + GeolocationTimeZoneSuggestion timeZoneSuggestion = createGeolocationTimeZoneSuggestion(); + + mTimeZoneDetectorService.suggestGeolocationTimeZone(timeZoneSuggestion); + mTestHandler.assertTotalMessagesEnqueued(1); + + verify(mMockContext).enforceCallingOrSelfPermission( + eq(android.Manifest.permission.SET_TIME_ZONE), + anyString()); + + mTestHandler.waitForMessagesToBeProcessed(); + mFakeTimeZoneDetectorStrategy.verifySuggestGeolocationTimeZoneCalled(timeZoneSuggestion); + } + @Test(expected = SecurityException.class) public void testSuggestManualTimeZone_withoutPermission() { doThrow(new SecurityException("Mock")) @@ -346,7 +380,7 @@ public class TimeZoneDetectorServiceTest { } @Test - public void testAutoTimeZoneDetectionChanged() throws Exception { + public void testHandleAutoTimeZoneConfigChanged() throws Exception { mTimeZoneDetectorService.handleAutoTimeZoneConfigChanged(); mTestHandler.assertTotalMessagesEnqueued(1); mTestHandler.waitForMessagesToBeProcessed(); @@ -370,10 +404,15 @@ public class TimeZoneDetectorServiceTest { private static TimeZoneCapabilities createTimeZoneCapabilities() { return new TimeZoneCapabilities.Builder(ARBITRARY_USER_ID) .setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED) + .setConfigureGeoDetectionEnabled(CAPABILITY_POSSESSED) .setSuggestManualTimeZone(CAPABILITY_POSSESSED) .build(); } + private static GeolocationTimeZoneSuggestion createGeolocationTimeZoneSuggestion() { + return new GeolocationTimeZoneSuggestion(Arrays.asList("TestZoneId")); + } + private static ManualTimeZoneSuggestion createManualTimeZoneSuggestion() { return new ManualTimeZoneSuggestion("TestZoneId"); } diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java index 68554451e43a..a6caa4299ef6 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java @@ -41,6 +41,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; @@ -56,10 +57,10 @@ import org.junit.Before; import org.junit.Test; import java.io.StringWriter; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; +import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; @@ -93,16 +94,26 @@ public class TimeZoneDetectorStrategyImplTest { TELEPHONY_SCORE_HIGHEST), }; - private static final TimeZoneConfiguration CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED = + private static final TimeZoneConfiguration CONFIG_AUTO_ENABLED = new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(true) .build(); - private static final TimeZoneConfiguration CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED = + private static final TimeZoneConfiguration CONFIG_AUTO_DISABLED = new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(false) .build(); + private static final TimeZoneConfiguration CONFIG_GEO_DETECTION_DISABLED = + new TimeZoneConfiguration.Builder() + .setGeoDetectionEnabled(false) + .build(); + + private static final TimeZoneConfiguration CONFIG_GEO_DETECTION_ENABLED = + new TimeZoneConfiguration.Builder() + .setGeoDetectionEnabled(true) + .build(); + private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy; private FakeCallback mFakeCallback; private MockStrategyListener mMockStrategyListener; @@ -120,7 +131,7 @@ public class TimeZoneDetectorStrategyImplTest { public void testGetCapabilities() { new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); TimeZoneCapabilities expectedCapabilities = mFakeCallback.getCapabilities(USER_ID); assertEquals(expectedCapabilities, mTimeZoneDetectorStrategy.getCapabilities(USER_ID)); } @@ -129,7 +140,7 @@ public class TimeZoneDetectorStrategyImplTest { public void testGetConfiguration() { new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); TimeZoneConfiguration expectedConfiguration = mFakeCallback.getConfiguration(USER_ID); assertTrue(expectedConfiguration.isComplete()); assertEquals(expectedConfiguration, mTimeZoneDetectorStrategy.getConfiguration(USER_ID)); @@ -140,20 +151,22 @@ public class TimeZoneDetectorStrategyImplTest { Script script = new Script(); script.initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); { // Check the fake test infra is doing what is expected. TimeZoneCapabilities capabilities = mFakeCallback.getCapabilities(USER_ID); assertEquals(CAPABILITY_POSSESSED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_POSSESSED, capabilities.getConfigureGeoDetectionEnabled()); assertEquals(CAPABILITY_NOT_APPLICABLE, capabilities.getSuggestManualTimeZone()); } script.initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED); + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)); { // Check the fake test infra is doing what is expected. TimeZoneCapabilities capabilities = mFakeCallback.getCapabilities(USER_ID); assertEquals(CAPABILITY_POSSESSED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_POSSESSED, capabilities.getConfigureGeoDetectionEnabled()); assertEquals(CAPABILITY_POSSESSED, capabilities.getSuggestManualTimeZone()); } } @@ -163,20 +176,22 @@ public class TimeZoneDetectorStrategyImplTest { Script script = new Script(); script.initializeUser(USER_ID, UserCase.RESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); { // Check the fake test infra is doing what is expected. TimeZoneCapabilities capabilities = mFakeCallback.getCapabilities(USER_ID); assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getConfigureGeoDetectionEnabled()); assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getSuggestManualTimeZone()); } script.initializeUser(USER_ID, UserCase.RESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED); + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)); { // Check the fake test infra is doing what is expected. TimeZoneCapabilities capabilities = mFakeCallback.getCapabilities(USER_ID); assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getConfigureGeoDetectionEnabled()); assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getSuggestManualTimeZone()); } } @@ -186,20 +201,22 @@ public class TimeZoneDetectorStrategyImplTest { Script script = new Script(); script.initializeUser(USER_ID, UserCase.AUTO_DETECT_NOT_SUPPORTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); { // Check the fake test infra is doing what is expected. TimeZoneCapabilities capabilities = mFakeCallback.getCapabilities(USER_ID); assertEquals(CAPABILITY_NOT_SUPPORTED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_SUPPORTED, capabilities.getConfigureGeoDetectionEnabled()); assertEquals(CAPABILITY_POSSESSED, capabilities.getSuggestManualTimeZone()); } script.initializeUser(USER_ID, UserCase.AUTO_DETECT_NOT_SUPPORTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED); + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)); { // Check the fake test infra is doing what is expected. TimeZoneCapabilities capabilities = mFakeCallback.getCapabilities(USER_ID); assertEquals(CAPABILITY_NOT_SUPPORTED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_SUPPORTED, capabilities.getConfigureGeoDetectionEnabled()); assertEquals(CAPABILITY_POSSESSED, capabilities.getSuggestManualTimeZone()); } } @@ -208,42 +225,61 @@ public class TimeZoneDetectorStrategyImplTest { public void testUpdateConfiguration_unrestricted() { Script script = new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); // Set the configuration with auto detection enabled. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */); // Nothing should have happened: it was initialized in this state. script.verifyConfigurationNotChanged(); // Update the configuration with auto detection disabled. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED); + script.simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */); // The settings should have been changed and the StrategyListener onChange() called. - script.verifyConfigurationChangedAndReset( - USER_ID, CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED); + script.verifyConfigurationChangedAndReset(USER_ID, + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)); // Update the configuration with auto detection enabled. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */); // The settings should have been changed and the StrategyListener onChange() called. - script.verifyConfigurationChangedAndReset(USER_ID, CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + script.verifyConfigurationChangedAndReset(USER_ID, + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); + + // Update the configuration to enable geolocation time zone detection. + script.simulateUpdateConfiguration( + USER_ID, CONFIG_GEO_DETECTION_ENABLED, true /* expectedResult */); + + // The settings should have been changed and the StrategyListener onChange() called. + script.verifyConfigurationChangedAndReset(USER_ID, + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_ENABLED)); } @Test public void testUpdateConfiguration_restricted() { Script script = new Script() .initializeUser(USER_ID, UserCase.RESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); // Try to update the configuration with auto detection disabled. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED); + script.simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_DISABLED, false /* expectedResult */); // The settings should not have been changed: user shouldn't have the capabilities. script.verifyConfigurationNotChanged(); // Update the configuration with auto detection enabled. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + script.simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_ENABLED, false /* expectedResult */); + + // The settings should not have been changed: user shouldn't have the capabilities. + script.verifyConfigurationNotChanged(); + + // Update the configuration to enable geolocation time zone detection. + script.simulateUpdateConfiguration( + USER_ID, CONFIG_GEO_DETECTION_ENABLED, false /* expectedResult */); // The settings should not have been changed: user shouldn't have the capabilities. script.verifyConfigurationNotChanged(); @@ -253,16 +289,18 @@ public class TimeZoneDetectorStrategyImplTest { public void testUpdateConfiguration_autoDetectNotSupported() { Script script = new Script() .initializeUser(USER_ID, UserCase.AUTO_DETECT_NOT_SUPPORTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); // Try to update the configuration with auto detection disabled. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED); + script.simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_DISABLED, false /* expectedResult */); // The settings should not have been changed: user shouldn't have the capabilities. script.verifyConfigurationNotChanged(); // Update the configuration with auto detection enabled. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + script.simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_ENABLED, false /* expectedResult */); // The settings should not have been changed: user shouldn't have the capabilities. script.verifyConfigurationNotChanged(); @@ -276,7 +314,7 @@ public class TimeZoneDetectorStrategyImplTest { createEmptySlotIndex2Suggestion(); Script script = new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED) + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); script.simulateTelephonyTimeZoneSuggestion(slotIndex1TimeZoneSuggestion) @@ -308,6 +346,10 @@ public class TimeZoneDetectorStrategyImplTest { mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); } + /** + * Telephony suggestions have quality metadata. Ordinarily, low scoring suggestions are not + * used, but this is not true if the device's time zone setting is uninitialized. + */ @Test public void testTelephonySuggestionsWhenTimeZoneUninitialized() { assertTrue(TELEPHONY_SCORE_LOW < TELEPHONY_SCORE_USAGE_THRESHOLD); @@ -319,7 +361,7 @@ public class TimeZoneDetectorStrategyImplTest { Script script = new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); // A low quality suggestions will not be taken: The device time zone setting is left // uninitialized. @@ -376,16 +418,16 @@ public class TimeZoneDetectorStrategyImplTest { /** * Confirms that toggling the auto time zone detection setting has the expected behavior when - * the strategy is "opinionated". + * the strategy is "opinionated" when using telephony auto detection. */ @Test - public void testTogglingAutoTimeZoneDetection() { + public void testTogglingAutoDetection_autoTelephony() { Script script = new Script(); for (TelephonyTestCase testCase : TELEPHONY_TEST_CASES) { // Start with the device in a known state. script.initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED) + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); TelephonyTimeZoneSuggestion suggestion = @@ -405,7 +447,8 @@ public class TimeZoneDetectorStrategyImplTest { mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); // Toggling the time zone setting on should cause the device setting to be set. - script.simulateAutoTimeZoneDetectionEnabled(USER_ID, true); + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, + true /* expectedResult */); // When time zone detection is already enabled the suggestion (if it scores highly // enough) should be set immediately. @@ -422,7 +465,8 @@ public class TimeZoneDetectorStrategyImplTest { mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); // Toggling the time zone setting should off should do nothing. - script.simulateAutoTimeZoneDetectionEnabled(USER_ID, false) + script.simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */) .verifyTimeZoneNotChanged(); // Assert internal service state. @@ -437,7 +481,7 @@ public class TimeZoneDetectorStrategyImplTest { public void testTelephonySuggestionsSingleSlotId() { Script script = new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED) + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); for (TelephonyTestCase testCase : TELEPHONY_TEST_CASES) { @@ -451,8 +495,7 @@ public class TimeZoneDetectorStrategyImplTest { */ // Each test case will have the same or lower score than the last. - ArrayList descendingCasesByScore = - new ArrayList<>(Arrays.asList(TELEPHONY_TEST_CASES)); + List descendingCasesByScore = list(TELEPHONY_TEST_CASES); Collections.reverse(descendingCasesByScore); for (TelephonyTestCase testCase : descendingCasesByScore) { @@ -504,7 +547,7 @@ public class TimeZoneDetectorStrategyImplTest { Script script = new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED) + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)) .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. @@ -583,15 +626,15 @@ public class TimeZoneDetectorStrategyImplTest { } /** - * The {@link TimeZoneDetectorStrategyImpl.Callback} is left to detect whether changing - * the time zone is actually necessary. This test proves that the service doesn't assume it - * knows the current setting. + * The {@link TimeZoneDetectorStrategyImpl.Callback} is left to detect whether changing the time + * zone is actually necessary. This test proves that the strategy doesn't assume it knows the + * current settings. */ @Test - public void testTelephonySuggestionTimeZoneDetectorStrategyDoesNotAssumeCurrentSetting() { + public void testTelephonySuggestionStrategyDoesNotAssumeCurrentSetting_autoTelephony() { Script script = new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED); + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); TelephonyTestCase testCase = newTelephonyTestCase( MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE, TELEPHONY_SCORE_HIGH); @@ -609,26 +652,40 @@ public class TimeZoneDetectorStrategyImplTest { // Toggling time zone detection should set the device time zone only if the current setting // value is different from the most recent telephony suggestion. - script.simulateAutoTimeZoneDetectionEnabled(USER_ID, false) + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */) .verifyTimeZoneNotChanged() - .simulateAutoTimeZoneDetectionEnabled(USER_ID, true) + .simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) .verifyTimeZoneNotChanged(); // 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.simulateAutoTimeZoneDetectionEnabled(USER_ID, false) + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */) .simulateTelephonyTimeZoneSuggestion(newYorkSuggestion) .verifyTimeZoneNotChanged(); // Latest suggestion should be used. - script.simulateAutoTimeZoneDetectionEnabled(USER_ID, true) + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) .verifyTimeZoneChangedAndReset(newYorkSuggestion); } @Test - public void testManualSuggestion_unrestricted_simulateAutoTimeZoneEnabled() { + public void testManualSuggestion_autoDetectionEnabled_autoTelephony() { + checkManualSuggestion_autoDetectionEnabled(false /* geoDetectionEnabled */); + } + + @Test + public void testManualSuggestion_autoDetectionEnabled_autoGeo() { + checkManualSuggestion_autoDetectionEnabled(true /* geoDetectionEnabled */); + } + + private void checkManualSuggestion_autoDetectionEnabled(boolean geoDetectionEnabled) { + TimeZoneConfiguration geoTzEnabledConfig = + new TimeZoneConfiguration.Builder() + .setGeoDetectionEnabled(geoDetectionEnabled) + .build(); Script script = new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED) + CONFIG_AUTO_ENABLED.with(geoTzEnabledConfig)) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Auto time zone detection is enabled so the manual suggestion should be ignored. @@ -641,7 +698,7 @@ public class TimeZoneDetectorStrategyImplTest { public void testManualSuggestion_restricted_simulateAutoTimeZoneEnabled() { Script script = new Script() .initializeUser(USER_ID, UserCase.RESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED) + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Auto time zone detection is enabled so the manual suggestion should be ignored. @@ -654,7 +711,7 @@ public class TimeZoneDetectorStrategyImplTest { public void testManualSuggestion_autoDetectNotSupported_simulateAutoTimeZoneEnabled() { Script script = new Script() .initializeUser(USER_ID, UserCase.AUTO_DETECT_NOT_SUPPORTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_ENABLED) + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Auto time zone detection is enabled so the manual suggestion should be ignored. @@ -668,7 +725,7 @@ public class TimeZoneDetectorStrategyImplTest { public void testManualSuggestion_unrestricted_autoTimeZoneDetectionDisabled() { Script script = new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED) + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Auto time zone detection is disabled so the manual suggestion should be used. @@ -682,7 +739,7 @@ public class TimeZoneDetectorStrategyImplTest { public void testManualSuggestion_restricted_autoTimeZoneDetectionDisabled() { Script script = new Script() .initializeUser(USER_ID, UserCase.RESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED) + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Restricted users do not have the capability. @@ -696,7 +753,7 @@ public class TimeZoneDetectorStrategyImplTest { public void testManualSuggestion_autoDetectNotSupported_autoTimeZoneDetectionDisabled() { Script script = new Script() .initializeUser(USER_ID, UserCase.AUTO_DETECT_NOT_SUPPORTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED) + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Unrestricted users have the capability. @@ -706,11 +763,262 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneChangedAndReset(manualSuggestion); } + @Test + public void testGeoSuggestion_uncertain() { + Script script = new Script() + .initializeUser(USER_ID, UserCase.UNRESTRICTED, + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_ENABLED)) + .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); + + GeolocationTimeZoneSuggestion uncertainSuggestion = createUncertainGeoLocationSuggestion(); + + script.simulateGeolocationTimeZoneSuggestion(uncertainSuggestion) + .verifyTimeZoneNotChanged(); + + // Assert internal service state. + assertEquals(uncertainSuggestion, + mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + } + + @Test + public void testGeoSuggestion_noZones() { + Script script = new Script() + .initializeUser(USER_ID, UserCase.UNRESTRICTED, + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_ENABLED)) + .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); + + GeolocationTimeZoneSuggestion noZonesSuggestion = createGeoLocationSuggestion(list()); + + script.simulateGeolocationTimeZoneSuggestion(noZonesSuggestion) + .verifyTimeZoneNotChanged(); + + // Assert internal service state. + assertEquals(noZonesSuggestion, mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + } + + @Test + public void testGeoSuggestion_oneZone() { + GeolocationTimeZoneSuggestion suggestion = + createGeoLocationSuggestion(list("Europe/London")); + + Script script = new Script() + .initializeUser(USER_ID, UserCase.UNRESTRICTED, + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_ENABLED)) + .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); + + script.simulateGeolocationTimeZoneSuggestion(suggestion) + .verifyTimeZoneChangedAndReset("Europe/London"); + + // Assert internal service state. + assertEquals(suggestion, mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + } + + /** + * In the current implementation, the first zone ID is always used unless the device is set to + * one of the other options. This is "stickiness" - the device favors the zone it is currently + * set to until that unambiguously can't be correct. + */ + @Test + public void testGeoSuggestion_multiZone() { + GeolocationTimeZoneSuggestion londonOnlySuggestion = + createGeoLocationSuggestion(list("Europe/London")); + GeolocationTimeZoneSuggestion londonOrParisSuggestion = + createGeoLocationSuggestion(list("Europe/Paris", "Europe/London")); + GeolocationTimeZoneSuggestion parisOnlySuggestion = + createGeoLocationSuggestion(list("Europe/Paris")); + + Script script = new Script() + .initializeUser(USER_ID, UserCase.UNRESTRICTED, + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_ENABLED)) + .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); + + script.simulateGeolocationTimeZoneSuggestion(londonOnlySuggestion) + .verifyTimeZoneChangedAndReset("Europe/London"); + assertEquals(londonOnlySuggestion, + mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + + // Confirm bias towards the current device zone when there's multiple zones to choose from. + script.simulateGeolocationTimeZoneSuggestion(londonOrParisSuggestion) + .verifyTimeZoneNotChanged(); + assertEquals(londonOrParisSuggestion, + mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + + script.simulateGeolocationTimeZoneSuggestion(parisOnlySuggestion) + .verifyTimeZoneChangedAndReset("Europe/Paris"); + assertEquals(parisOnlySuggestion, + mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + + // Now the suggestion that previously left the device on Europe/London will leave the device + // on Europe/Paris. + script.simulateGeolocationTimeZoneSuggestion(londonOrParisSuggestion) + .verifyTimeZoneNotChanged(); + assertEquals(londonOrParisSuggestion, + mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + } + + /** + * Confirms that toggling the auto time zone detection enabled setting has the expected behavior + * when the strategy is "opinionated" and "un-opinionated" when in geolocation detection is + * enabled. + */ + @Test + public void testTogglingAutoDetectionEnabled_autoGeo() { + GeolocationTimeZoneSuggestion geolocationSuggestion = + createGeoLocationSuggestion(list("Europe/London")); + GeolocationTimeZoneSuggestion uncertainGeolocationSuggestion = + createUncertainGeoLocationSuggestion(); + ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris"); + + Script script = new Script() + .initializeUser(USER_ID, UserCase.UNRESTRICTED, + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_ENABLED)) + .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); + + script.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion); + + // When time zone detection is not enabled, the time zone suggestion will not be set. + script.verifyTimeZoneNotChanged(); + + // Assert internal service state. + assertEquals(geolocationSuggestion, + mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + + // Toggling the time zone setting on should cause the device setting to be set. + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) + .verifyTimeZoneChangedAndReset("Europe/London"); + + // Toggling the time zone setting should off should do nothing because the device is now + // set to that time zone. + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */) + .verifyTimeZoneNotChanged() + .simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) + .verifyTimeZoneNotChanged(); + + // Now toggle auto time zone setting, and confirm it is opinionated. + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */) + .simulateManualTimeZoneSuggestion( + USER_ID, manualSuggestion, true /* expectedResult */) + .verifyTimeZoneChangedAndReset(manualSuggestion) + .simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) + .verifyTimeZoneChangedAndReset("Europe/London"); + + // Now withdraw the geolocation suggestion, and assert the strategy is no longer + // opinionated. + /* expectedResult */ + script.simulateGeolocationTimeZoneSuggestion(uncertainGeolocationSuggestion) + .verifyTimeZoneNotChanged() + .simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */) + .verifyTimeZoneNotChanged() + .simulateManualTimeZoneSuggestion( + USER_ID, manualSuggestion, true /* expectedResult */) + .verifyTimeZoneChangedAndReset(manualSuggestion) + .simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) + .verifyTimeZoneNotChanged(); + + // Assert internal service state. + assertEquals(uncertainGeolocationSuggestion, + mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + } + + /** + * Confirms that changing the geolocation time zone detection enabled setting has the expected + * behavior, i.e. immediately recompute the detected time zone using different signals. + */ + @Test + public void testChangingGeoDetectionEnabled() { + GeolocationTimeZoneSuggestion geolocationSuggestion = + createGeoLocationSuggestion(list("Europe/London")); + TelephonyTimeZoneSuggestion telephonySuggestion = createTelephonySuggestion( + SLOT_INDEX1, MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE, + "Europe/Paris"); + + Script script = new Script() + .initializeUser(USER_ID, UserCase.UNRESTRICTED, + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); + + // Add suggestions. Nothing should happen as time zone detection is disabled. + script.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion) + .verifyTimeZoneNotChanged(); + script.simulateTelephonyTimeZoneSuggestion(telephonySuggestion) + .verifyTimeZoneNotChanged(); + + // Assert internal service state. + assertEquals(geolocationSuggestion, + mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + assertEquals(telephonySuggestion, + mTimeZoneDetectorStrategy.getLatestTelephonySuggestion(SLOT_INDEX1).suggestion); + + // Toggling the time zone detection enabled setting on should cause the device setting to be + // set from the telephony signal, as we've started with geolocation time zone detection + // disabled. + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) + .verifyTimeZoneChangedAndReset(telephonySuggestion); + + // Changing the detection to enable geo detection should cause the device tz setting to + // change to the geo suggestion. + script.simulateUpdateConfiguration( + USER_ID, CONFIG_GEO_DETECTION_ENABLED, true /* expectedResult */) + .verifyTimeZoneChangedAndReset(geolocationSuggestion.getZoneIds().get(0)); + + // Changing the detection to disable geo detection should cause the device tz setting to + // change to the telephony suggestion. + script.simulateUpdateConfiguration( + USER_ID, CONFIG_GEO_DETECTION_DISABLED, true /* expectedResult */) + .verifyTimeZoneChangedAndReset(telephonySuggestion); + } + + /** + * The {@link TimeZoneDetectorStrategyImpl.Callback} is left to detect whether changing the time + * zone is actually necessary. This test proves that the strategy doesn't assume it knows the + * current setting. + */ + @Test + public void testTimeZoneDetectorStrategyDoesNotAssumeCurrentSetting_autoGeo() { + GeolocationTimeZoneSuggestion losAngelesSuggestion = + createGeoLocationSuggestion(list("America/Los_Angeles")); + GeolocationTimeZoneSuggestion newYorkSuggestion = + createGeoLocationSuggestion(list("America/New_York")); + + Script script = new Script() + .initializeUser(USER_ID, UserCase.UNRESTRICTED, + CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_ENABLED)); + + // Initialization. + script.simulateGeolocationTimeZoneSuggestion(losAngelesSuggestion) + .verifyTimeZoneChangedAndReset("America/Los_Angeles"); + // Suggest it again - it should not be set because it is already set. + script.simulateGeolocationTimeZoneSuggestion(losAngelesSuggestion) + .verifyTimeZoneNotChanged(); + + // Toggling time zone detection should set the device time zone only if the current setting + // value is different from the most recent telephony suggestion. + /* expectedResult */ + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */) + .verifyTimeZoneNotChanged() + .simulateUpdateConfiguration( + USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) + .verifyTimeZoneNotChanged(); + + // 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.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */) + .simulateGeolocationTimeZoneSuggestion(newYorkSuggestion) + .verifyTimeZoneNotChanged(); + // Latest suggestion should be used. + script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) + .verifyTimeZoneChangedAndReset("America/New_York"); + } + @Test public void testAddDumpable() { new Script() .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_TIME_ZONE_DETECTION_DISABLED) + CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); AtomicBoolean dumpCalled = new AtomicBoolean(false); @@ -733,6 +1041,15 @@ public class TimeZoneDetectorStrategyImplTest { return new ManualTimeZoneSuggestion(zoneId); } + private static TelephonyTimeZoneSuggestion createTelephonySuggestion( + int slotIndex, @MatchType int matchType, @Quality int quality, String zoneId) { + return new TelephonyTimeZoneSuggestion.Builder(slotIndex) + .setMatchType(matchType) + .setQuality(quality) + .setZoneId(zoneId) + .build(); + } + private static TelephonyTimeZoneSuggestion createEmptySlotIndex1Suggestion() { return new TelephonyTimeZoneSuggestion.Builder(SLOT_INDEX1).build(); } @@ -741,6 +1058,17 @@ public class TimeZoneDetectorStrategyImplTest { return new TelephonyTimeZoneSuggestion.Builder(SLOT_INDEX2).build(); } + private static GeolocationTimeZoneSuggestion createUncertainGeoLocationSuggestion() { + return createGeoLocationSuggestion(null); + } + + private static GeolocationTimeZoneSuggestion createGeoLocationSuggestion( + @Nullable List zoneIds) { + GeolocationTimeZoneSuggestion suggestion = new GeolocationTimeZoneSuggestion(zoneIds); + suggestion.addDebugInfo("Test suggestion"); + return suggestion; + } + static class FakeCallback implements TimeZoneDetectorStrategyImpl.Callback { private TimeZoneCapabilities mCapabilities; @@ -757,7 +1085,8 @@ public class TimeZoneDetectorStrategyImplTest { TimeZoneConfiguration configuration) { assertEquals(userId, capabilities.getUserId()); mCapabilities = capabilities; - assertTrue(configuration.isComplete()); + assertTrue("Configuration must be complete when initializing, config=" + configuration, + configuration.isComplete()); mConfiguration.init(new UserConfiguration(userId, configuration)); } @@ -790,11 +1119,8 @@ public class TimeZoneDetectorStrategyImplTest { mConfiguration.set(new UserConfiguration(userId, newConfig)); if (!newConfig.equals(oldConfig)) { - if (oldConfig.isAutoDetectionEnabled() != newConfig.isAutoDetectionEnabled()) { - // Simulate what happens when the auto detection enabled configuration is - // changed. - mStrategy.handleAutoTimeZoneConfigChanged(); - } + // Simulate what happens when the auto detection configuration is changed. + mStrategy.handleAutoTimeZoneConfigChanged(); } } @@ -803,6 +1129,11 @@ public class TimeZoneDetectorStrategyImplTest { return mConfiguration.getLatest().configuration.isAutoDetectionEnabled(); } + @Override + public boolean isGeoDetectionEnabled() { + return mConfiguration.getLatest().configuration.isGeoDetectionEnabled(); + } + @Override public boolean isDeviceTimeZoneInitialized() { return mTimeZoneId.getLatest() != null; @@ -942,32 +1273,33 @@ public class TimeZoneDetectorStrategyImplTest { * supplied configuration. */ private static TimeZoneCapabilities createCapabilities( - int userId, UserCase userRole, TimeZoneConfiguration configuration) { - switch (userRole) { + int userId, UserCase userCase, TimeZoneConfiguration configuration) { + switch (userCase) { case UNRESTRICTED: { int suggestManualTimeZoneCapability = configuration.isAutoDetectionEnabled() ? CAPABILITY_NOT_APPLICABLE : CAPABILITY_POSSESSED; return new TimeZoneCapabilities.Builder(userId) .setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED) + .setConfigureGeoDetectionEnabled(CAPABILITY_POSSESSED) .setSuggestManualTimeZone(suggestManualTimeZoneCapability) .build(); } case RESTRICTED: { return new TimeZoneCapabilities.Builder(userId) .setConfigureAutoDetectionEnabled(CAPABILITY_NOT_ALLOWED) + .setConfigureGeoDetectionEnabled(CAPABILITY_NOT_ALLOWED) .setSuggestManualTimeZone(CAPABILITY_NOT_ALLOWED) .build(); - } case AUTO_DETECT_NOT_SUPPORTED: { return new TimeZoneCapabilities.Builder(userId) .setConfigureAutoDetectionEnabled(CAPABILITY_NOT_SUPPORTED) + .setConfigureGeoDetectionEnabled(CAPABILITY_NOT_SUPPORTED) .setSuggestManualTimeZone(CAPABILITY_POSSESSED) .build(); - } default: - throw new AssertionError(userRole + " not recognized"); + throw new AssertionError(userCase + " not recognized"); } } @@ -978,8 +1310,8 @@ public class TimeZoneDetectorStrategyImplTest { private class Script { Script initializeUser( - @UserIdInt int userId, UserCase userRole, TimeZoneConfiguration configuration) { - TimeZoneCapabilities capabilities = createCapabilities(userId, userRole, configuration); + @UserIdInt int userId, UserCase userCase, TimeZoneConfiguration configuration) { + TimeZoneCapabilities capabilities = createCapabilities(userId, userCase, configuration); mFakeCallback.initializeUser(userId, capabilities, configuration); return this; } @@ -989,27 +1321,24 @@ public class TimeZoneDetectorStrategyImplTest { return this; } - Script simulateAutoTimeZoneDetectionEnabled(@UserIdInt int userId, boolean enabled) { - TimeZoneConfiguration configuration = new TimeZoneConfiguration.Builder() - .setAutoDetectionEnabled(enabled) - .build(); - return simulateUpdateConfiguration(userId, configuration); - } - /** - * Simulates the time zone detection strategy receiving an updated configuration. + * Simulates the time zone detection strategy receiving an updated configuration and checks + * the return value. */ Script simulateUpdateConfiguration( - @UserIdInt int userId, TimeZoneConfiguration configuration) { - mTimeZoneDetectorStrategy.updateConfiguration(userId, configuration); + @UserIdInt int userId, TimeZoneConfiguration configuration, + boolean expectedResult) { + assertEquals(expectedResult, + mTimeZoneDetectorStrategy.updateConfiguration(userId, configuration)); return this; } /** - * Simulates the time zone detection strategy receiving a telephony-originated suggestion. + * Simulates the time zone detection strategy receiving a geolocation-originated + * suggestion. */ - Script simulateTelephonyTimeZoneSuggestion(TelephonyTimeZoneSuggestion timeZoneSuggestion) { - mTimeZoneDetectorStrategy.suggestTelephonyTimeZone(timeZoneSuggestion); + Script simulateGeolocationTimeZoneSuggestion(GeolocationTimeZoneSuggestion suggestion) { + mTimeZoneDetectorStrategy.suggestGeolocationTimeZone(suggestion); return this; } @@ -1024,13 +1353,26 @@ public class TimeZoneDetectorStrategyImplTest { return this; } + /** + * Simulates the time zone detection strategy receiving a telephony-originated suggestion. + */ + Script simulateTelephonyTimeZoneSuggestion(TelephonyTimeZoneSuggestion timeZoneSuggestion) { + mTimeZoneDetectorStrategy.suggestTelephonyTimeZone(timeZoneSuggestion); + return this; + } + + /** + * Confirms that the device's time zone has not been set by previous actions since the test + * state was last reset. + */ Script verifyTimeZoneNotChanged() { mFakeCallback.assertTimeZoneNotChanged(); return this; } - Script verifyTimeZoneChangedAndReset(TelephonyTimeZoneSuggestion suggestion) { - mFakeCallback.assertTimeZoneChangedTo(suggestion.getZoneId()); + /** Verifies the device's time zone has been set and clears change tracking history. */ + Script verifyTimeZoneChangedAndReset(String zoneId) { + mFakeCallback.assertTimeZoneChangedTo(zoneId); mFakeCallback.commitAllChanges(); return this; } @@ -1041,6 +1383,12 @@ public class TimeZoneDetectorStrategyImplTest { return this; } + Script verifyTimeZoneChangedAndReset(TelephonyTimeZoneSuggestion suggestion) { + mFakeCallback.assertTimeZoneChangedTo(suggestion.getZoneId()); + mFakeCallback.commitAllChanges(); + return this; + } + /** * Verifies that the configuration has been changed to the expected value. */ @@ -1120,4 +1468,8 @@ public class TimeZoneDetectorStrategyImplTest { mOnConfigurationChangedCalled = false; } } + + private static List list(T... values) { + return Arrays.asList(values); + } } -- cgit v1.2.3-59-g8ed1b