From a223581eee79357f71254dd41099f2a54f9cef87 Mon Sep 17 00:00:00 2001 From: Neil Fuller Date: Sat, 15 Aug 2020 15:48:37 +0100 Subject: [PATCH] Implement user-scoped geolocation configuration Implement user-scoped geolocation configuration / behavior. The main changes: 1) For simplicity, configuration change listener is no longer passed the latest config. It is left to the listener to request the latest config. This removes some of the previous complexity that required a SparseArray to track listeners by userId. 2) The Callback implementation is now responsible for listening for configuration / user changes and invoking a listener (which will invoke other listeners). This is to try to have one class know all the various settings that affect auto time / geolocation detection behavior, which has become more complicated with the user-scoped geolocation config. The TimeZoneDetectorInternal has some listener methods added in preparation for later steps. 3) The new "geolocation detection enabled" setting is added. This allows users to selectively disable geolocation time zone detection independently of the location toggle. 4) The TimeZoneConfiguration now carries the ID of the user it is for, since it now contains user-specific state. 5) Internally, the TimeZoneConfiguration is replaced by the ConfigurationInternal class, which includes all properties (not just user visible settings) that influence time zone behavior on the device. The ConfigurationInternal generates the TimeZoneCapabilities and TimeZoneConfiguration. 6) In a change from the initial behavior, any previously received geolocation suggestion is cleared if geolocation detection is turned off (or the current user switches to one that has it turned off), and suggestions received when geolocation detection is off are ignored. This is trying to be careful to avoid caching the geolocation timezone if the user has signalled they don't want their device to use it. Test: atest services/tests/servicestests/src/com/android/server/timezonedetector Test: atest core/tests/coretests/src/android/app/timezonedetector Bug: 149014708 Change-Id: Ieac9f8f646e16edcb8b6ea6c8749e45761898ac1 --- .../ITimeZoneConfigurationListener.aidl | 2 +- .../ITimeZoneDetectorService.aidl | 6 +- .../TimeZoneCapabilities.java | 109 ++- .../TimeZoneConfiguration.java | 150 ++-- .../timezonedetector/TimeZoneDetector.java | 42 +- .../TimeZoneDetectorImpl.java | 21 +- core/java/android/provider/Settings.java | 11 + .../TimeZoneCapabilitiesTest.java | 84 ++- .../TimeZoneConfigurationTest.java | 53 +- .../CallerIdentityInjector.java | 62 ++ .../ConfigurationChangeListener.java | 26 + .../ConfigurationInternal.java | 288 ++++++++ .../TimeZoneDetectorCallbackImpl.java | 213 +++--- .../TimeZoneDetectorInternal.java | 6 + .../TimeZoneDetectorInternalImpl.java | 37 +- .../TimeZoneDetectorService.java | 184 ++--- .../TimeZoneDetectorStrategy.java | 88 ++- .../TimeZoneDetectorStrategyImpl.java | 282 +++---- .../ConfigurationInternalTest.java | 182 +++++ .../FakeTimeZoneDetectorStrategy.java | 79 +- .../TestCallerIdentityInjector.java | 53 ++ .../TimeZoneDetectorInternalImplTest.java | 13 + .../TimeZoneDetectorServiceTest.java | 95 +-- .../TimeZoneDetectorStrategyImplTest.java | 691 ++++++------------ 24 files changed, 1636 insertions(+), 1141 deletions(-) create mode 100644 services/core/java/com/android/server/timezonedetector/CallerIdentityInjector.java create mode 100644 services/core/java/com/android/server/timezonedetector/ConfigurationChangeListener.java create mode 100644 services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java create mode 100644 services/tests/servicestests/src/com/android/server/timezonedetector/ConfigurationInternalTest.java create mode 100644 services/tests/servicestests/src/com/android/server/timezonedetector/TestCallerIdentityInjector.java diff --git a/core/java/android/app/timezonedetector/ITimeZoneConfigurationListener.aidl b/core/java/android/app/timezonedetector/ITimeZoneConfigurationListener.aidl index af77fe05b9026..6d0fe72b9de12 100644 --- a/core/java/android/app/timezonedetector/ITimeZoneConfigurationListener.aidl +++ b/core/java/android/app/timezonedetector/ITimeZoneConfigurationListener.aidl @@ -20,5 +20,5 @@ import android.app.timezonedetector.TimeZoneConfiguration; /** {@hide} */ oneway interface ITimeZoneConfigurationListener { - void onChange(in TimeZoneConfiguration configuration); + void onChange(); } \ No newline at end of file diff --git a/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl b/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl index 6e93af6a053b4..4f7e1f62928a7 100644 --- a/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl +++ b/core/java/android/app/timezonedetector/ITimeZoneDetectorService.aidl @@ -32,17 +32,15 @@ import android.app.timezonedetector.TimeZoneConfiguration; * this Binder interface directly. See {@link android.app.timezonedetector.TimeZoneDetectorService} * for more complete documentation. * - * * {@hide} */ interface ITimeZoneDetectorService { TimeZoneCapabilities getCapabilities(); - - TimeZoneConfiguration getConfiguration(); - boolean updateConfiguration(in TimeZoneConfiguration configuration); void addConfigurationListener(ITimeZoneConfigurationListener listener); void removeConfigurationListener(ITimeZoneConfigurationListener listener); + boolean updateConfiguration(in TimeZoneConfiguration configuration); + boolean suggestManualTimeZone(in ManualTimeZoneSuggestion timeZoneSuggestion); void suggestTelephonyTimeZone(in TelephonyTimeZoneSuggestion timeZoneSuggestion); } diff --git a/core/java/android/app/timezonedetector/TimeZoneCapabilities.java b/core/java/android/app/timezonedetector/TimeZoneCapabilities.java index cc0af3f97e495..09fffe9f4f250 100644 --- a/core/java/android/app/timezonedetector/TimeZoneCapabilities.java +++ b/core/java/android/app/timezonedetector/TimeZoneCapabilities.java @@ -16,9 +16,12 @@ package android.app.timezonedetector; +import static android.app.timezonedetector.TimeZoneConfiguration.SETTING_AUTO_DETECTION_ENABLED; +import static android.app.timezonedetector.TimeZoneConfiguration.SETTING_GEO_DETECTION_ENABLED; + import android.annotation.IntDef; import android.annotation.NonNull; -import android.annotation.UserIdInt; +import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; @@ -38,9 +41,9 @@ import java.util.Objects; * *

Actions have associated methods, see the documentation for each action for details. * - *

For configuration capabilities, the associated current configuration value can be retrieved - * using {@link TimeZoneDetector#getConfiguration()} and may be changed using - * {@link TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)}. + *

For configuration settings capabilities, the associated settings value can be found via + * {@link #getConfiguration()} and may be changed using {@link + * TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)} (if the user's capabilities allow). * *

Note: Capabilities are independent of app permissions required to call the associated APIs. * @@ -60,7 +63,8 @@ public final class TimeZoneCapabilities implements Parcelable { public static final int CAPABILITY_NOT_SUPPORTED = 10; /** - * Indicates that a capability is supported on this device, but not allowed for the user. + * Indicates that a capability is supported on this device, but not allowed for the user, e.g. + * if the capability relates to the ability to modify settings the user is not able to. * This could be because of the user's type (e.g. maybe it applies to the primary user only) or * device policy. Depending on the capability, this could mean the associated UI * should be hidden, or displayed but disabled. @@ -68,9 +72,11 @@ public final class TimeZoneCapabilities implements Parcelable { public static final int CAPABILITY_NOT_ALLOWED = 20; /** - * Indicates that a capability is possessed but not applicable, e.g. if it is configuration, - * the current configuration or device state renders it irrelevant. The associated UI may be - * hidden, disabled, or left visible (but ineffective) depending on requirements. + * Indicates that a capability is possessed but not currently applicable, e.g. if the + * capability relates to the ability to modify settings, the user has the ability to modify + * it, but it is currently rendered irrelevant by other settings or other device state (flags, + * resource config, etc.). The associated UI may be hidden, disabled, or left visible (but + * ineffective) depending on requirements. */ public static final int CAPABILITY_NOT_APPLICABLE = 30; @@ -89,13 +95,13 @@ public final class TimeZoneCapabilities implements Parcelable { }; - private final @UserIdInt int mUserId; + @NonNull private final TimeZoneConfiguration mConfiguration; 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.mConfiguration = Objects.requireNonNull(builder.mConfiguration); this.mConfigureAutoDetectionEnabled = builder.mConfigureAutoDetectionEnabled; this.mConfigureGeoDetectionEnabled = builder.mConfigureGeoDetectionEnabled; this.mSuggestManualTimeZone = builder.mSuggestManualTimeZone; @@ -103,7 +109,8 @@ public final class TimeZoneCapabilities implements Parcelable { @NonNull private static TimeZoneCapabilities createFromParcel(Parcel in) { - return new TimeZoneCapabilities.Builder(in.readInt()) + return new TimeZoneCapabilities.Builder() + .setConfiguration(in.readParcelable(null)) .setConfigureAutoDetectionEnabled(in.readInt()) .setConfigureGeoDetectionEnabled(in.readInt()) .setSuggestManualTimeZone(in.readInt()) @@ -112,21 +119,24 @@ public final class TimeZoneCapabilities implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeInt(mUserId); + dest.writeParcelable(mConfiguration, flags); dest.writeInt(mConfigureAutoDetectionEnabled); dest.writeInt(mConfigureGeoDetectionEnabled); dest.writeInt(mSuggestManualTimeZone); } - /** Returns the user ID the capabilities are for. */ - public @UserIdInt int getUserId() { - return mUserId; + /** + * Returns the user's time zone behavior configuration. + */ + public @NonNull TimeZoneConfiguration getConfiguration() { + return mConfiguration; } /** - * Returns the user's capability state for controlling whether automatic time zone detection is - * enabled via {@link TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)} and {@link - * TimeZoneConfiguration#isAutoDetectionEnabled()}. + * Returns the capability state associated with the user's ability to modify the automatic time + * zone detection setting. The setting can be updated via {@link + * TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)} and accessed via {@link + * #getConfiguration()}. */ @CapabilityState public int getConfigureAutoDetectionEnabled() { @@ -134,9 +144,10 @@ public final class TimeZoneCapabilities implements Parcelable { } /** - * 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()}. + * Returns the capability state associated with the user's ability to modify the geolocation + * detection setting. The setting can be updated via {@link + * TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)} and accessed via {@link + * #getConfiguration()}. */ @CapabilityState public int getConfigureGeoDetectionEnabled() { @@ -144,8 +155,8 @@ public final class TimeZoneCapabilities implements Parcelable { } /** - * Returns the user's capability state for manually setting the time zone on a device via - * {@link TimeZoneDetector#suggestManualTimeZone(ManualTimeZoneSuggestion)}. + * Returns the capability state associated with the user's ability to manually set the time zone + * on a device via {@link TimeZoneDetector#suggestManualTimeZone(ManualTimeZoneSuggestion)}. * *

The suggestion will be ignored in all cases unless the value is {@link * #CAPABILITY_POSSESSED}. See also {@link TimeZoneConfiguration#isAutoDetectionEnabled()}. @@ -155,6 +166,38 @@ public final class TimeZoneCapabilities implements Parcelable { return mSuggestManualTimeZone; } + /** + * Constructs a new {@link TimeZoneConfiguration} from an {@code oldConfiguration} and a set of + * {@code requestedChanges}, if the current capabilities allow. The new configuration is + * returned and the capabilities are left unchanged. If the capabilities do not permit one or + * more of the changes then {@code null} is returned. + */ + @Nullable + public TimeZoneConfiguration applyUpdate(TimeZoneConfiguration requestedChanges) { + if (requestedChanges.getUserId() != mConfiguration.getUserId()) { + throw new IllegalArgumentException("User does not match:" + + " this=" + mConfiguration + ", other=" + requestedChanges); + } + + TimeZoneConfiguration.Builder newConfigBuilder = + new TimeZoneConfiguration.Builder(mConfiguration); + if (requestedChanges.hasSetting(SETTING_AUTO_DETECTION_ENABLED)) { + if (getConfigureAutoDetectionEnabled() < CAPABILITY_NOT_APPLICABLE) { + return null; + } + newConfigBuilder.setAutoDetectionEnabled(requestedChanges.isAutoDetectionEnabled()); + } + + if (requestedChanges.hasSetting(SETTING_GEO_DETECTION_ENABLED)) { + if (getConfigureGeoDetectionEnabled() < CAPABILITY_NOT_APPLICABLE) { + return null; + } + newConfigBuilder.setGeoDetectionEnabled(requestedChanges.isGeoDetectionEnabled()); + } + + return newConfigBuilder.build(); + } + @Override public int describeContents() { return 0; @@ -169,7 +212,7 @@ public final class TimeZoneCapabilities implements Parcelable { return false; } TimeZoneCapabilities that = (TimeZoneCapabilities) o; - return mUserId == that.mUserId + return Objects.equals(mConfiguration, that.mConfiguration) && mConfigureAutoDetectionEnabled == that.mConfigureAutoDetectionEnabled && mConfigureGeoDetectionEnabled == that.mConfigureGeoDetectionEnabled && mSuggestManualTimeZone == that.mSuggestManualTimeZone; @@ -177,7 +220,7 @@ public final class TimeZoneCapabilities implements Parcelable { @Override public int hashCode() { - return Objects.hash(mUserId, + return Objects.hash(mConfiguration, mConfigureAutoDetectionEnabled, mConfigureGeoDetectionEnabled, mSuggestManualTimeZone); @@ -186,7 +229,7 @@ public final class TimeZoneCapabilities implements Parcelable { @Override public String toString() { return "TimeZoneDetectorCapabilities{" - + "mUserId=" + mUserId + + "mConfiguration=" + mConfiguration + ", mConfigureAutomaticDetectionEnabled=" + mConfigureAutoDetectionEnabled + ", mConfigureGeoDetectionEnabled=" + mConfigureGeoDetectionEnabled + ", mSuggestManualTimeZone=" + mSuggestManualTimeZone @@ -196,16 +239,18 @@ public final class TimeZoneCapabilities implements Parcelable { /** @hide */ public static class Builder { - private final @UserIdInt int mUserId; + private TimeZoneConfiguration mConfiguration; private @CapabilityState int mConfigureAutoDetectionEnabled; private @CapabilityState int mConfigureGeoDetectionEnabled; private @CapabilityState int mSuggestManualTimeZone; - /** - * Creates a new Builder with no properties set. - */ - public Builder(@UserIdInt int userId) { - mUserId = userId; + /** Sets the user-visible configuration settings. */ + public Builder setConfiguration(@NonNull TimeZoneConfiguration configuration) { + if (!configuration.isComplete()) { + throw new IllegalArgumentException(configuration + " is not complete"); + } + this.mConfiguration = configuration; + return this; } /** Sets the state for the automatic time zone detection enabled config. */ diff --git a/core/java/android/app/timezonedetector/TimeZoneConfiguration.java b/core/java/android/app/timezonedetector/TimeZoneConfiguration.java index 6f84ee22a985a..95db0a26cc6e9 100644 --- a/core/java/android/app/timezonedetector/TimeZoneConfiguration.java +++ b/core/java/android/app/timezonedetector/TimeZoneConfiguration.java @@ -18,6 +18,7 @@ package android.app.timezonedetector; import android.annotation.NonNull; import android.annotation.StringDef; +import android.annotation.UserIdInt; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -27,21 +28,20 @@ import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** - * Configuration that controls the behavior of the time zone detector associated with a specific - * user. + * User visible settings that control the behavior of the time zone detector / manual time zone + * entry. * - *

Configuration consists of a set of known properties. When reading configuration via - * {@link TimeZoneDetector#getConfiguration()} values for all known properties will be provided. In - * some cases, such as when the configuration relies on optional hardware, the values may be - * meaningless / defaulted to safe values. + *

When reading the configuration, values for all settings will be provided. In some cases, such + * as when the device behavior relies on optional hardware / OEM configuration, or the value of + * several settings, the device behavior may not be directly affected by the setting value. * - *

Configuration properties can be left absent when updating configuration via {@link - * TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)} and those values will not be - * changed. Not all configuration properties can be modified by all users. See {@link - * TimeZoneDetector#getCapabilities()} and {@link TimeZoneCapabilities}. + *

Settings can be left absent when updating configuration via {@link + * TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)} and those settings will not be + * changed. Not all configuration settings can be modified by all users: see {@link + * TimeZoneDetector#getCapabilities()} and {@link TimeZoneCapabilities} for details. * - *

See {@link #isComplete()} to tell if all known properties are present, and {@link - * #hasProperty(String)} with {@code PROPERTY_} constants for testing individual properties. + *

See {@link #hasSetting(String)} with {@code PROPERTY_} constants for testing for the presence + * of individual settings. * * @hide */ @@ -59,80 +59,82 @@ public final class TimeZoneConfiguration implements Parcelable { }; /** All configuration properties */ - @StringDef(PROPERTY_AUTO_DETECTION_ENABLED) + @StringDef({ SETTING_AUTO_DETECTION_ENABLED, SETTING_GEO_DETECTION_ENABLED }) @Retention(RetentionPolicy.SOURCE) - @interface Property {} + @interface Setting {} /** See {@link TimeZoneConfiguration#isAutoDetectionEnabled()} for details. */ - @Property - public static final String PROPERTY_AUTO_DETECTION_ENABLED = "autoDetectionEnabled"; + @Setting + public static final String SETTING_AUTO_DETECTION_ENABLED = "autoDetectionEnabled"; /** See {@link TimeZoneConfiguration#isGeoDetectionEnabled()} for details. */ - @Property - public static final String PROPERTY_GEO_DETECTION_ENABLED = "geoDetectionEnabled"; + @Setting + public static final String SETTING_GEO_DETECTION_ENABLED = "geoDetectionEnabled"; - private final Bundle mBundle; + private final @UserIdInt int mUserId; + @NonNull private final Bundle mBundle; private TimeZoneConfiguration(Builder builder) { - this.mBundle = builder.mBundle; + this.mUserId = builder.mUserId; + this.mBundle = Objects.requireNonNull(builder.mBundle); } private static TimeZoneConfiguration createFromParcel(Parcel in) { - return new TimeZoneConfiguration.Builder() + return new TimeZoneConfiguration.Builder(in.readInt()) .setPropertyBundleInternal(in.readBundle()) .build(); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mUserId); dest.writeBundle(mBundle); } - /** Returns {@code true} if all known properties are set. */ - public boolean isComplete() { - return hasProperty(PROPERTY_AUTO_DETECTION_ENABLED) - && hasProperty(PROPERTY_GEO_DETECTION_ENABLED); + /** Returns the ID of the user this configuration is associated with. */ + public @UserIdInt int getUserId() { + return mUserId; } - /** Returns true if the specified property is set. */ - public boolean hasProperty(@Property String property) { - return mBundle.containsKey(property); + /** Returns {@code true} if all known settings are present. */ + public boolean isComplete() { + return hasSetting(SETTING_AUTO_DETECTION_ENABLED) + && hasSetting(SETTING_GEO_DETECTION_ENABLED); + } + + /** Returns true if the specified setting is set. */ + public boolean hasSetting(@Setting String setting) { + return mBundle.containsKey(setting); } /** - * Returns the value of the {@link #PROPERTY_AUTO_DETECTION_ENABLED} property. This + * Returns the value of the {@link #SETTING_AUTO_DETECTION_ENABLED} setting. This * controls whether a device will attempt to determine the time zone automatically using - * contextual information. + * contextual information if the device supports auto detection. * - * @throws IllegalStateException if the field has not been set + *

This setting is global and can be updated by some users. + * + * @throws IllegalStateException if the setting has not been set */ public boolean isAutoDetectionEnabled() { - if (!mBundle.containsKey(PROPERTY_AUTO_DETECTION_ENABLED)) { - throw new IllegalStateException(PROPERTY_AUTO_DETECTION_ENABLED + " is not set"); - } - return mBundle.getBoolean(PROPERTY_AUTO_DETECTION_ENABLED); + enforceSettingPresent(SETTING_AUTO_DETECTION_ENABLED); + return mBundle.getBoolean(SETTING_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. + * Returns the value of the {@link #SETTING_GEO_DETECTION_ENABLED} setting. This + * controls whether a device can use geolocation to determine time zone. Only used when + * {@link #isAutoDetectionEnabled()} is {@code true} and when the user has allowed their + * location to be used. * - * @throws IllegalStateException if the field has not been set + *

This setting is user-scoped and can be updated by some users. + * See {@link TimeZoneCapabilities#getConfigureGeoDetectionEnabled()}. + * + * @throws IllegalStateException if the setting 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(); + enforceSettingPresent(SETTING_GEO_DETECTION_ENABLED); + return mBundle.getBoolean(SETTING_GEO_DETECTION_ENABLED); } @Override @@ -149,43 +151,61 @@ public final class TimeZoneConfiguration implements Parcelable { return false; } TimeZoneConfiguration that = (TimeZoneConfiguration) o; - return mBundle.kindofEquals(that.mBundle); + return mUserId == that.mUserId + && mBundle.kindofEquals(that.mBundle); } @Override public int hashCode() { - return Objects.hash(mBundle); + return Objects.hash(mUserId, mBundle); } @Override public String toString() { return "TimeZoneDetectorConfiguration{" + + "mUserId=" + mUserId + "mBundle=" + mBundle + '}'; } + private void enforceSettingPresent(@Setting String setting) { + if (!mBundle.containsKey(setting)) { + throw new IllegalStateException(setting + " is not set"); + } + } + /** @hide */ public static class Builder { - private Bundle mBundle = new Bundle(); + private final @UserIdInt int mUserId; + private final Bundle mBundle = new Bundle(); /** - * Creates a new Builder with no properties set. + * Creates a new Builder for a userId with no settings held. */ - public Builder() {} + public Builder(@UserIdInt int userId) { + mUserId = userId; + } /** - * Creates a new Builder by copying properties from an existing instance. + * Creates a new Builder by copying the user ID and settings from an existing instance. */ public Builder(TimeZoneConfiguration toCopy) { + this.mUserId = toCopy.mUserId; mergeProperties(toCopy); } /** - * Merges {@code other} properties into this instances, replacing existing values in this - * where the properties appear in both. + * Merges {@code other} settings into this instances, replacing existing values in this + * where the settings appear in both. */ public Builder mergeProperties(TimeZoneConfiguration other) { + if (mUserId != other.mUserId) { + throw new IllegalArgumentException( + "Cannot merge configurations for different user IDs." + + " this.mUserId=" + this.mUserId + + ", other.mUserId=" + other.mUserId); + } this.mBundle.putAll(other.mBundle); return this; } @@ -195,15 +215,19 @@ public final class TimeZoneConfiguration implements Parcelable { return this; } - /** Sets the desired state of the automatic time zone detection property. */ + /** + * Sets the state of the {@link #SETTING_AUTO_DETECTION_ENABLED} setting. + */ public Builder setAutoDetectionEnabled(boolean enabled) { - this.mBundle.putBoolean(PROPERTY_AUTO_DETECTION_ENABLED, enabled); + this.mBundle.putBoolean(SETTING_AUTO_DETECTION_ENABLED, enabled); return this; } - /** Sets the desired state of the geolocation time zone detection enabled property. */ + /** + * Sets the state of the {@link #SETTING_GEO_DETECTION_ENABLED} setting. + */ public Builder setGeoDetectionEnabled(boolean enabled) { - this.mBundle.putBoolean(PROPERTY_GEO_DETECTION_ENABLED, enabled); + this.mBundle.putBoolean(SETTING_GEO_DETECTION_ENABLED, enabled); return this; } diff --git a/core/java/android/app/timezonedetector/TimeZoneDetector.java b/core/java/android/app/timezonedetector/TimeZoneDetector.java index 7885613bfb59c..2b1cbf259c550 100644 --- a/core/java/android/app/timezonedetector/TimeZoneDetector.java +++ b/core/java/android/app/timezonedetector/TimeZoneDetector.java @@ -36,38 +36,30 @@ public interface TimeZoneDetector { @NonNull TimeZoneCapabilities getCapabilities(); - /** - * Returns the current user's complete time zone configuration. See {@link - * TimeZoneConfiguration}. - */ - @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) - @NonNull - TimeZoneConfiguration getConfiguration(); - /** * Modifies the time zone detection configuration. * - *

Configuration properties vary in scope: some may be device-wide, others may be specific to - * the current user. + *

Configuration settings vary in scope: some may be global (affect all users), others may be + * specific to the current user. * - *

The ability to modify configuration properties can be subject to restrictions. For + *

The ability to modify configuration settings can be subject to restrictions. For * example, they may be determined by device hardware, general policy (i.e. only the primary - * user can set them), or by a managed device policy. See {@link #getCapabilities()} to obtain + * user can set them), or by a managed device policy. Use {@link #getCapabilities()} to obtain * information at runtime about the user's capabilities. * - *

Attempts to set configuration with capabilities that are {@link + *

Attempts to modify configuration settings with capabilities that are {@link * TimeZoneCapabilities#CAPABILITY_NOT_SUPPORTED} or {@link * TimeZoneCapabilities#CAPABILITY_NOT_ALLOWED} will have no effect and a {@code false} - * will be returned. Setting configuration with capabilities that are {@link + * will be returned. Modifying configuration settings with capabilities that are {@link * TimeZoneCapabilities#CAPABILITY_NOT_APPLICABLE} or {@link * TimeZoneCapabilities#CAPABILITY_POSSESSED} will succeed. See {@link * TimeZoneCapabilities} for further details. * - *

If the configuration is not "complete", then only the specified properties will be - * updated (where the user's capabilities allow) and other settings will be left unchanged. See - * {@link TimeZoneConfiguration#isComplete()}. + *

If the supplied configuration only has some values set, then only the specified settings + * will be updated (where the user's capabilities allow) and other settings will be left + * unchanged. * - * @return {@code true} if all the configuration properties specified have been set to the + * @return {@code true} if all the configuration settings specified have been set to the * new values, {@code false} if none have */ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) @@ -76,14 +68,20 @@ public interface TimeZoneDetector { /** * An interface that can be used to listen for changes to the time zone detector configuration. */ + @FunctionalInterface interface TimeZoneConfigurationListener { - /** Called when the configuration changes. There are no guarantees about the thread used. */ - void onChange(@NonNull TimeZoneConfiguration configuration); + /** + * Called when something about the time zone configuration on the device has changed. + * This could be because the current user has changed, one of the device's relevant settings + * has changed, or something that could affect a user's capabilities has changed. + * There are no guarantees about the thread used. + */ + void onChange(); } /** - * Registers a listener that will be informed when the configuration changes. The complete - * configuration is passed to the listener, not just the properties that have changed. + * Registers a listener that will be informed when something about the time zone configuration + * changes. */ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) void addConfigurationListener(@NonNull TimeZoneConfigurationListener listener); diff --git a/core/java/android/app/timezonedetector/TimeZoneDetectorImpl.java b/core/java/android/app/timezonedetector/TimeZoneDetectorImpl.java index 0770aff4e9bb6..4c69732abec9c 100644 --- a/core/java/android/app/timezonedetector/TimeZoneDetectorImpl.java +++ b/core/java/android/app/timezonedetector/TimeZoneDetectorImpl.java @@ -56,19 +56,6 @@ public final class TimeZoneDetectorImpl implements TimeZoneDetector { } } - @Override - @NonNull - public TimeZoneConfiguration getConfiguration() { - if (DEBUG) { - Log.d(TAG, "getConfiguration called"); - } - try { - return mITimeZoneDetectorService.getConfiguration(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - @Override public boolean updateConfiguration(@NonNull TimeZoneConfiguration configuration) { if (DEBUG) { @@ -94,8 +81,8 @@ public final class TimeZoneDetectorImpl implements TimeZoneDetector { ITimeZoneConfigurationListener iListener = new ITimeZoneConfigurationListener.Stub() { @Override - public void onChange(@NonNull TimeZoneConfiguration configuration) { - notifyConfigurationListeners(configuration); + public void onChange() { + notifyConfigurationListeners(); } }; mConfigurationReceiver = iListener; @@ -116,14 +103,14 @@ public final class TimeZoneDetectorImpl implements TimeZoneDetector { } } - private void notifyConfigurationListeners(@NonNull TimeZoneConfiguration configuration) { + private void notifyConfigurationListeners() { final ArraySet configurationListeners; synchronized (this) { configurationListeners = new ArraySet<>(mConfigurationListeners); } int size = configurationListeners.size(); for (int i = 0; i < size; i++) { - configurationListeners.valueAt(i).onChange(configuration); + configurationListeners.valueAt(i).onChange(); } } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 660455e59a4a5..d3cb30277738a 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6402,6 +6402,17 @@ public final class Settings { @SystemApi public static final int LOCATION_MODE_ON = LOCATION_MODE_HIGH_ACCURACY; + /** + * The current location time zone detection enabled state for the user. + * + * See {@link + * android.app.timezonedetector.TimeZoneDetector#getCapabilities} for access. See {@link + * android.app.timezonedetector.TimeZoneDetector#updateConfiguration} to update. + * @hide + */ + public static final String LOCATION_TIME_ZONE_DETECTION_ENABLED = + "location_time_zone_detection_enabled"; + /** * The accuracy in meters used for coarsening location for clients with only the coarse * location permission. diff --git a/core/tests/coretests/src/android/app/timezonedetector/TimeZoneCapabilitiesTest.java b/core/tests/coretests/src/android/app/timezonedetector/TimeZoneCapabilitiesTest.java index 72391f4d7dec9..db127c6cb9ed9 100644 --- a/core/tests/coretests/src/android/app/timezonedetector/TimeZoneCapabilitiesTest.java +++ b/core/tests/coretests/src/android/app/timezonedetector/TimeZoneCapabilitiesTest.java @@ -22,6 +22,7 @@ import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_POSSE import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; import org.junit.Test; @@ -31,11 +32,22 @@ public class TimeZoneCapabilitiesTest { @Test public void testEquals() { - TimeZoneCapabilities.Builder builder1 = new TimeZoneCapabilities.Builder(ARBITRARY_USER_ID) + TimeZoneConfiguration configuration1 = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) + .setAutoDetectionEnabled(true) + .setGeoDetectionEnabled(true) + .build(); + TimeZoneConfiguration configuration2 = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) + .setAutoDetectionEnabled(false) + .setGeoDetectionEnabled(false) + .build(); + + TimeZoneCapabilities.Builder builder1 = new TimeZoneCapabilities.Builder() + .setConfiguration(configuration1) .setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED) .setConfigureGeoDetectionEnabled(CAPABILITY_POSSESSED) .setSuggestManualTimeZone(CAPABILITY_POSSESSED); - TimeZoneCapabilities.Builder builder2 = new TimeZoneCapabilities.Builder(ARBITRARY_USER_ID) + TimeZoneCapabilities.Builder builder2 = new TimeZoneCapabilities.Builder() + .setConfiguration(configuration1) .setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED) .setConfigureGeoDetectionEnabled(CAPABILITY_POSSESSED) .setSuggestManualTimeZone(CAPABILITY_POSSESSED); @@ -45,6 +57,20 @@ public class TimeZoneCapabilitiesTest { assertEquals(one, two); } + builder2.setConfiguration(configuration2); + { + TimeZoneCapabilities one = builder1.build(); + TimeZoneCapabilities two = builder2.build(); + assertNotEquals(one, two); + } + + builder1.setConfiguration(configuration2); + { + TimeZoneCapabilities one = builder1.build(); + TimeZoneCapabilities two = builder2.build(); + assertEquals(one, two); + } + builder2.setConfigureAutoDetectionEnabled(CAPABILITY_NOT_ALLOWED); { TimeZoneCapabilities one = builder1.build(); @@ -90,7 +116,12 @@ public class TimeZoneCapabilitiesTest { @Test public void testParcelable() { - TimeZoneCapabilities.Builder builder = new TimeZoneCapabilities.Builder(ARBITRARY_USER_ID) + TimeZoneConfiguration configuration = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) + .setAutoDetectionEnabled(true) + .setGeoDetectionEnabled(true) + .build(); + TimeZoneCapabilities.Builder builder = new TimeZoneCapabilities.Builder() + .setConfiguration(configuration) .setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED) .setConfigureGeoDetectionEnabled(CAPABILITY_POSSESSED) .setSuggestManualTimeZone(CAPABILITY_POSSESSED); @@ -105,4 +136,51 @@ public class TimeZoneCapabilitiesTest { builder.setSuggestManualTimeZone(CAPABILITY_NOT_ALLOWED); assertRoundTripParcelable(builder.build()); } + + @Test + public void testApplyUpdate_permitted() { + TimeZoneConfiguration oldConfiguration = + new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) + .setAutoDetectionEnabled(true) + .setGeoDetectionEnabled(true) + .build(); + TimeZoneCapabilities capabilities = new TimeZoneCapabilities.Builder() + .setConfiguration(oldConfiguration) + .setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED) + .setConfigureGeoDetectionEnabled(CAPABILITY_POSSESSED) + .setSuggestManualTimeZone(CAPABILITY_POSSESSED) + .build(); + assertEquals(oldConfiguration, capabilities.getConfiguration()); + + TimeZoneConfiguration configChange = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) + .setAutoDetectionEnabled(false) + .build(); + + TimeZoneConfiguration expected = new TimeZoneConfiguration.Builder(oldConfiguration) + .setAutoDetectionEnabled(false) + .build(); + assertEquals(expected, capabilities.applyUpdate(configChange)); + } + + @Test + public void testApplyUpdate_notPermitted() { + TimeZoneConfiguration oldConfiguration = + new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) + .setAutoDetectionEnabled(true) + .setGeoDetectionEnabled(true) + .build(); + TimeZoneCapabilities capabilities = new TimeZoneCapabilities.Builder() + .setConfiguration(oldConfiguration) + .setConfigureAutoDetectionEnabled(CAPABILITY_NOT_ALLOWED) + .setConfigureGeoDetectionEnabled(CAPABILITY_NOT_ALLOWED) + .setSuggestManualTimeZone(CAPABILITY_NOT_ALLOWED) + .build(); + assertEquals(oldConfiguration, capabilities.getConfiguration()); + + TimeZoneConfiguration configChange = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) + .setAutoDetectionEnabled(false) + .build(); + + assertNull(capabilities.applyUpdate(configChange)); + } } diff --git a/core/tests/coretests/src/android/app/timezonedetector/TimeZoneConfigurationTest.java b/core/tests/coretests/src/android/app/timezonedetector/TimeZoneConfigurationTest.java index 00dc73ed269fd..faf908de8d4aa 100644 --- a/core/tests/coretests/src/android/app/timezonedetector/TimeZoneConfigurationTest.java +++ b/core/tests/coretests/src/android/app/timezonedetector/TimeZoneConfigurationTest.java @@ -27,11 +27,14 @@ import org.junit.Test; public class TimeZoneConfigurationTest { + private static final int ARBITRARY_USER_ID = 9876; + @Test public void testBuilder_copyConstructor() { - TimeZoneConfiguration.Builder builder1 = new TimeZoneConfiguration.Builder() - .setAutoDetectionEnabled(true) - .setGeoDetectionEnabled(true); + TimeZoneConfiguration.Builder builder1 = + new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) + .setAutoDetectionEnabled(true) + .setGeoDetectionEnabled(true); TimeZoneConfiguration configuration1 = builder1.build(); TimeZoneConfiguration configuration2 = @@ -41,28 +44,28 @@ public class TimeZoneConfigurationTest { } @Test - public void testIsComplete() { - TimeZoneConfiguration.Builder builder = - new TimeZoneConfiguration.Builder(); - assertFalse(builder.build().isComplete()); + public void testIntrospectionMethods() { + TimeZoneConfiguration empty = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID).build(); + assertFalse(empty.isComplete()); + assertFalse(empty.hasSetting(TimeZoneConfiguration.SETTING_AUTO_DETECTION_ENABLED)); - builder.setAutoDetectionEnabled(true); - assertFalse(builder.build().isComplete()); - - builder.setGeoDetectionEnabled(true); - assertTrue(builder.build().isComplete()); + TimeZoneConfiguration completeConfig = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) + .setAutoDetectionEnabled(true) + .setGeoDetectionEnabled(true) + .build(); + assertTrue(completeConfig.isComplete()); + assertTrue(completeConfig.hasSetting(TimeZoneConfiguration.SETTING_AUTO_DETECTION_ENABLED)); } @Test public void testBuilder_mergeProperties() { - TimeZoneConfiguration configuration1 = - new TimeZoneConfiguration.Builder() - .setAutoDetectionEnabled(true) - .build(); + TimeZoneConfiguration configuration1 = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) + .setAutoDetectionEnabled(true) + .build(); { TimeZoneConfiguration mergedEmptyAnd1 = - new TimeZoneConfiguration.Builder() + new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) .mergeProperties(configuration1) .build(); assertEquals(configuration1, mergedEmptyAnd1); @@ -70,7 +73,7 @@ public class TimeZoneConfigurationTest { { TimeZoneConfiguration configuration2 = - new TimeZoneConfiguration.Builder() + new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) .setAutoDetectionEnabled(false) .build(); @@ -87,14 +90,22 @@ public class TimeZoneConfigurationTest { @Test public void testEquals() { TimeZoneConfiguration.Builder builder1 = - new TimeZoneConfiguration.Builder(); + new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID); { TimeZoneConfiguration one = builder1.build(); assertEquals(one, one); } + { + TimeZoneConfiguration.Builder differentUserBuilder = + new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID + 1); + TimeZoneConfiguration one = builder1.build(); + TimeZoneConfiguration two = differentUserBuilder.build(); + assertNotEquals(one, two); + } + TimeZoneConfiguration.Builder builder2 = - new TimeZoneConfiguration.Builder(); + new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID); { TimeZoneConfiguration one = builder1.build(); TimeZoneConfiguration two = builder2.build(); @@ -148,7 +159,7 @@ public class TimeZoneConfigurationTest { @Test public void testParcelable() { TimeZoneConfiguration.Builder builder = - new TimeZoneConfiguration.Builder(); + new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID); assertRoundTripParcelable(builder.build()); builder.setAutoDetectionEnabled(true); diff --git a/services/core/java/com/android/server/timezonedetector/CallerIdentityInjector.java b/services/core/java/com/android/server/timezonedetector/CallerIdentityInjector.java new file mode 100644 index 0000000000000..1500cfaeb3a24 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/CallerIdentityInjector.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import android.annotation.UserIdInt; +import android.os.Binder; +import android.os.UserHandle; + +/** + * An interface to wrap various difficult-to-intercept calls that services make to access / manage + * caller identity, e.g. {@link Binder#clearCallingIdentity()}. + */ +public interface CallerIdentityInjector { + + /** A singleton for the real implementation of {@link CallerIdentityInjector}. */ + CallerIdentityInjector REAL = new Real(); + + /** A {@link UserHandle#getCallingUserId()} call. */ + @UserIdInt int getCallingUserId(); + + /** A {@link Binder#clearCallingIdentity()} call. */ + long clearCallingIdentity(); + + /** A {@link Binder#restoreCallingIdentity(long)} ()} call. */ + void restoreCallingIdentity(long token); + + /** The real implementation of {@link CallerIdentityInjector}. */ + class Real implements CallerIdentityInjector { + + protected Real() { + } + + @Override + public int getCallingUserId() { + return UserHandle.getCallingUserId(); + } + + @Override + public long clearCallingIdentity() { + return Binder.clearCallingIdentity(); + } + + @Override + public void restoreCallingIdentity(long token) { + Binder.restoreCallingIdentity(token); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/ConfigurationChangeListener.java b/services/core/java/com/android/server/timezonedetector/ConfigurationChangeListener.java new file mode 100644 index 0000000000000..4c7b1f38dd5ae --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/ConfigurationChangeListener.java @@ -0,0 +1,26 @@ +/* + * 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 com.android.server.timezonedetector; + +/** + * A listener used to receive notification that time zone configuration has changed. + */ +@FunctionalInterface +public interface ConfigurationChangeListener { + /** Called when the current user or a configuration value has changed. */ + void onChange(); +} diff --git a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java new file mode 100644 index 0000000000000..aee3d8d3499bf --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_ALLOWED; +import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_APPLICABLE; +import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_SUPPORTED; +import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_POSSESSED; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.timezonedetector.TimeZoneCapabilities; +import android.app.timezonedetector.TimeZoneConfiguration; + +import java.util.Objects; + +/** + * Holds all configuration values that affect time zone behavior and some associated logic, e.g. + * {@link #getAutoDetectionEnabledBehavior()}, {@link #getGeoDetectionEnabledBehavior()} and {@link + * #createCapabilities()}. + */ +public final class ConfigurationInternal { + + private final @UserIdInt int mUserId; + private final boolean mUserConfigAllowed; + private final boolean mAutoDetectionSupported; + private final boolean mAutoDetectionEnabled; + private final boolean mLocationEnabled; + private final boolean mGeoDetectionEnabled; + + private ConfigurationInternal(Builder builder) { + mUserId = builder.mUserId; + mUserConfigAllowed = builder.mUserConfigAllowed; + mAutoDetectionSupported = builder.mAutoDetectionSupported; + mAutoDetectionEnabled = builder.mAutoDetectionEnabled; + mLocationEnabled = builder.mLocationEnabled; + mGeoDetectionEnabled = builder.mGeoDetectionEnabled; + } + + /** Returns the ID of the user this configuration is associated with. */ + public @UserIdInt int getUserId() { + return mUserId; + } + + /** Returns true if the user allowed to modify time zone configuration. */ + public boolean isUserConfigAllowed() { + return mUserConfigAllowed; + } + + /** Returns true if the device supports some form of auto time zone detection. */ + public boolean isAutoDetectionSupported() { + return mAutoDetectionSupported; + } + + /** Returns the value of the auto time zone detection enabled setting. */ + public boolean getAutoDetectionEnabledSetting() { + return mAutoDetectionEnabled; + } + + /** + * Returns true if auto time zone detection behavior is actually enabled, which can be distinct + * from the raw setting value. */ + public boolean getAutoDetectionEnabledBehavior() { + return mAutoDetectionSupported && mAutoDetectionEnabled; + } + + /** Returns true if user's location can be used generally. */ + public boolean isLocationEnabled() { + return mLocationEnabled; + } + + /** Returns the value of the geolocation time zone detection enabled setting. */ + public boolean getGeoDetectionEnabledSetting() { + return mGeoDetectionEnabled; + } + + /** + * Returns true if geolocation time zone detection behavior is actually enabled, which can be + * distinct from the raw setting value. + */ + public boolean getGeoDetectionEnabledBehavior() { + if (getAutoDetectionEnabledBehavior()) { + return mLocationEnabled && mGeoDetectionEnabled; + } + return false; + } + + /** Creates a {@link TimeZoneCapabilities} object using the configuration values. */ + public TimeZoneCapabilities createCapabilities() { + TimeZoneCapabilities.Builder builder = new TimeZoneCapabilities.Builder() + .setConfiguration(asConfiguration()); + + boolean allowConfigDateTime = isUserConfigAllowed(); + + // Automatic time zone detection is only supported on devices if there is a telephony + // network available or geolocation time zone detection is possible. + boolean deviceHasTimeZoneDetection = isAutoDetectionSupported(); + + final int configureAutoDetectionEnabledCapability; + if (!deviceHasTimeZoneDetection) { + configureAutoDetectionEnabledCapability = CAPABILITY_NOT_SUPPORTED; + } else if (!allowConfigDateTime) { + configureAutoDetectionEnabledCapability = CAPABILITY_NOT_ALLOWED; + } else { + configureAutoDetectionEnabledCapability = CAPABILITY_POSSESSED; + } + builder.setConfigureAutoDetectionEnabled(configureAutoDetectionEnabledCapability); + + final int configureGeolocationDetectionEnabledCapability; + if (!deviceHasTimeZoneDetection) { + configureGeolocationDetectionEnabledCapability = CAPABILITY_NOT_SUPPORTED; + } else if (!allowConfigDateTime) { + configureGeolocationDetectionEnabledCapability = CAPABILITY_NOT_ALLOWED; + } else if (!isLocationEnabled()) { + configureGeolocationDetectionEnabledCapability = CAPABILITY_NOT_APPLICABLE; + } else { + configureGeolocationDetectionEnabledCapability = CAPABILITY_POSSESSED; + } + builder.setConfigureGeoDetectionEnabled(configureGeolocationDetectionEnabledCapability); + + // 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 + // user is unable to disable auto detection. + final int suggestManualTimeZoneCapability; + if (!allowConfigDateTime) { + suggestManualTimeZoneCapability = CAPABILITY_NOT_ALLOWED; + } else if (getAutoDetectionEnabledBehavior()) { + suggestManualTimeZoneCapability = CAPABILITY_NOT_APPLICABLE; + } else { + suggestManualTimeZoneCapability = CAPABILITY_POSSESSED; + } + builder.setSuggestManualTimeZone(suggestManualTimeZoneCapability); + + return builder.build(); + } + + /** Returns a {@link TimeZoneConfiguration} from the configuration values. */ + public TimeZoneConfiguration asConfiguration() { + return new TimeZoneConfiguration.Builder(mUserId) + .setAutoDetectionEnabled(getAutoDetectionEnabledSetting()) + .setGeoDetectionEnabled(getGeoDetectionEnabledSetting()) + .build(); + } + + /** + * Merges the configuration values from this with any properties set in {@code + * newConfiguration}. The new configuration has precedence. Used to apply user updates to + * internal configuration. + */ + public ConfigurationInternal merge(TimeZoneConfiguration newConfiguration) { + Builder builder = new Builder(this); + if (newConfiguration.hasSetting(TimeZoneConfiguration.SETTING_AUTO_DETECTION_ENABLED)) { + builder.setAutoDetectionEnabled(newConfiguration.isAutoDetectionEnabled()); + } + if (newConfiguration.hasSetting(TimeZoneConfiguration.SETTING_GEO_DETECTION_ENABLED)) { + builder.setGeoDetectionEnabled(newConfiguration.isGeoDetectionEnabled()); + } + return builder.build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfigurationInternal that = (ConfigurationInternal) o; + return mUserId == that.mUserId + && mUserConfigAllowed == that.mUserConfigAllowed + && mAutoDetectionSupported == that.mAutoDetectionSupported + && mAutoDetectionEnabled == that.mAutoDetectionEnabled + && mLocationEnabled == that.mLocationEnabled + && mGeoDetectionEnabled == that.mGeoDetectionEnabled; + } + + @Override + public int hashCode() { + return Objects.hash(mUserId, mUserConfigAllowed, mAutoDetectionSupported, + mAutoDetectionEnabled, mLocationEnabled, mGeoDetectionEnabled); + } + + @Override + public String toString() { + return "TimeZoneDetectorConfiguration{" + + "mUserId=" + mUserId + + "mUserConfigAllowed=" + mUserConfigAllowed + + "mAutoDetectionSupported=" + mAutoDetectionSupported + + "mAutoDetectionEnabled=" + mAutoDetectionEnabled + + "mLocationEnabled=" + mLocationEnabled + + "mGeoDetectionEnabled=" + mGeoDetectionEnabled + + '}'; + } + + /** + * A Builder for {@link ConfigurationInternal}. + */ + public static class Builder { + + private final @UserIdInt int mUserId; + private boolean mUserConfigAllowed; + private boolean mAutoDetectionSupported; + private boolean mAutoDetectionEnabled; + private boolean mLocationEnabled; + private boolean mGeoDetectionEnabled; + + /** + * Creates a new Builder with only the userId set. + */ + public Builder(@UserIdInt int userId) { + mUserId = userId; + } + + /** + * Creates a new Builder by copying values from an existing instance. + */ + public Builder(ConfigurationInternal toCopy) { + this.mUserId = toCopy.mUserId; + this.mUserConfigAllowed = toCopy.mUserConfigAllowed; + this.mAutoDetectionSupported = toCopy.mAutoDetectionSupported; + this.mAutoDetectionEnabled = toCopy.mAutoDetectionEnabled; + this.mLocationEnabled = toCopy.mLocationEnabled; + this.mGeoDetectionEnabled = toCopy.mGeoDetectionEnabled; + } + + /** + * Sets whether the user is allowed to configure time zone settings on this device. + */ + public Builder setUserConfigAllowed(boolean configAllowed) { + mUserConfigAllowed = configAllowed; + return this; + } + + /** + * Sets whether automatic time zone detection is supported on this device. + */ + public Builder setAutoDetectionSupported(boolean supported) { + mAutoDetectionSupported = supported; + return this; + } + + /** + * Sets the value of the automatic time zone detection enabled setting for this device. + */ + public Builder setAutoDetectionEnabled(boolean enabled) { + mAutoDetectionEnabled = enabled; + return this; + } + + /** + * Sets the value of the location mode setting for this user. + */ + public Builder setLocationEnabled(boolean enabled) { + mLocationEnabled = enabled; + return this; + } + + /** + * Sets the value of the geolocation time zone detection setting for this user. + */ + public Builder setGeoDetectionEnabled(boolean enabled) { + mGeoDetectionEnabled = enabled; + return this; + } + + /** Returns a new {@link ConfigurationInternal}. */ + @NonNull + public ConfigurationInternal build() { + return new ConfigurationInternal(this); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java index 0ca36e0fc2581..d640323255390 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorCallbackImpl.java @@ -16,24 +16,30 @@ package com.android.server.timezonedetector; -import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_ALLOWED; -import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_APPLICABLE; -import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_SUPPORTED; -import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_POSSESSED; +import static android.content.Intent.ACTION_USER_SWITCHED; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.app.ActivityManagerInternal; import android.app.AlarmManager; -import android.app.timezonedetector.TimeZoneCapabilities; import android.app.timezonedetector.TimeZoneConfiguration; +import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.location.LocationManager; import android.net.ConnectivityManager; +import android.os.Handler; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.util.Slog; + +import com.android.server.LocalServices; import java.util.Objects; @@ -42,103 +48,87 @@ import java.util.Objects; */ public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrategyImpl.Callback { + private static final String LOG_TAG = "TimeZoneDetectorCallbackImpl"; private static final String TIMEZONE_PROPERTY = "persist.sys.timezone"; - private final Context mContext; - private final ContentResolver mCr; - private final UserManager mUserManager; + @NonNull private final Context mContext; + @NonNull private final Handler mHandler; + @NonNull private final ContentResolver mCr; + @NonNull private final UserManager mUserManager; + @NonNull private final boolean mGeoDetectionFeatureEnabled; + @NonNull private final LocationManager mLocationManager; + // @NonNull after setConfigChangeListener() is called. + private ConfigurationChangeListener mConfigChangeListener; - TimeZoneDetectorCallbackImpl(Context context) { - mContext = context; + TimeZoneDetectorCallbackImpl(@NonNull Context context, @NonNull Handler handler, + boolean geoDetectionFeatureEnabled) { + mContext = Objects.requireNonNull(context); + mHandler = Objects.requireNonNull(handler); mCr = context.getContentResolver(); mUserManager = context.getSystemService(UserManager.class); + mLocationManager = context.getSystemService(LocationManager.class); + mGeoDetectionFeatureEnabled = geoDetectionFeatureEnabled; + + // Wire up the change listener. All invocations are performed on the mHandler thread. + + // Listen for the user changing / the user's location mode changing. + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_USER_SWITCHED); + filter.addAction(LocationManager.MODE_CHANGED_ACTION); + mContext.registerReceiverForAllUsers(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleConfigChangeOnHandlerThread(); + } + }, filter, null, mHandler); + + // Add async callbacks for global settings being changed. + ContentResolver contentResolver = mContext.getContentResolver(); + contentResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true, + new ContentObserver(mHandler) { + public void onChange(boolean selfChange) { + handleConfigChangeOnHandlerThread(); + } + }); + + // Add async callbacks for user scoped location settings being changed. + contentResolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.LOCATION_TIME_ZONE_DETECTION_ENABLED), + true, + new ContentObserver(mHandler) { + public void onChange(boolean selfChange) { + handleConfigChangeOnHandlerThread(); + } + }, UserHandle.USER_ALL); + } + + private void handleConfigChangeOnHandlerThread() { + if (mConfigChangeListener == null) { + Slog.wtf(LOG_TAG, "mConfigChangeListener is unexpectedly null"); + } + mConfigChangeListener.onChange(); } @Override - public TimeZoneCapabilities getCapabilities(@UserIdInt int userId) { - UserHandle userHandle = UserHandle.of(userId); - boolean disallowConfigDateTime = - mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_DATE_TIME, userHandle); - - TimeZoneCapabilities.Builder builder = new TimeZoneCapabilities.Builder(userId); - - // Automatic time zone detection is only supported (currently) on devices if there is a - // telephony network available. - if (!deviceHasTelephonyNetwork()) { - builder.setConfigureAutoDetectionEnabled(CAPABILITY_NOT_SUPPORTED); - } else if (disallowConfigDateTime) { - builder.setConfigureAutoDetectionEnabled(CAPABILITY_NOT_ALLOWED); - } else { - 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 - // user is unable to disable auto detection. - if (disallowConfigDateTime) { - builder.setSuggestManualTimeZone(CAPABILITY_NOT_ALLOWED); - } else if (isAutoDetectionEnabled()) { - builder.setSuggestManualTimeZone(CAPABILITY_NOT_APPLICABLE); - } else { - builder.setSuggestManualTimeZone(CAPABILITY_POSSESSED); - } - return builder.build(); + public void setConfigChangeListener(@NonNull ConfigurationChangeListener listener) { + mConfigChangeListener = Objects.requireNonNull(listener); } @Override - public TimeZoneConfiguration getConfiguration(@UserIdInt int userId) { - return new TimeZoneConfiguration.Builder() + public ConfigurationInternal getConfigurationInternal(@UserIdInt int userId) { + return new ConfigurationInternal.Builder(userId) + .setUserConfigAllowed(isUserConfigAllowed(userId)) + .setAutoDetectionSupported(isAutoDetectionSupported()) .setAutoDetectionEnabled(isAutoDetectionEnabled()) - .setGeoDetectionEnabled(isGeoDetectionEnabled()) + .setLocationEnabled(isLocationEnabled(userId)) + .setGeoDetectionEnabled(isGeoDetectionEnabled(userId)) .build(); } @Override - public void setConfiguration( - @UserIdInt int userId, @NonNull TimeZoneConfiguration configuration) { - Objects.requireNonNull(configuration); - if (!configuration.isComplete()) { - throw new IllegalArgumentException("configuration=" + configuration + " not complete"); - } - - // Avoid writing auto detection config for devices that do not support auto time zone - // 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 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. - } - } - - @Override - public boolean isAutoDetectionEnabled() { - // To ensure that TimeZoneConfiguration is "complete" for simplicity, devices that do not - // support auto detection have safe, hard coded configuration values that make it look like - // auto detection is turned off. It is therefore important that false is returned from this - // method for devices that do not support auto time zone detection. Such devices will not - // have a UI to turn the auto detection on/off. Returning true could prevent the user - // entering information manually. On devices that do support auto time detection the default - // is to turn auto detection on. - if (isAutoDetectionSupported()) { - return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE, 1 /* default */) > 0; - } - 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; + public @UserIdInt int getCurrentUserId() { + return LocalServices.getService(ActivityManagerInternal.class).getCurrentUserId(); } @Override @@ -165,8 +155,55 @@ public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrat alarmManager.setTimeZone(zoneId); } + @Override + public void storeConfiguration(TimeZoneConfiguration configuration) { + Objects.requireNonNull(configuration); + + // Avoid writing the auto detection enabled setting for devices that do not support auto + // time zone detection: if we wrote it down then we'd set the value explicitly, which would + // prevent detecting "default" later. That might influence what happens on later releases + // that support new types of auto detection on the same hardware. + if (isAutoDetectionSupported()) { + final boolean autoDetectionEnabled = configuration.isAutoDetectionEnabled(); + setAutoDetectionEnabled(autoDetectionEnabled); + + final int userId = configuration.getUserId(); + final boolean geoTzDetectionEnabled = configuration.isGeoDetectionEnabled(); + setGeoDetectionEnabled(userId, geoTzDetectionEnabled); + } + } + + private boolean isUserConfigAllowed(@UserIdInt int userId) { + UserHandle userHandle = UserHandle.of(userId); + return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_DATE_TIME, userHandle); + } + private boolean isAutoDetectionSupported() { - return deviceHasTelephonyNetwork(); + return deviceHasTelephonyNetwork() || mGeoDetectionFeatureEnabled; + } + + private boolean isAutoDetectionEnabled() { + return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE, 1 /* default */) > 0; + } + + private void setAutoDetectionEnabled(boolean enabled) { + Settings.Global.putInt(mCr, Settings.Global.AUTO_TIME_ZONE, enabled ? 1 : 0); + } + + private boolean isLocationEnabled(@UserIdInt int userId) { + return mLocationManager.isLocationEnabledForUser(UserHandle.of(userId)); + } + + private boolean isGeoDetectionEnabled(@UserIdInt int userId) { + final boolean locationEnabled = isLocationEnabled(userId); + return Settings.Secure.getIntForUser(mCr, + Settings.Secure.LOCATION_TIME_ZONE_DETECTION_ENABLED, + locationEnabled ? 1 : 0 /* defaultValue */, userId) != 0; + } + + private void setGeoDetectionEnabled(@UserIdInt int userId, boolean enabled) { + Settings.Secure.putIntForUser(mCr, Settings.Secure.LOCATION_TIME_ZONE_DETECTION_ENABLED, + enabled ? 1 : 0, userId); } private boolean deviceHasTelephonyNetwork() { @@ -174,4 +211,4 @@ public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrat return mContext.getSystemService(ConnectivityManager.class) .isNetworkSupported(ConnectivityManager.TYPE_MOBILE); } -} +} \ No newline at end of file diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java index fb7a73d126327..2d50390c27a9c 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternal.java @@ -27,6 +27,12 @@ import android.annotation.NonNull; */ public interface TimeZoneDetectorInternal extends Dumpable.Container { + /** Adds a listener that will be invoked when time zone detection configuration is changed. */ + void addConfigurationListener(ConfigurationChangeListener listener); + + /** Returns the {@link ConfigurationInternal} for the current user. */ + ConfigurationInternal getCurrentUserConfigurationInternal(); + /** * 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 diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java index 15412a0d14a1f..f0ce827cec5e9 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorInternalImpl.java @@ -20,8 +20,8 @@ import android.annotation.NonNull; import android.content.Context; import android.os.Handler; -import com.android.internal.annotations.VisibleForTesting; - +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -34,18 +34,26 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter @NonNull private final Context mContext; @NonNull private final Handler mHandler; @NonNull private final TimeZoneDetectorStrategy mTimeZoneDetectorStrategy; + @NonNull private final List mConfigurationListeners = + new ArrayList<>(); - static TimeZoneDetectorInternalImpl create(@NonNull Context context, @NonNull Handler handler, - @NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) { - return new TimeZoneDetectorInternalImpl(context, handler, timeZoneDetectorStrategy); - } - - @VisibleForTesting public TimeZoneDetectorInternalImpl(@NonNull Context context, @NonNull Handler handler, @NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) { mContext = Objects.requireNonNull(context); mHandler = Objects.requireNonNull(handler); mTimeZoneDetectorStrategy = Objects.requireNonNull(timeZoneDetectorStrategy); + + // Wire up a change listener so that any downstream listeners can be notified when + // the configuration changes for any reason. + mTimeZoneDetectorStrategy.addConfigChangeListener(this::handleConfigurationChanged); + } + + private void handleConfigurationChanged() { + synchronized (mConfigurationListeners) { + for (ConfigurationChangeListener listener : mConfigurationListeners) { + listener.onChange(); + } + } } @Override @@ -53,6 +61,19 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter mTimeZoneDetectorStrategy.addDumpable(dumpable); } + @Override + public void addConfigurationListener(ConfigurationChangeListener listener) { + synchronized (mConfigurationListeners) { + mConfigurationListeners.add(Objects.requireNonNull(listener)); + } + } + + @Override + @NonNull + public ConfigurationInternal getCurrentUserConfigurationInternal() { + return mTimeZoneDetectorStrategy.getCurrentUserConfigurationInternal(); + } + @Override public void suggestGeolocationTimeZone( @NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) { diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java index d81f949742bd0..7501d9fe6a7cc 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java @@ -24,32 +24,24 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.app.timezonedetector.TimeZoneCapabilities; import android.app.timezonedetector.TimeZoneConfiguration; -import android.content.ContentResolver; import android.content.Context; -import android.database.ContentObserver; -import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; -import android.os.UserHandle; -import android.provider.Settings; import android.util.IndentingPrintWriter; import android.util.Slog; -import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; import com.android.server.FgThread; import com.android.server.SystemService; -import com.android.server.timezonedetector.TimeZoneDetectorStrategy.StrategyListener; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Iterator; import java.util.Objects; /** @@ -65,6 +57,9 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub private static final String TAG = "TimeZoneDetectorService"; + /** A compile time switch for enabling / disabling geolocation-based time zone detection. */ + private static final boolean GEOLOCATION_TIME_ZONE_DETECTION_ENABLED = false; + /** * Handles the service lifecycle for {@link TimeZoneDetectorService} and * {@link TimeZoneDetectorInternalImpl}. @@ -80,18 +75,20 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub // Obtain / create the shared dependencies. Context context = getContext(); Handler handler = FgThread.getHandler(); + TimeZoneDetectorStrategy timeZoneDetectorStrategy = - TimeZoneDetectorStrategyImpl.create(context); + TimeZoneDetectorStrategyImpl.create( + context, handler, GEOLOCATION_TIME_ZONE_DETECTION_ENABLED); // Create and publish the local service for use by internal callers. TimeZoneDetectorInternal internal = - TimeZoneDetectorInternalImpl.create(context, handler, timeZoneDetectorStrategy); + new TimeZoneDetectorInternalImpl(context, handler, timeZoneDetectorStrategy); publishLocalService(TimeZoneDetectorInternal.class, internal); // Publish the binder service so it can be accessed from other (appropriately // permissioned) processes. - TimeZoneDetectorService service = - TimeZoneDetectorService.create(context, handler, timeZoneDetectorStrategy); + TimeZoneDetectorService service = TimeZoneDetectorService.create( + context, handler, timeZoneDetectorStrategy); publishBinderService(Context.TIME_ZONE_DETECTOR_SERVICE, service); } } @@ -102,53 +99,39 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub @NonNull private final Handler mHandler; + @NonNull + private final CallerIdentityInjector mCallerIdentityInjector; + @NonNull private final TimeZoneDetectorStrategy mTimeZoneDetectorStrategy; - /** - * This sparse array acts as a map from userId to listeners running as that userId. User scoped - * as time zone detection configuration is partially user-specific, so different users can - * get different configuration. - */ @GuardedBy("mConfigurationListeners") @NonNull - private final SparseArray> mConfigurationListeners = - new SparseArray<>(); + private final ArrayList mConfigurationListeners = + new ArrayList<>(); private static TimeZoneDetectorService create( @NonNull Context context, @NonNull Handler handler, @NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) { - TimeZoneDetectorService service = - new TimeZoneDetectorService(context, handler, timeZoneDetectorStrategy); - - ContentResolver contentResolver = context.getContentResolver(); - contentResolver.registerContentObserver( - Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true, - new ContentObserver(handler) { - public void onChange(boolean selfChange) { - 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. + CallerIdentityInjector callerIdentityInjector = CallerIdentityInjector.REAL; + TimeZoneDetectorService service = new TimeZoneDetectorService( + context, handler, callerIdentityInjector, timeZoneDetectorStrategy); return service; } @VisibleForTesting public TimeZoneDetectorService(@NonNull Context context, @NonNull Handler handler, + @NonNull CallerIdentityInjector callerIdentityInjector, @NonNull TimeZoneDetectorStrategy timeZoneDetectorStrategy) { mContext = Objects.requireNonNull(context); mHandler = Objects.requireNonNull(handler); + mCallerIdentityInjector = Objects.requireNonNull(callerIdentityInjector); mTimeZoneDetectorStrategy = Objects.requireNonNull(timeZoneDetectorStrategy); - mTimeZoneDetectorStrategy.setStrategyListener(new StrategyListener() { - @Override - public void onConfigurationChanged() { - handleConfigurationChanged(); - } - }); + + // Wire up a change listener so that ITimeZoneConfigurationListeners can be notified when + // the configuration changes for any reason. + mTimeZoneDetectorStrategy.addConfigChangeListener(this::handleConfigurationChanged); } @Override @@ -156,26 +139,12 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub public TimeZoneCapabilities getCapabilities() { enforceManageTimeZoneDetectorConfigurationPermission(); - int userId = UserHandle.getCallingUserId(); - long token = Binder.clearCallingIdentity(); + int userId = mCallerIdentityInjector.getCallingUserId(); + long token = mCallerIdentityInjector.clearCallingIdentity(); try { - return mTimeZoneDetectorStrategy.getCapabilities(userId); + return mTimeZoneDetectorStrategy.getConfigurationInternal(userId).createCapabilities(); } finally { - Binder.restoreCallingIdentity(token); - } - } - - @Override - @NonNull - public TimeZoneConfiguration getConfiguration() { - enforceManageTimeZoneDetectorConfigurationPermission(); - - int userId = UserHandle.getCallingUserId(); - long token = Binder.clearCallingIdentity(); - try { - return mTimeZoneDetectorStrategy.getConfiguration(userId); - } finally { - Binder.restoreCallingIdentity(token); + mCallerIdentityInjector.restoreCallingIdentity(token); } } @@ -184,12 +153,16 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub enforceManageTimeZoneDetectorConfigurationPermission(); Objects.requireNonNull(configuration); - int userId = UserHandle.getCallingUserId(); - long token = Binder.clearCallingIdentity(); + int callingUserId = mCallerIdentityInjector.getCallingUserId(); + if (callingUserId != configuration.getUserId()) { + return false; + } + + long token = mCallerIdentityInjector.clearCallingIdentity(); try { - return mTimeZoneDetectorStrategy.updateConfiguration(userId, configuration); + return mTimeZoneDetectorStrategy.updateConfiguration(configuration); } finally { - Binder.restoreCallingIdentity(token); + mCallerIdentityInjector.restoreCallingIdentity(token); } } @@ -197,25 +170,17 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub public void addConfigurationListener(@NonNull ITimeZoneConfigurationListener listener) { enforceManageTimeZoneDetectorConfigurationPermission(); Objects.requireNonNull(listener); - int userId = UserHandle.getCallingUserId(); synchronized (mConfigurationListeners) { - ArrayList listeners = - mConfigurationListeners.get(userId); - if (listeners != null && listeners.contains(listener)) { + if (mConfigurationListeners.contains(listener)) { return; } try { - if (listeners == null) { - listeners = new ArrayList<>(1); - mConfigurationListeners.put(userId, listeners); - } - // Ensure the reference to the listener will be removed if the client process dies. listener.asBinder().linkToDeath(this, 0 /* flags */); // Only add the listener if we can linkToDeath(). - listeners.add(listener); + mConfigurationListeners.add(listener); } catch (RemoteException e) { Slog.e(TAG, "Unable to linkToDeath() for listener=" + listener, e); } @@ -226,19 +191,16 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub public void removeConfigurationListener(@NonNull ITimeZoneConfigurationListener listener) { enforceManageTimeZoneDetectorConfigurationPermission(); Objects.requireNonNull(listener); - int userId = UserHandle.getCallingUserId(); synchronized (mConfigurationListeners) { boolean removedListener = false; - ArrayList userListeners = - mConfigurationListeners.get(userId); - if (userListeners.remove(listener)) { + if (mConfigurationListeners.remove(listener)) { // Stop listening for the client process to die. listener.asBinder().unlinkToDeath(this, 0 /* flags */); removedListener = true; } if (!removedListener) { - Slog.w(TAG, "Client asked to remove listenener=" + listener + Slog.w(TAG, "Client asked to remove listener=" + listener + ", but no listeners were removed." + " mConfigurationListeners=" + mConfigurationListeners); } @@ -259,19 +221,14 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub public void binderDied(IBinder who) { synchronized (mConfigurationListeners) { boolean removedListener = false; - final int userCount = mConfigurationListeners.size(); - for (int i = 0; i < userCount; i++) { - ArrayList userListeners = - mConfigurationListeners.valueAt(i); - Iterator userListenerIterator = - userListeners.iterator(); - while (userListenerIterator.hasNext()) { - ITimeZoneConfigurationListener userListener = userListenerIterator.next(); - if (userListener.asBinder().equals(who)) { - userListenerIterator.remove(); - removedListener = true; - break; - } + final int listenerCount = mConfigurationListeners.size(); + for (int listenerIndex = listenerCount - 1; listenerIndex >= 0; listenerIndex--) { + ITimeZoneConfigurationListener listener = + mConfigurationListeners.get(listenerIndex); + if (listener.asBinder().equals(who)) { + mConfigurationListeners.remove(listenerIndex); + removedListener = true; + break; } } if (!removedListener) { @@ -283,42 +240,25 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub } void handleConfigurationChanged() { - // Note: we could trigger an async time zone detection operation here via a call to - // 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. - // 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++) { - int userId = mConfigurationListeners.keyAt(userIndex); - TimeZoneConfiguration configuration = - mTimeZoneDetectorStrategy.getConfiguration(userId); - - ArrayList listeners = - mConfigurationListeners.valueAt(userIndex); - final int listenerCount = listeners.size(); - for (int listenerIndex = 0; listenerIndex < listenerCount; listenerIndex++) { - ITimeZoneConfigurationListener listener = listeners.get(listenerIndex); - try { - listener.onChange(configuration); - } catch (RemoteException e) { - Slog.w(TAG, "Unable to notify listener=" + listener - + " for userId=" + userId - + " of updated configuration=" + configuration, e); - } + final int listenerCount = mConfigurationListeners.size(); + for (int listenerIndex = 0; listenerIndex < listenerCount; listenerIndex++) { + ITimeZoneConfigurationListener listener = + mConfigurationListeners.get(listenerIndex); + try { + listener.onChange(); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to notify listener=" + listener, e); } } } } /** Provided for command-line access. This is not exposed as a binder API. */ - void suggestGeolocationTimeZone( - @NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) { + void suggestGeolocationTimeZone(@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) { enforceSuggestGeolocationTimeZonePermission(); Objects.requireNonNull(timeZoneSuggestion); @@ -331,12 +271,12 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub enforceSuggestManualTimeZonePermission(); Objects.requireNonNull(timeZoneSuggestion); - int userId = UserHandle.getCallingUserId(); - long token = Binder.clearCallingIdentity(); + int userId = mCallerIdentityInjector.getCallingUserId(); + long token = mCallerIdentityInjector.clearCallingIdentity(); try { return mTimeZoneDetectorStrategy.suggestManualTimeZone(userId, timeZoneSuggestion); } finally { - Binder.restoreCallingIdentity(token); + mCallerIdentityInjector.restoreCallingIdentity(token); } } @@ -358,12 +298,6 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub ipw.flush(); } - /** Internal method for handling the auto time zone configuration being changed. */ - @VisibleForTesting - public void handleAutoTimeZoneConfigChanged() { - mHandler.post(mTimeZoneDetectorStrategy::handleAutoTimeZoneConfigChanged); - } - private void enforceManageTimeZoneDetectorConfigurationPermission() { // TODO Switch to a dedicated MANAGE_TIME_AND_ZONE_CONFIGURATION permission. mContext.enforceCallingPermission( diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java index c5b7e39f4fef8..f944c5638fa9c 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java @@ -19,51 +19,84 @@ import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; -import android.app.timezonedetector.TimeZoneCapabilities; import android.app.timezonedetector.TimeZoneConfiguration; import android.util.IndentingPrintWriter; /** - * The interface for the class that implements the time detection algorithm used by the - * {@link TimeZoneDetectorService}. + * The interface for the class that is responsible for setting the time zone on a device, used by + * {@link TimeZoneDetectorService} and {@link TimeZoneDetectorInternal}. * - *

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

The strategy receives suggestions, which it may use to modify the device's time zone setting. + * Suggestions are acted on or ignored as needed, depending on previously received suggestions and + * the current user's configuration (see {@link ConfigurationInternal}). * - *

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. + *

Devices can have zero, one or two automatic time zone detection algorithm available at any + * point in time. + * + *

The two automatic detection algorithms supported are "telephony" and "geolocation". Algorithm + * availability and use depends on several factors: + *

+ * + *

If there are no automatic time zone detections algorithms available then the user can usually + * change the device time zone manually. Under most circumstances the current user can turn + * automatic time zone detection on or off, or choose the algorithm via settings. + * + *

Telephony detection is independent of the current user. The device keeps track of the most + * recent telephony suggestion from each slotIndex. When telephony detection is in use, the highest + * scoring suggestion is used to set the device time zone based on a scoring algorithm. If several + * slotIndexes provide the same score then the slotIndex with the lowest numeric value "wins". If + * the situation changes and it is no longer possible to be confident about the time zone, + * slotIndexes must have an empty suggestion submitted in order to "withdraw" their previous + * suggestion otherwise it will remain in use. + * + *

Geolocation detection is dependent on the current user and their settings. The device retains + * at most one geolocation suggestion. Generally, use of a device's location is dependent on the + * user's "location toggle", but even when that is enabled the user may choose to enable / disable + * the use of geolocation for device time zone detection. If the current user changes to one that + * does not have geolocation detection enabled, or the user turns off geolocation detection, then + * the strategy discards the latest geolocation suggestion. Devices that lose a location fix must + * have an empty suggestion submitted in order to "withdraw" their previous suggestion otherwise it + * will remain in use. + * + *

Threading: + * + *

Suggestion calls with a void return type may be handed off to a separate thread and handled + * asynchronously. Synchronous calls like {@link #getCurrentUserConfigurationInternal()}, and debug + * calls like {@link #dump(IndentingPrintWriter, String[])}, may be called on a different thread + * concurrently with other operations. * * @hide */ public interface TimeZoneDetectorStrategy extends Dumpable, Dumpable.Container { - /** A listener for strategy events. */ - interface StrategyListener { - /** - * Invoked when configuration has been changed. - */ - void onConfigurationChanged(); - } + /** + * Sets a listener that will be triggered whenever time zone detection configuration is + * changed. + */ + void addConfigChangeListener(@NonNull ConfigurationChangeListener listener); - /** Sets the listener that enables the strategy to communicate with the surrounding service. */ - void setStrategyListener(@NonNull StrategyListener listener); - - /** Returns the user's time zone capabilities. */ + /** Returns the user's time zone configuration. */ @NonNull - TimeZoneCapabilities getCapabilities(@UserIdInt int userId); + ConfigurationInternal getConfigurationInternal(@UserIdInt int userId); /** - * Returns the configuration that controls time zone detector behavior. + * Returns the configuration that controls time zone detector behavior for the current user. */ @NonNull - TimeZoneConfiguration getConfiguration(@UserIdInt int userId); + ConfigurationInternal getCurrentUserConfigurationInternal(); /** - * Updates the configuration settings that control time zone detector behavior. + * Updates the configuration properties that control a device's time zone behavior. + * + *

This method returns {@code true} if the configuration was changed, + * {@code false} otherwise. */ - boolean updateConfiguration( - @UserIdInt int userId, @NonNull TimeZoneConfiguration configuration); + boolean updateConfiguration(@NonNull TimeZoneConfiguration configuration); /** * Suggests zero, one or more time zones for the device, or withdraws a previous suggestion if @@ -85,9 +118,4 @@ public interface TimeZoneDetectorStrategy extends Dumpable, Dumpable.Container { * suggestion. */ void suggestTelephonyTimeZone(@NonNull TelephonyTimeZoneSuggestion suggestion); - - /** - * Called when there has been a change to the automatic time zone detection configuration. - */ - void handleAutoTimeZoneConfigChanged(); } diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java index d1369a2894288..8a42b18b514fc 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java @@ -20,10 +20,7 @@ import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYP import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS; import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET; import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE; -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; @@ -33,6 +30,7 @@ import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.app.timezonedetector.TimeZoneCapabilities; import android.app.timezonedetector.TimeZoneConfiguration; import android.content.Context; +import android.os.Handler; import android.util.IndentingPrintWriter; import android.util.LocalLog; import android.util.Slog; @@ -45,15 +43,7 @@ import java.util.List; import java.util.Objects; /** - * An implementation of {@link TimeZoneDetectorStrategy} that handle telephony and manual - * suggestions. Suggestions are acted on or ignored as needed, dependent on the current "auto time - * zone detection" setting. - * - *

For automatic detection, it keeps track of the most recent telephony suggestion from each - * slotIndex and it uses the best suggestion based on a scoring algorithm. If several slotIndexes - * provide the same score then the slotIndex with the lowest numeric value "wins". If the situation - * changes and it is no longer possible to be confident about the time zone, slotIndexes must have - * an empty suggestion submitted in order to "withdraw" their previous suggestion. + * The real implementation of {@link TimeZoneDetectorStrategy}. * *

Most public methods are marked synchronized to ensure thread safety around internal state. */ @@ -61,48 +51,27 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * Used by {@link TimeZoneDetectorStrategyImpl} to interact with device configuration / settings - * / system properties. It can be faked for testing different scenarios. + * / system properties. It can be faked for testing. * *

Note: Because the settings / system properties-derived values can currently 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 TimeZoneDetectorStrategyImpl} (which is thread safe). + * independently and from different threads (and processes!), their use is prone to race + * conditions. */ @VisibleForTesting public interface Callback { /** - * Returns the capabilities for the user. + * Sets a {@link ConfigurationChangeListener} that will be invoked when there are any + * changes that could affect time zone detection. This is invoked during system server + * setup. */ - @NonNull - TimeZoneCapabilities getCapabilities(@UserIdInt int userId); + void setConfigChangeListener(@NonNull ConfigurationChangeListener listener); - /** - * Returns the configuration for the user. - * @param userId - */ - @NonNull - TimeZoneConfiguration getConfiguration(int userId); + /** Returns the current user at the instant it is called. */ + @UserIdInt int getCurrentUserId(); - /** - * Sets the configuration for the user. This method handles storage only, the configuration - * must have been validated by the caller and be complete. - * - * @throws IllegalArgumentException if {@link TimeZoneConfiguration#isComplete()} - * returns {@code false} - */ - void setConfiguration(@UserIdInt int userId, @NonNull TimeZoneConfiguration configuration); - - /** - * Returns true if automatic time zone detection is currently enabled. - */ - boolean isAutoDetectionEnabled(); - - /** - * Returns whether geolocation can be used for time zone detection when {@link - * #isAutoDetectionEnabled()} returns {@code true}. - */ - boolean isGeoDetectionEnabled(); + /** Returns the {@link ConfigurationInternal} for the specified user. */ + ConfigurationInternal getConfigurationInternal(@UserIdInt int userId); /** * Returns true if the device has had an explicit time zone set. @@ -118,6 +87,13 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat * Sets the device's time zone. */ void setDeviceTimeZone(@NonNull String zoneId); + + /** + * Stores the configuration properties contained in {@code newConfiguration}. + * All checks about user capabilities must be done by the caller and + * {@link TimeZoneConfiguration#isComplete()} must be {@code true}. + */ + void storeConfiguration(TimeZoneConfiguration newConfiguration); } private static final String LOG_TAG = "TimeZoneDetectorStrategy"; @@ -189,9 +165,9 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat @NonNull private final Callback mCallback; - /** Non-null after {@link #setStrategyListener(StrategyListener)} is called. */ - @Nullable - private StrategyListener mListener; + @GuardedBy("this") + @NonNull + private List mConfigChangeListeners = new ArrayList<>(); /** * A log that records the decisions / decision metadata that affected the device's time zone. @@ -211,7 +187,8 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat new ArrayMapWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE); /** - * The latest geolocation suggestion received. + * The latest geolocation suggestion received. If the user disabled geolocation time zone + * detection then the latest suggestion is cleared. */ @GuardedBy("this") private ReferenceWithHistory mLatestGeoLocationSuggestion = @@ -223,113 +200,120 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * Creates a new instance of {@link TimeZoneDetectorStrategyImpl}. */ - public static TimeZoneDetectorStrategyImpl create(Context context) { - Callback timeZoneDetectionServiceHelper = new TimeZoneDetectorCallbackImpl(context); - return new TimeZoneDetectorStrategyImpl(timeZoneDetectionServiceHelper); + public static TimeZoneDetectorStrategyImpl create( + @NonNull Context context, @NonNull Handler handler, + boolean geolocationTimeZoneDetectionEnabled) { + + TimeZoneDetectorCallbackImpl callback = new TimeZoneDetectorCallbackImpl( + context, handler, geolocationTimeZoneDetectionEnabled); + return new TimeZoneDetectorStrategyImpl(callback); } @VisibleForTesting - public TimeZoneDetectorStrategyImpl(Callback callback) { + public TimeZoneDetectorStrategyImpl(@NonNull Callback callback) { mCallback = Objects.requireNonNull(callback); + mCallback.setConfigChangeListener(this::handleConfigChanged); } /** - * Sets a listener that allows the strategy to communicate with the surrounding service. This - * must be called before the instance is used and must only be called once. + * Adds a listener that allows the strategy to communicate with the surrounding service / + * internal. This must be called before the instance is used. */ @Override - public synchronized void setStrategyListener(@NonNull StrategyListener listener) { - if (mListener != null) { - throw new IllegalStateException("Strategy already has a listener"); - } - mListener = Objects.requireNonNull(listener); + public synchronized void addConfigChangeListener( + @NonNull ConfigurationChangeListener listener) { + Objects.requireNonNull(listener); + mConfigChangeListeners.add(listener); } @Override @NonNull - public synchronized TimeZoneCapabilities getCapabilities(@UserIdInt int userId) { - return mCallback.getCapabilities(userId); + public ConfigurationInternal getConfigurationInternal(@UserIdInt int userId) { + return mCallback.getConfigurationInternal(userId); } @Override @NonNull - public synchronized TimeZoneConfiguration getConfiguration(@UserIdInt int userId) { - return mCallback.getConfiguration(userId); + public synchronized ConfigurationInternal getCurrentUserConfigurationInternal() { + int currentUserId = mCallback.getCurrentUserId(); + return getConfigurationInternal(currentUserId); } @Override public synchronized boolean updateConfiguration( - @UserIdInt int userId, @NonNull TimeZoneConfiguration configurationChanges) { - Objects.requireNonNull(configurationChanges); + @NonNull TimeZoneConfiguration requestedConfiguration) { + Objects.requireNonNull(requestedConfiguration); - // Validate the requested configuration changes before applying any of them. - TimeZoneCapabilities capabilities = mCallback.getCapabilities(userId); - boolean canManageTimeZoneDetection = - capabilities.getConfigureAutoDetectionEnabled() >= CAPABILITY_NOT_APPLICABLE; - if (!canManageTimeZoneDetection - && containsAutoTimeDetectionProperties(configurationChanges)) { + int userId = requestedConfiguration.getUserId(); + TimeZoneCapabilities capabilities = getConfigurationInternal(userId).createCapabilities(); + + // Create a new configuration builder, and copy across the mutable properties users are + // able to modify. Other properties are therefore ignored. + final TimeZoneConfiguration newConfiguration = + capabilities.applyUpdate(requestedConfiguration); + if (newConfiguration == null) { + // The changes could not be made due to return false; } - // Create a complete configuration by merging the existing and new (possibly partial) - // configuration. - final TimeZoneConfiguration oldConfiguration = mCallback.getConfiguration(userId); - final TimeZoneConfiguration newConfiguration = - new TimeZoneConfiguration.Builder(oldConfiguration) - .mergeProperties(configurationChanges) - .build(); + // Store the configuration / notify as needed. This will cause the mCallback to invoke + // handleConfigChanged() asynchronously. + mCallback.storeConfiguration(newConfiguration); - // Set the configuration / notify as needed. - boolean configurationChanged = !oldConfiguration.equals(newConfiguration); - if (configurationChanged) { - mCallback.setConfiguration(userId, newConfiguration); - - String logMsg = "Configuration changed:" - + "oldConfiguration=" + oldConfiguration - + ", configuration=" + configurationChanges - + ", newConfiguration=" + newConfiguration; - mTimeZoneChangesLog.log(logMsg); - if (DBG) { - Slog.d(LOG_TAG, logMsg); - } - mListener.onConfigurationChanged(); + TimeZoneConfiguration oldConfiguration = capabilities.getConfiguration(); + String logMsg = "Configuration changed:" + + " oldConfiguration=" + oldConfiguration + + ", newConfiguration=" + newConfiguration; + mTimeZoneChangesLog.log(logMsg); + if (DBG) { + Slog.d(LOG_TAG, logMsg); } return true; } - private static boolean containsAutoTimeDetectionProperties( - @NonNull TimeZoneConfiguration configuration) { - return configuration.hasProperty(PROPERTY_AUTO_DETECTION_ENABLED) - || configuration.hasProperty(PROPERTY_GEO_DETECTION_ENABLED); - } - @Override public synchronized void suggestGeolocationTimeZone( @NonNull GeolocationTimeZoneSuggestion suggestion) { + + int currentUserId = mCallback.getCurrentUserId(); + ConfigurationInternal currentUserConfig = mCallback.getConfigurationInternal(currentUserId); if (DBG) { - Slog.d(LOG_TAG, "Geolocation suggestion received. newSuggestion=" + suggestion); + Slog.d(LOG_TAG, "Geolocation suggestion received." + + " currentUserConfig=" + currentUserConfig + + " newSuggestion=" + suggestion); } - Objects.requireNonNull(suggestion); - mLatestGeoLocationSuggestion.set(suggestion); - // Now perform auto time zone detection. The new suggestion may be used to modify the time - // zone setting. - if (mCallback.isGeoDetectionEnabled()) { + if (currentUserConfig.getGeoDetectionEnabledBehavior()) { + // Only store a geolocation suggestion if geolocation detection is currently enabled. + mLatestGeoLocationSuggestion.set(suggestion); + + // Now perform auto time zone detection. The new suggestion may be used to modify the + // time zone setting. String reason = "New geolocation time zone suggested. suggestion=" + suggestion; - doAutoTimeZoneDetection(reason); + doAutoTimeZoneDetection(currentUserConfig, reason); } } @Override public synchronized boolean suggestManualTimeZone( @UserIdInt int userId, @NonNull ManualTimeZoneSuggestion suggestion) { + + int currentUserId = mCallback.getCurrentUserId(); + if (userId != currentUserId) { + Slog.w(LOG_TAG, "Manual suggestion received but user != current user, userId=" + userId + + " suggestion=" + suggestion); + + // Only listen to changes from the current user. + return false; + } + Objects.requireNonNull(suggestion); String timeZoneId = suggestion.getZoneId(); String cause = "Manual time suggestion received: suggestion=" + suggestion; - TimeZoneCapabilities capabilities = mCallback.getCapabilities(userId); + TimeZoneCapabilities capabilities = getConfigurationInternal(userId).createCapabilities(); if (capabilities.getSuggestManualTimeZone() != CAPABILITY_POSSESSED) { Slog.i(LOG_TAG, "User does not have the capability needed to set the time zone manually" + ", capabilities=" + capabilities @@ -345,8 +329,12 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat @Override public synchronized void suggestTelephonyTimeZone( @NonNull TelephonyTimeZoneSuggestion suggestion) { + + int currentUserId = mCallback.getCurrentUserId(); + ConfigurationInternal currentUserConfig = mCallback.getConfigurationInternal(currentUserId); if (DBG) { - Slog.d(LOG_TAG, "Telephony suggestion received. newSuggestion=" + suggestion); + Slog.d(LOG_TAG, "Telephony suggestion received. currentUserConfig=" + currentUserConfig + + " newSuggestion=" + suggestion); } Objects.requireNonNull(suggestion); @@ -360,9 +348,9 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // Now perform auto time zone detection. The new suggestion may be used to modify the time // zone setting. - if (!mCallback.isGeoDetectionEnabled()) { + if (!currentUserConfig.getGeoDetectionEnabledBehavior()) { String reason = "New telephony time zone suggested. suggestion=" + suggestion; - doAutoTimeZoneDetection(reason); + doAutoTimeZoneDetection(currentUserConfig, reason); } } @@ -392,15 +380,15 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat * Performs automatic time zone detection. */ @GuardedBy("this") - private void doAutoTimeZoneDetection(@NonNull String detectionReason) { - if (!mCallback.isAutoDetectionEnabled()) { - // Avoid doing unnecessary work with this (race-prone) check. + private void doAutoTimeZoneDetection( + @NonNull ConfigurationInternal currentUserConfig, @NonNull String detectionReason) { + if (!currentUserConfig.getAutoDetectionEnabledBehavior()) { + // Avoid doing unnecessary work. 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()) { + // Use the right suggestions based on the current configuration. + if (currentUserConfig.getGeoDetectionEnabledBehavior()) { doGeolocationTimeZoneDetection(detectionReason); } else { doTelephonyTimeZoneDetection(detectionReason); @@ -480,35 +468,18 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time // zone ID. - String newZoneId = bestTelephonySuggestion.suggestion.getZoneId(); - if (newZoneId == null) { + String zoneId = bestTelephonySuggestion.suggestion.getZoneId(); + if (zoneId == null) { Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:" + " bestTelephonySuggestion=" + bestTelephonySuggestion + " detectionReason=" + detectionReason); return; } - String zoneId = bestTelephonySuggestion.suggestion.getZoneId(); String cause = "Found good suggestion." + ", bestTelephonySuggestion=" + bestTelephonySuggestion + ", detectionReason=" + detectionReason; - setAutoDeviceTimeZoneIfRequired(zoneId, cause); - } - - @GuardedBy("this") - private void setAutoDeviceTimeZoneIfRequired(@NonNull String newZoneId, @NonNull String cause) { - Objects.requireNonNull(newZoneId); - Objects.requireNonNull(cause); - - if (!mCallback.isAutoDetectionEnabled()) { - if (DBG) { - Slog.d(LOG_TAG, "Auto time zone detection is not enabled." - + ", newZoneId=" + newZoneId - + ", cause=" + cause); - } - return; - } - setDeviceTimeZoneIfRequired(newZoneId, cause); + setDeviceTimeZoneIfRequired(zoneId, cause); } @GuardedBy("this") @@ -582,13 +553,39 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat return findBestTelephonySuggestion(); } - @Override - public synchronized void handleAutoTimeZoneConfigChanged() { + private synchronized void handleConfigChanged() { if (DBG) { - Slog.d(LOG_TAG, "handleAutoTimeZoneConfigChanged()"); + Slog.d(LOG_TAG, "handleConfigChanged()"); } - doAutoTimeZoneDetection("handleAutoTimeZoneConfigChanged()"); + clearGeolocationSuggestionIfNeeded(); + + for (ConfigurationChangeListener listener : mConfigChangeListeners) { + listener.onChange(); + } + } + + @GuardedBy("this") + private void clearGeolocationSuggestionIfNeeded() { + // This method is called whenever the user changes or the config for any user changes. We + // don't know what happened, so we capture the current user's config, check to see if we + // need to clear state associated with a previous user, and rerun detection. + int currentUserId = mCallback.getCurrentUserId(); + ConfigurationInternal currentUserConfig = mCallback.getConfigurationInternal(currentUserId); + + GeolocationTimeZoneSuggestion latestGeoLocationSuggestion = + mLatestGeoLocationSuggestion.get(); + if (latestGeoLocationSuggestion != null + && !currentUserConfig.getGeoDetectionEnabledBehavior()) { + // The current user's config has geodetection disabled, so clear the latest suggestion. + // This is done to ensure we only ever keep a geolocation suggestion if the user has + // said it is ok to do so. + mLatestGeoLocationSuggestion.set(null); + mTimeZoneChangesLog.log( + "clearGeolocationSuggestionIfNeeded: Cleared latest Geolocation suggestion."); + } + + doAutoTimeZoneDetection(currentUserConfig, "clearGeolocationSuggestionIfNeeded()"); } @Override @@ -604,11 +601,14 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat ipw.println("TimeZoneDetectorStrategy:"); ipw.increaseIndent(); // level 1 - ipw.println("mCallback.isAutoDetectionEnabled()=" + mCallback.isAutoDetectionEnabled()); + int currentUserId = mCallback.getCurrentUserId(); + ipw.println("mCallback.getCurrentUserId()=" + currentUserId); + ConfigurationInternal configuration = mCallback.getConfigurationInternal(currentUserId); + ipw.println("mCallback.getConfiguration(currentUserId)=" + configuration); + ipw.println("[Capabilities=" + configuration.createCapabilities() + "]"); 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 diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/ConfigurationInternalTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/ConfigurationInternalTest.java new file mode 100644 index 0000000000000..d7ed96fd58333 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/ConfigurationInternalTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_ALLOWED; +import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_APPLICABLE; +import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_SUPPORTED; +import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_POSSESSED; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.app.timezonedetector.TimeZoneCapabilities; + +import org.junit.Test; + +/** + * Tests for {@link ConfigurationInternal} and the {@link TimeZoneCapabilities} and + * {@link android.app.timezonedetector.TimeZoneConfiguration} that can be generated from it. + */ +public class ConfigurationInternalTest { + + private static final int ARBITRARY_USER_ID = 99999; + + /** + * Tests when {@link ConfigurationInternal#isUserConfigAllowed()} and + * {@link ConfigurationInternal#isAutoDetectionSupported()} are both true. + */ + @Test + public void test_unrestricted() { + ConfigurationInternal baseConfig = new ConfigurationInternal.Builder(ARBITRARY_USER_ID) + .setUserConfigAllowed(true) + .setAutoDetectionSupported(true) + .setAutoDetectionEnabled(true) + .setLocationEnabled(true) + .setGeoDetectionEnabled(true) + .build(); + { + ConfigurationInternal autoOnConfig = new ConfigurationInternal.Builder(baseConfig) + .setAutoDetectionEnabled(true) + .build(); + assertTrue(autoOnConfig.getAutoDetectionEnabledSetting()); + assertTrue(autoOnConfig.getGeoDetectionEnabledSetting()); + assertTrue(autoOnConfig.getAutoDetectionEnabledBehavior()); + assertTrue(autoOnConfig.getGeoDetectionEnabledBehavior()); + + TimeZoneCapabilities capabilities = autoOnConfig.createCapabilities(); + assertEquals(CAPABILITY_POSSESSED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_POSSESSED, capabilities.getConfigureGeoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_APPLICABLE, capabilities.getSuggestManualTimeZone()); + assertEquals(autoOnConfig.asConfiguration(), capabilities.getConfiguration()); + assertTrue(capabilities.getConfiguration().isAutoDetectionEnabled()); + assertTrue(capabilities.getConfiguration().isGeoDetectionEnabled()); + } + + { + ConfigurationInternal autoOffConfig = new ConfigurationInternal.Builder(baseConfig) + .setAutoDetectionEnabled(false) + .build(); + assertFalse(autoOffConfig.getAutoDetectionEnabledSetting()); + assertTrue(autoOffConfig.getGeoDetectionEnabledSetting()); + assertFalse(autoOffConfig.getAutoDetectionEnabledBehavior()); + assertFalse(autoOffConfig.getGeoDetectionEnabledBehavior()); + + TimeZoneCapabilities capabilities = autoOffConfig.createCapabilities(); + assertEquals(CAPABILITY_POSSESSED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_POSSESSED, capabilities.getConfigureGeoDetectionEnabled()); + assertEquals(CAPABILITY_POSSESSED, capabilities.getSuggestManualTimeZone()); + assertEquals(autoOffConfig.asConfiguration(), capabilities.getConfiguration()); + assertFalse(capabilities.getConfiguration().isAutoDetectionEnabled()); + assertTrue(capabilities.getConfiguration().isGeoDetectionEnabled()); + } + } + + /** Tests when {@link ConfigurationInternal#isUserConfigAllowed()} is false */ + @Test + public void test_restricted() { + ConfigurationInternal baseConfig = new ConfigurationInternal.Builder(ARBITRARY_USER_ID) + .setUserConfigAllowed(false) + .setAutoDetectionSupported(true) + .setAutoDetectionEnabled(true) + .setLocationEnabled(true) + .setGeoDetectionEnabled(true) + .build(); + { + ConfigurationInternal autoOnConfig = new ConfigurationInternal.Builder(baseConfig) + .setAutoDetectionEnabled(true) + .build(); + assertTrue(autoOnConfig.getAutoDetectionEnabledSetting()); + assertTrue(autoOnConfig.getGeoDetectionEnabledSetting()); + assertTrue(autoOnConfig.getAutoDetectionEnabledBehavior()); + assertTrue(autoOnConfig.getGeoDetectionEnabledBehavior()); + + TimeZoneCapabilities capabilities = autoOnConfig.createCapabilities(); + assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getConfigureGeoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getSuggestManualTimeZone()); + assertEquals(autoOnConfig.asConfiguration(), capabilities.getConfiguration()); + assertTrue(capabilities.getConfiguration().isAutoDetectionEnabled()); + assertTrue(capabilities.getConfiguration().isGeoDetectionEnabled()); + } + + { + ConfigurationInternal autoOffConfig = new ConfigurationInternal.Builder(baseConfig) + .setAutoDetectionEnabled(false) + .build(); + assertFalse(autoOffConfig.getAutoDetectionEnabledSetting()); + assertTrue(autoOffConfig.getGeoDetectionEnabledSetting()); + assertFalse(autoOffConfig.getAutoDetectionEnabledBehavior()); + assertFalse(autoOffConfig.getGeoDetectionEnabledBehavior()); + + TimeZoneCapabilities capabilities = autoOffConfig.createCapabilities(); + assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getConfigureGeoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_ALLOWED, capabilities.getSuggestManualTimeZone()); + assertEquals(autoOffConfig.asConfiguration(), capabilities.getConfiguration()); + assertFalse(capabilities.getConfiguration().isAutoDetectionEnabled()); + assertTrue(capabilities.getConfiguration().isGeoDetectionEnabled()); + } + } + + /** Tests when {@link ConfigurationInternal#isAutoDetectionSupported()} is false. */ + @Test + public void test_autoDetectNotSupported() { + ConfigurationInternal baseConfig = new ConfigurationInternal.Builder(ARBITRARY_USER_ID) + .setUserConfigAllowed(true) + .setAutoDetectionSupported(false) + .setAutoDetectionEnabled(true) + .setLocationEnabled(true) + .setGeoDetectionEnabled(true) + .build(); + { + ConfigurationInternal autoOnConfig = new ConfigurationInternal.Builder(baseConfig) + .setAutoDetectionEnabled(true) + .build(); + assertTrue(autoOnConfig.getAutoDetectionEnabledSetting()); + assertTrue(autoOnConfig.getGeoDetectionEnabledSetting()); + assertFalse(autoOnConfig.getAutoDetectionEnabledBehavior()); + assertFalse(autoOnConfig.getGeoDetectionEnabledBehavior()); + + TimeZoneCapabilities capabilities = autoOnConfig.createCapabilities(); + assertEquals(CAPABILITY_NOT_SUPPORTED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_SUPPORTED, capabilities.getConfigureGeoDetectionEnabled()); + assertEquals(CAPABILITY_POSSESSED, capabilities.getSuggestManualTimeZone()); + assertEquals(autoOnConfig.asConfiguration(), capabilities.getConfiguration()); + assertTrue(capabilities.getConfiguration().isAutoDetectionEnabled()); + assertTrue(capabilities.getConfiguration().isGeoDetectionEnabled()); + } + { + ConfigurationInternal autoOffConfig = new ConfigurationInternal.Builder(baseConfig) + .setAutoDetectionEnabled(false) + .build(); + assertFalse(autoOffConfig.getAutoDetectionEnabledSetting()); + assertTrue(autoOffConfig.getGeoDetectionEnabledSetting()); + assertFalse(autoOffConfig.getAutoDetectionEnabledBehavior()); + assertFalse(autoOffConfig.getGeoDetectionEnabledBehavior()); + + TimeZoneCapabilities capabilities = autoOffConfig.createCapabilities(); + assertEquals(CAPABILITY_NOT_SUPPORTED, capabilities.getConfigureAutoDetectionEnabled()); + assertEquals(CAPABILITY_NOT_SUPPORTED, capabilities.getConfigureGeoDetectionEnabled()); + assertEquals(CAPABILITY_POSSESSED, capabilities.getSuggestManualTimeZone()); + assertEquals(autoOffConfig.asConfiguration(), capabilities.getConfiguration()); + assertFalse(capabilities.getConfiguration().isAutoDetectionEnabled()); + assertTrue(capabilities.getConfiguration().isGeoDetectionEnabled()); + } + } +} 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 e5e931115c050..4ef20829f2dc1 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/FakeTimeZoneDetectorStrategy.java @@ -18,6 +18,7 @@ package com.android.server.timezonedetector; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import android.annotation.NonNull; import android.annotation.UserIdInt; @@ -32,56 +33,64 @@ import java.util.List; class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { - private StrategyListener mListener; + private ConfigurationChangeListener mConfigurationChangeListener; // Fake state - private TimeZoneCapabilities mCapabilities; - private TimeZoneConfiguration mConfiguration; + private ConfigurationInternal mConfigurationInternal; // Call tracking. private GeolocationTimeZoneSuggestion mLastGeolocationSuggestion; private ManualTimeZoneSuggestion mLastManualSuggestion; private TelephonyTimeZoneSuggestion mLastTelephonySuggestion; - private boolean mHandleAutoTimeZoneConfigChangedCalled; private boolean mDumpCalled; private final List mDumpables = new ArrayList<>(); @Override - public void setStrategyListener(@NonNull StrategyListener listener) { - mListener = listener; + public void addConfigChangeListener(@NonNull ConfigurationChangeListener listener) { + if (mConfigurationChangeListener != null) { + fail("Fake only supports one listener"); + } + mConfigurationChangeListener = listener; } @Override - public TimeZoneCapabilities getCapabilities(@UserIdInt int userId) { - return mCapabilities; + public ConfigurationInternal getConfigurationInternal(int userId) { + if (mConfigurationInternal.getUserId() != userId) { + fail("Fake only supports one user"); + } + return mConfigurationInternal; } @Override - public boolean updateConfiguration( - @UserIdInt int userId, @NonNull TimeZoneConfiguration configuration) { - assertNotNull(mConfiguration); - assertNotNull(configuration); + public ConfigurationInternal getCurrentUserConfigurationInternal() { + return mConfigurationInternal; + } - // Simulate the strategy's behavior: the new configuration will be the old configuration - // merged with the new. - TimeZoneConfiguration oldConfiguration = mConfiguration; - TimeZoneConfiguration newConfiguration = - new TimeZoneConfiguration.Builder(mConfiguration) - .mergeProperties(configuration) - .build(); + @Override + public boolean updateConfiguration(@NonNull TimeZoneConfiguration requestedChanges) { + assertNotNull(mConfigurationInternal); + assertNotNull(requestedChanges); - if (newConfiguration.equals(oldConfiguration)) { + // Simulate the real strategy's behavior: the new configuration will be updated to be the + // old configuration merged with the new if the user has the capability to up the settings. + // Then, if the configuration changed, the change listener is invoked. + TimeZoneCapabilities capabilities = mConfigurationInternal.createCapabilities(); + TimeZoneConfiguration newConfiguration = capabilities.applyUpdate(requestedChanges); + if (newConfiguration == null) { return false; } - mConfiguration = newConfiguration; - mListener.onConfigurationChanged(); + + if (!newConfiguration.equals(capabilities.getConfiguration())) { + mConfigurationInternal = mConfigurationInternal.merge(newConfiguration); + + // Note: Unlike the real strategy, the listeners is invoked synchronously. + mConfigurationChangeListener.onChange(); + } return true; } - @Override - @NonNull - public TimeZoneConfiguration getConfiguration(@UserIdInt int userId) { - return mConfiguration; + public void simulateConfigurationChangeForTests() { + mConfigurationChangeListener.onChange(); } @Override @@ -102,11 +111,6 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { mLastTelephonySuggestion = timeZoneSuggestion; } - @Override - public void handleAutoTimeZoneConfigChanged() { - mHandleAutoTimeZoneConfigChangedCalled = true; - } - @Override public void addDumpable(Dumpable dumpable) { mDumpables.add(dumpable); @@ -117,19 +121,14 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { mDumpCalled = true; } - void initializeConfiguration(TimeZoneConfiguration configuration) { - mConfiguration = configuration; - } - - void initializeCapabilities(TimeZoneCapabilities capabilities) { - mCapabilities = capabilities; + void initializeConfiguration(ConfigurationInternal configurationInternal) { + mConfigurationInternal = configurationInternal; } void resetCallTracking() { mLastGeolocationSuggestion = null; mLastManualSuggestion = null; mLastTelephonySuggestion = null; - mHandleAutoTimeZoneConfigChangedCalled = false; mDumpCalled = false; } @@ -146,10 +145,6 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { assertEquals(expectedSuggestion, mLastTelephonySuggestion); } - void verifyHandleAutoTimeZoneConfigChangedCalled() { - assertTrue(mHandleAutoTimeZoneConfigChangedCalled); - } - void verifyDumpCalled() { assertTrue(mDumpCalled); } diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/TestCallerIdentityInjector.java b/services/tests/servicestests/src/com/android/server/timezonedetector/TestCallerIdentityInjector.java new file mode 100644 index 0000000000000..f45b3a822f1a8 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TestCallerIdentityInjector.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.annotation.UserIdInt; + +/** A fake {@link CallerIdentityInjector} used in tests. */ +public class TestCallerIdentityInjector implements CallerIdentityInjector { + + private long mToken = 9999L; + private int mCallingUserId; + private Integer mCurrentCallingUserId; + + public void initializeCallingUserId(@UserIdInt int userId) { + mCallingUserId = userId; + mCurrentCallingUserId = userId; + } + + @Override + public int getCallingUserId() { + assertNotNull("callingUserId has been cleared", mCurrentCallingUserId); + return mCurrentCallingUserId; + } + + @Override + public long clearCallingIdentity() { + mCurrentCallingUserId = null; + return mToken; + } + + @Override + public void restoreCallingIdentity(long token) { + assertEquals(token, mToken); + mCurrentCallingUserId = mCallingUserId; + } +} 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 e9d57e52ce699..918babca677ed 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorInternalImplTest.java @@ -16,6 +16,7 @@ package com.android.server.timezonedetector; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import android.content.Context; @@ -85,6 +86,18 @@ public class TimeZoneDetectorInternalImplTest { mFakeTimeZoneDetectorStrategy.verifyHasDumpable(stubbedDumpable); } + @Test + public void testAddConfigurationListener() throws Exception { + boolean[] changeCalled = new boolean[2]; + mTimeZoneDetectorInternal.addConfigurationListener(() -> changeCalled[0] = true); + mTimeZoneDetectorInternal.addConfigurationListener(() -> changeCalled[1] = true); + + mFakeTimeZoneDetectorStrategy.simulateConfigurationChangeForTests(); + + assertTrue(changeCalled[0]); + assertTrue(changeCalled[1]); + } + 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 3a1ec4f90d7a4..27b04b6ab17dd 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorServiceTest.java @@ -16,8 +16,6 @@ package com.android.server.timezonedetector; -import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_POSSESSED; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; @@ -36,7 +34,6 @@ import static org.mockito.Mockito.when; import android.app.timezonedetector.ITimeZoneConfigurationListener; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; -import android.app.timezonedetector.TimeZoneCapabilities; import android.app.timezonedetector.TimeZoneConfiguration; import android.content.Context; import android.content.pm.PackageManager; @@ -65,6 +62,7 @@ public class TimeZoneDetectorServiceTest { private TimeZoneDetectorService mTimeZoneDetectorService; private HandlerThread mHandlerThread; private TestHandler mTestHandler; + private TestCallerIdentityInjector mTestCallerIdentityInjector; @Before @@ -76,10 +74,14 @@ public class TimeZoneDetectorServiceTest { mHandlerThread.start(); mTestHandler = new TestHandler(mHandlerThread.getLooper()); + mTestCallerIdentityInjector = new TestCallerIdentityInjector(); + mTestCallerIdentityInjector.initializeCallingUserId(ARBITRARY_USER_ID); + mFakeTimeZoneDetectorStrategy = new FakeTimeZoneDetectorStrategy(); mTimeZoneDetectorService = new TimeZoneDetectorService( - mMockContext, mTestHandler, mFakeTimeZoneDetectorStrategy); + mMockContext, mTestHandler, mTestCallerIdentityInjector, + mFakeTimeZoneDetectorStrategy); } @After @@ -107,40 +109,12 @@ public class TimeZoneDetectorServiceTest { public void testGetCapabilities() { doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); - TimeZoneCapabilities capabilities = createTimeZoneCapabilities(); - mFakeTimeZoneDetectorStrategy.initializeCapabilities(capabilities); - - assertEquals(capabilities, mTimeZoneDetectorService.getCapabilities()); - - verify(mMockContext).enforceCallingPermission( - eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), - anyString()); - } - - @Test(expected = SecurityException.class) - public void testGetConfiguration_withoutPermission() { - doThrow(new SecurityException("Mock")) - .when(mMockContext).enforceCallingPermission(anyString(), any()); - - try { - mTimeZoneDetectorService.getConfiguration(); - fail(); - } finally { - verify(mMockContext).enforceCallingPermission( - eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), - anyString()); - } - } - - @Test - public void testGetConfiguration() { - doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); - - TimeZoneConfiguration configuration = - createTimeZoneConfiguration(false /* autoDetectionEnabled */); + ConfigurationInternal configuration = + createConfigurationInternal(true /* autoDetectionEnabled*/); mFakeTimeZoneDetectorStrategy.initializeConfiguration(configuration); - assertEquals(configuration, mTimeZoneDetectorService.getConfiguration()); + assertEquals(configuration.createCapabilities(), + mTimeZoneDetectorService.getCapabilities()); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), @@ -181,10 +155,9 @@ public class TimeZoneDetectorServiceTest { @Test public void testConfigurationChangeListenerRegistrationAndCallbacks() throws Exception { - TimeZoneConfiguration autoDetectDisabledConfiguration = - createTimeZoneConfiguration(false /* autoDetectionEnabled */); - - mFakeTimeZoneDetectorStrategy.initializeConfiguration(autoDetectDisabledConfiguration); + ConfigurationInternal initialConfiguration = + createConfigurationInternal(false /* autoDetectionEnabled */); + mFakeTimeZoneDetectorStrategy.initializeConfiguration(initialConfiguration); IBinder mockListenerBinder = mock(IBinder.class); ITimeZoneConfigurationListener mockListener = mock(ITimeZoneConfigurationListener.class); @@ -210,13 +183,12 @@ public class TimeZoneDetectorServiceTest { // Simulate the configuration being changed and verify the mockListener was notified. TimeZoneConfiguration autoDetectEnabledConfiguration = createTimeZoneConfiguration(true /* autoDetectionEnabled */); - mTimeZoneDetectorService.updateConfiguration(autoDetectEnabledConfiguration); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); - verify(mockListener).onChange(autoDetectEnabledConfiguration); + verify(mockListener).onChange(); verifyNoMoreInteractions(mockListenerBinder, mockListener, mMockContext); reset(mockListenerBinder, mockListener, mMockContext); } @@ -242,12 +214,14 @@ public class TimeZoneDetectorServiceTest { { doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); + TimeZoneConfiguration autoDetectDisabledConfiguration = + createTimeZoneConfiguration(false /* autoDetectionEnabled */); mTimeZoneDetectorService.updateConfiguration(autoDetectDisabledConfiguration); verify(mMockContext).enforceCallingPermission( eq(android.Manifest.permission.WRITE_SECURE_SETTINGS), anyString()); - verify(mockListener, never()).onChange(any()); + verify(mockListener, never()).onChange(); verifyNoMoreInteractions(mockListenerBinder, mockListener, mMockContext); reset(mockListenerBinder, mockListener, mMockContext); } @@ -379,33 +353,22 @@ public class TimeZoneDetectorServiceTest { mFakeTimeZoneDetectorStrategy.verifyDumpCalled(); } - @Test - public void testHandleAutoTimeZoneConfigChanged() throws Exception { - mTimeZoneDetectorService.handleAutoTimeZoneConfigChanged(); - mTestHandler.assertTotalMessagesEnqueued(1); - mTestHandler.waitForMessagesToBeProcessed(); - mFakeTimeZoneDetectorStrategy.verifyHandleAutoTimeZoneConfigChangedCalled(); - - mFakeTimeZoneDetectorStrategy.resetCallTracking(); - - mTimeZoneDetectorService.handleAutoTimeZoneConfigChanged(); - mTestHandler.assertTotalMessagesEnqueued(2); - mTestHandler.waitForMessagesToBeProcessed(); - mFakeTimeZoneDetectorStrategy.verifyHandleAutoTimeZoneConfigChangedCalled(); - } - - private static TimeZoneConfiguration createTimeZoneConfiguration( - boolean autoDetectionEnabled) { - return new TimeZoneConfiguration.Builder() + private static TimeZoneConfiguration createTimeZoneConfiguration(boolean autoDetectionEnabled) { + return new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID) .setAutoDetectionEnabled(autoDetectionEnabled) .build(); } - private static TimeZoneCapabilities createTimeZoneCapabilities() { - return new TimeZoneCapabilities.Builder(ARBITRARY_USER_ID) - .setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED) - .setConfigureGeoDetectionEnabled(CAPABILITY_POSSESSED) - .setSuggestManualTimeZone(CAPABILITY_POSSESSED) + private static ConfigurationInternal createConfigurationInternal(boolean autoDetectionEnabled) { + // Default geo detection settings from auto detection settings - they are not important to + // the tests. + final boolean geoDetectionEnabled = autoDetectionEnabled; + return new ConfigurationInternal.Builder(ARBITRARY_USER_ID) + .setAutoDetectionSupported(true) + .setUserConfigAllowed(true) + .setAutoDetectionEnabled(autoDetectionEnabled) + .setLocationEnabled(geoDetectionEnabled) + .setGeoDetectionEnabled(geoDetectionEnabled) .build(); } 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 a6caa4299ef68..2bee5e5e72956 100644 --- a/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java +++ b/services/tests/servicestests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java @@ -23,10 +23,6 @@ import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.MATCH_TYP import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS; import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_MULTIPLE_ZONES_WITH_SAME_OFFSET; import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_SINGLE_ZONE; -import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_ALLOWED; -import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_APPLICABLE; -import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_SUPPORTED; -import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_POSSESSED; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGH; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGHEST; @@ -37,9 +33,9 @@ import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.T import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -47,7 +43,6 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality; -import android.app.timezonedetector.TimeZoneCapabilities; import android.app.timezonedetector.TimeZoneConfiguration; import android.util.IndentingPrintWriter; @@ -61,7 +56,6 @@ 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; /** @@ -94,192 +88,149 @@ public class TimeZoneDetectorStrategyImplTest { TELEPHONY_SCORE_HIGHEST), }; - private static final TimeZoneConfiguration CONFIG_AUTO_ENABLED = - new TimeZoneConfiguration.Builder() - .setAutoDetectionEnabled(true) - .build(); - - private static final TimeZoneConfiguration CONFIG_AUTO_DISABLED = - new TimeZoneConfiguration.Builder() + private static final ConfigurationInternal CONFIG_INT_USER_RESTRICTED_AUTO_DISABLED = + new ConfigurationInternal.Builder(USER_ID) + .setUserConfigAllowed(false) + .setAutoDetectionSupported(true) .setAutoDetectionEnabled(false) - .build(); - - private static final TimeZoneConfiguration CONFIG_GEO_DETECTION_DISABLED = - new TimeZoneConfiguration.Builder() + .setLocationEnabled(true) .setGeoDetectionEnabled(false) .build(); - private static final TimeZoneConfiguration CONFIG_GEO_DETECTION_ENABLED = - new TimeZoneConfiguration.Builder() + private static final ConfigurationInternal CONFIG_INT_USER_RESTRICTED_AUTO_ENABLED = + new ConfigurationInternal.Builder(USER_ID) + .setUserConfigAllowed(false) + .setAutoDetectionSupported(true) + .setAutoDetectionEnabled(true) + .setLocationEnabled(true) .setGeoDetectionEnabled(true) .build(); + private static final ConfigurationInternal CONFIG_INT_AUTO_DETECT_NOT_SUPPORTED = + new ConfigurationInternal.Builder(USER_ID) + .setUserConfigAllowed(true) + .setAutoDetectionSupported(false) + .setAutoDetectionEnabled(false) + .setLocationEnabled(true) + .setGeoDetectionEnabled(false) + .build(); + + private static final ConfigurationInternal CONFIG_INT_AUTO_DISABLED_GEO_DISABLED = + new ConfigurationInternal.Builder(USER_ID) + .setUserConfigAllowed(true) + .setAutoDetectionSupported(true) + .setAutoDetectionEnabled(false) + .setLocationEnabled(true) + .setGeoDetectionEnabled(false) + .build(); + + private static final ConfigurationInternal CONFIG_INT_AUTO_DISABLED_GEO_ENABLED = + new ConfigurationInternal.Builder(USER_ID) + .setUserConfigAllowed(true) + .setAutoDetectionSupported(true) + .setAutoDetectionEnabled(false) + .setLocationEnabled(true) + .setGeoDetectionEnabled(true) + .build(); + + private static final ConfigurationInternal CONFIG_INT_AUTO_ENABLED_GEO_DISABLED = + new ConfigurationInternal.Builder(USER_ID) + .setAutoDetectionSupported(true) + .setUserConfigAllowed(true) + .setAutoDetectionEnabled(true) + .setLocationEnabled(true) + .setGeoDetectionEnabled(false) + .build(); + + private static final ConfigurationInternal CONFIG_INT_AUTO_ENABLED_GEO_ENABLED = + new ConfigurationInternal.Builder(USER_ID) + .setAutoDetectionSupported(true) + .setUserConfigAllowed(true) + .setAutoDetectionEnabled(true) + .setLocationEnabled(true) + .setGeoDetectionEnabled(true) + .build(); + + private static final TimeZoneConfiguration CONFIG_AUTO_DISABLED = + createConfig(false /* autoDetection */, null); + private static final TimeZoneConfiguration CONFIG_AUTO_ENABLED = + createConfig(true /* autoDetection */, null); + private static final TimeZoneConfiguration CONFIG_GEO_DETECTION_ENABLED = + createConfig(null, true /* geoDetection */); + private static final TimeZoneConfiguration CONFIG_GEO_DETECTION_DISABLED = + createConfig(null, false /* geoDetection */); + private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy; private FakeCallback mFakeCallback; - private MockStrategyListener mMockStrategyListener; + private MockConfigChangeListener mMockConfigChangeListener; + @Before public void setUp() { mFakeCallback = new FakeCallback(); - mMockStrategyListener = new MockStrategyListener(); + mMockConfigChangeListener = new MockConfigChangeListener(); mTimeZoneDetectorStrategy = new TimeZoneDetectorStrategyImpl(mFakeCallback); - mFakeCallback.setStrategyForSettingsCallbacks(mTimeZoneDetectorStrategy); - mTimeZoneDetectorStrategy.setStrategyListener(mMockStrategyListener); + mTimeZoneDetectorStrategy.addConfigChangeListener(mMockConfigChangeListener); } @Test - public void testGetCapabilities() { - new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); - TimeZoneCapabilities expectedCapabilities = mFakeCallback.getCapabilities(USER_ID); - assertEquals(expectedCapabilities, mTimeZoneDetectorStrategy.getCapabilities(USER_ID)); - } - - @Test - public void testGetConfiguration() { - new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); - TimeZoneConfiguration expectedConfiguration = mFakeCallback.getConfiguration(USER_ID); - assertTrue(expectedConfiguration.isComplete()); - assertEquals(expectedConfiguration, mTimeZoneDetectorStrategy.getConfiguration(USER_ID)); - } - - @Test - public void testCapabilitiesTestInfra_unrestricted() { - Script script = new Script(); - - script.initializeUser(USER_ID, UserCase.UNRESTRICTED, - 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_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()); - } - } - - @Test - public void testCapabilitiesTestInfra_restricted() { - Script script = new Script(); - - script.initializeUser(USER_ID, UserCase.RESTRICTED, - 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_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()); - } - } - - @Test - public void testCapabilitiesTestInfra_autoDetectNotSupported() { - Script script = new Script(); - - script.initializeUser(USER_ID, UserCase.AUTO_DETECT_NOT_SUPPORTED, - 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_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()); - } + public void testGetCurrentUserConfiguration() { + new Script().initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_DISABLED); + ConfigurationInternal expectedConfiguration = + mFakeCallback.getConfigurationInternal(USER_ID); + assertEquals(expectedConfiguration, + mTimeZoneDetectorStrategy.getCurrentUserConfigurationInternal()); } @Test public void testUpdateConfiguration_unrestricted() { - Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); + Script script = new Script().initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_DISABLED); // Set the configuration with auto detection enabled. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */); + script.simulateUpdateConfiguration(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_DISABLED, true /* expectedResult */); + script.simulateUpdateConfiguration(CONFIG_AUTO_DISABLED, true /* expectedResult */); // The settings should have been changed and the StrategyListener onChange() called. - script.verifyConfigurationChangedAndReset(USER_ID, - CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)); + script.verifyConfigurationChangedAndReset(CONFIG_INT_AUTO_DISABLED_GEO_DISABLED); // Update the configuration with auto detection enabled. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */); + script.simulateUpdateConfiguration(CONFIG_AUTO_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_DISABLED)); + script.verifyConfigurationChangedAndReset(CONFIG_INT_AUTO_ENABLED_GEO_DISABLED); // Update the configuration to enable geolocation time zone detection. script.simulateUpdateConfiguration( - USER_ID, CONFIG_GEO_DETECTION_ENABLED, true /* expectedResult */); + 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)); + script.verifyConfigurationChangedAndReset(CONFIG_INT_AUTO_ENABLED_GEO_ENABLED); } @Test public void testUpdateConfiguration_restricted() { - Script script = new Script() - .initializeUser(USER_ID, UserCase.RESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); + Script script = new Script().initializeConfig(CONFIG_INT_USER_RESTRICTED_AUTO_ENABLED); // Try to update the configuration with auto detection disabled. - script.simulateUpdateConfiguration( - USER_ID, CONFIG_AUTO_DISABLED, false /* expectedResult */); + script.simulateUpdateConfiguration(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_ENABLED, false /* expectedResult */); + script.simulateUpdateConfiguration(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. + // Try to update the configuration to enable geolocation time zone detection. script.simulateUpdateConfiguration( - USER_ID, CONFIG_GEO_DETECTION_ENABLED, false /* expectedResult */); + CONFIG_GEO_DETECTION_ENABLED, false /* expectedResult */); // The settings should not have been changed: user shouldn't have the capabilities. script.verifyConfigurationNotChanged(); @@ -287,20 +238,16 @@ public class TimeZoneDetectorStrategyImplTest { @Test public void testUpdateConfiguration_autoDetectNotSupported() { - Script script = new Script() - .initializeUser(USER_ID, UserCase.AUTO_DETECT_NOT_SUPPORTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); + Script script = new Script().initializeConfig(CONFIG_INT_AUTO_DETECT_NOT_SUPPORTED); // Try to update the configuration with auto detection disabled. - script.simulateUpdateConfiguration( - USER_ID, CONFIG_AUTO_DISABLED, false /* expectedResult */); + script.simulateUpdateConfiguration(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_ENABLED, false /* expectedResult */); + script.simulateUpdateConfiguration(CONFIG_AUTO_ENABLED, false /* expectedResult */); // The settings should not have been changed: user shouldn't have the capabilities. script.verifyConfigurationNotChanged(); @@ -313,8 +260,7 @@ public class TimeZoneDetectorStrategyImplTest { TelephonyTimeZoneSuggestion slotIndex2TimeZoneSuggestion = createEmptySlotIndex2Suggestion(); Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + .initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_DISABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); script.simulateTelephonyTimeZoneSuggestion(slotIndex1TimeZoneSuggestion) @@ -359,9 +305,7 @@ public class TimeZoneDetectorStrategyImplTest { TelephonyTestCase testCase2 = newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_SINGLE_ZONE, TELEPHONY_SCORE_HIGH); - Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); + Script script = new Script().initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_DISABLED); // A low quality suggestions will not be taken: The device time zone setting is left // uninitialized. @@ -426,8 +370,7 @@ public class TimeZoneDetectorStrategyImplTest { for (TelephonyTestCase testCase : TELEPHONY_TEST_CASES) { // Start with the device in a known state. - script.initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + script.initializeConfig(CONFIG_INT_AUTO_DISABLED_GEO_DISABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); TelephonyTimeZoneSuggestion suggestion = @@ -447,8 +390,7 @@ public class TimeZoneDetectorStrategyImplTest { mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); // Toggling the time zone setting on should cause the device setting to be set. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, - true /* expectedResult */); + script.simulateUpdateConfiguration(CONFIG_AUTO_ENABLED, true /* expectedResult */); // When time zone detection is already enabled the suggestion (if it scores highly // enough) should be set immediately. @@ -465,8 +407,7 @@ public class TimeZoneDetectorStrategyImplTest { mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); // Toggling the time zone setting should off should do nothing. - script.simulateUpdateConfiguration( - USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */) + script.simulateUpdateConfiguration(CONFIG_AUTO_DISABLED, true /* expectedResult */) .verifyTimeZoneNotChanged(); // Assert internal service state. @@ -480,8 +421,7 @@ public class TimeZoneDetectorStrategyImplTest { @Test public void testTelephonySuggestionsSingleSlotId() { Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + .initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_DISABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); for (TelephonyTestCase testCase : TELEPHONY_TEST_CASES) { @@ -546,8 +486,7 @@ public class TimeZoneDetectorStrategyImplTest { TELEPHONY_SCORE_NONE); Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + .initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_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. @@ -632,9 +571,7 @@ public class TimeZoneDetectorStrategyImplTest { */ @Test public void testTelephonySuggestionStrategyDoesNotAssumeCurrentSetting_autoTelephony() { - Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)); + Script script = new Script().initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_DISABLED); TelephonyTestCase testCase = newTelephonyTestCase( MATCH_TYPE_NETWORK_COUNTRY_AND_OFFSET, QUALITY_SINGLE_ZONE, TELEPHONY_SCORE_HIGH); @@ -652,40 +589,39 @@ 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.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_DISABLED, true /* expectedResult */) + script.simulateUpdateConfiguration(CONFIG_AUTO_DISABLED, true /* expectedResult */) .verifyTimeZoneNotChanged() - .simulateUpdateConfiguration( - USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) + .simulateUpdateConfiguration(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 */) + script.simulateUpdateConfiguration(CONFIG_AUTO_DISABLED, true /* expectedResult */) .simulateTelephonyTimeZoneSuggestion(newYorkSuggestion) .verifyTimeZoneNotChanged(); // Latest suggestion should be used. - script.simulateUpdateConfiguration(USER_ID, CONFIG_AUTO_ENABLED, true /* expectedResult */) + script.simulateUpdateConfiguration(CONFIG_AUTO_ENABLED, true /* expectedResult */) .verifyTimeZoneChangedAndReset(newYorkSuggestion); } @Test - public void testManualSuggestion_autoDetectionEnabled_autoTelephony() { - checkManualSuggestion_autoDetectionEnabled(false /* geoDetectionEnabled */); + public void testManualSuggestion_unrestricted_autoDetectionEnabled_autoTelephony() { + checkManualSuggestion_unrestricted_autoDetectionEnabled(false /* geoDetectionEnabled */); } @Test - public void testManualSuggestion_autoDetectionEnabled_autoGeo() { - checkManualSuggestion_autoDetectionEnabled(true /* geoDetectionEnabled */); + public void testManualSuggestion_unrestricted_autoDetectionEnabled_autoGeo() { + checkManualSuggestion_unrestricted_autoDetectionEnabled(true /* geoDetectionEnabled */); } - private void checkManualSuggestion_autoDetectionEnabled(boolean geoDetectionEnabled) { - TimeZoneConfiguration geoTzEnabledConfig = - new TimeZoneConfiguration.Builder() + private void checkManualSuggestion_unrestricted_autoDetectionEnabled( + boolean geoDetectionEnabled) { + ConfigurationInternal geoTzEnabledConfig = + new ConfigurationInternal.Builder(CONFIG_INT_AUTO_ENABLED_GEO_DISABLED) .setGeoDetectionEnabled(geoDetectionEnabled) .build(); Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(geoTzEnabledConfig)) + .initializeConfig(geoTzEnabledConfig) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Auto time zone detection is enabled so the manual suggestion should be ignored. @@ -697,35 +633,19 @@ public class TimeZoneDetectorStrategyImplTest { @Test public void testManualSuggestion_restricted_simulateAutoTimeZoneEnabled() { Script script = new Script() - .initializeUser(USER_ID, UserCase.RESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + .initializeConfig(CONFIG_INT_USER_RESTRICTED_AUTO_ENABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); - // Auto time zone detection is enabled so the manual suggestion should be ignored. + // User is restricted so the manual suggestion should be ignored. script.simulateManualTimeZoneSuggestion( USER_ID, createManualSuggestion("Europe/Paris"), false /* expectedResult */) - .verifyTimeZoneNotChanged(); - } - - @Test - public void testManualSuggestion_autoDetectNotSupported_simulateAutoTimeZoneEnabled() { - Script script = new Script() - .initializeUser(USER_ID, UserCase.AUTO_DETECT_NOT_SUPPORTED, - 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. - ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris"); - script.simulateManualTimeZoneSuggestion( - USER_ID, manualSuggestion, true /* expectedResult */) - .verifyTimeZoneChangedAndReset(manualSuggestion); + .verifyTimeZoneNotChanged(); } @Test public void testManualSuggestion_unrestricted_autoTimeZoneDetectionDisabled() { Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + .initializeConfig(CONFIG_INT_AUTO_DISABLED_GEO_DISABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Auto time zone detection is disabled so the manual suggestion should be used. @@ -738,8 +658,7 @@ public class TimeZoneDetectorStrategyImplTest { @Test public void testManualSuggestion_restricted_autoTimeZoneDetectionDisabled() { Script script = new Script() - .initializeUser(USER_ID, UserCase.RESTRICTED, - CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + .initializeConfig(CONFIG_INT_USER_RESTRICTED_AUTO_DISABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Restricted users do not have the capability. @@ -750,10 +669,9 @@ public class TimeZoneDetectorStrategyImplTest { } @Test - public void testManualSuggestion_autoDetectNotSupported_autoTimeZoneDetectionDisabled() { + public void testManualSuggestion_autoDetectNotSupported() { Script script = new Script() - .initializeUser(USER_ID, UserCase.AUTO_DETECT_NOT_SUPPORTED, - CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + .initializeConfig(CONFIG_INT_AUTO_DETECT_NOT_SUPPORTED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Unrestricted users have the capability. @@ -765,9 +683,7 @@ public class TimeZoneDetectorStrategyImplTest { @Test public void testGeoSuggestion_uncertain() { - Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_ENABLED)) + Script script = new Script().initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_ENABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); GeolocationTimeZoneSuggestion uncertainSuggestion = createUncertainGeoLocationSuggestion(); @@ -783,8 +699,7 @@ public class TimeZoneDetectorStrategyImplTest { @Test public void testGeoSuggestion_noZones() { Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_ENABLED)) + .initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_ENABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); GeolocationTimeZoneSuggestion noZonesSuggestion = createGeoLocationSuggestion(list()); @@ -802,8 +717,7 @@ public class TimeZoneDetectorStrategyImplTest { createGeoLocationSuggestion(list("Europe/London")); Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_ENABLED)) + .initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_ENABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); script.simulateGeolocationTimeZoneSuggestion(suggestion) @@ -828,8 +742,7 @@ public class TimeZoneDetectorStrategyImplTest { createGeoLocationSuggestion(list("Europe/Paris")); Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_ENABLED.with(CONFIG_GEO_DETECTION_ENABLED)) + .initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_ENABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); script.simulateGeolocationTimeZoneSuggestion(londonOnlySuggestion) @@ -856,72 +769,27 @@ public class TimeZoneDetectorStrategyImplTest { 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 = + public void testGeoSuggestion_togglingGeoDetectionClearsLastSuggestion() { + GeolocationTimeZoneSuggestion suggestion = 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)) + .initializeConfig(CONFIG_INT_AUTO_ENABLED_GEO_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 */) + script.simulateGeolocationTimeZoneSuggestion(suggestion) .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(); + // Assert internal service state. + assertEquals(suggestion, mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); - // 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(); + // Turn off geo detection and verify the latest suggestion is cleared. + script.simulateUpdateConfiguration(CONFIG_GEO_DETECTION_DISABLED, true) + .verifyConfigurationChangedAndReset(CONFIG_INT_AUTO_ENABLED_GEO_DISABLED); // Assert internal service state. - assertEquals(uncertainGeolocationSuggestion, - mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + assertNull(mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); } /** @@ -937,88 +805,48 @@ public class TimeZoneDetectorStrategyImplTest { "Europe/Paris"); Script script = new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + .initializeConfig(CONFIG_INT_AUTO_DISABLED_GEO_DISABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); // Add suggestions. Nothing should happen as time zone detection is disabled. script.simulateGeolocationTimeZoneSuggestion(geolocationSuggestion) .verifyTimeZoneNotChanged(); + + // Geolocation suggestions are only stored when geolocation detection is enabled. + assertNull(mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + script.simulateTelephonyTimeZoneSuggestion(telephonySuggestion) .verifyTimeZoneNotChanged(); - // Assert internal service state. - assertEquals(geolocationSuggestion, - mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); + // Telephony suggestions are always stored. 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 */) + script.simulateUpdateConfiguration(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 */) + // Changing the detection to enable geo detection won't cause the device tz setting to + // change because the geo suggestion is empty. + script.simulateUpdateConfiguration(CONFIG_GEO_DETECTION_ENABLED, true /* expectedResult */) + .verifyTimeZoneNotChanged() + .simulateGeolocationTimeZoneSuggestion(geolocationSuggestion) .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 */) + script.simulateUpdateConfiguration(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"); + assertNull(mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); } @Test public void testAddDumpable() { new Script() - .initializeUser(USER_ID, UserCase.UNRESTRICTED, - CONFIG_AUTO_DISABLED.with(CONFIG_GEO_DETECTION_DISABLED)) + .initializeConfig(CONFIG_INT_AUTO_DISABLED_GEO_DISABLED) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID); AtomicBoolean dumpCalled = new AtomicBoolean(false); @@ -1069,25 +897,26 @@ public class TimeZoneDetectorStrategyImplTest { return suggestion; } + private static TimeZoneConfiguration createConfig( + @Nullable Boolean autoDetection, @Nullable Boolean geoDetection) { + TimeZoneConfiguration.Builder builder = new TimeZoneConfiguration.Builder(USER_ID); + if (autoDetection != null) { + builder.setAutoDetectionEnabled(autoDetection); + } + if (geoDetection != null) { + builder.setGeoDetectionEnabled(geoDetection); + } + return builder.build(); + } + static class FakeCallback implements TimeZoneDetectorStrategyImpl.Callback { - private TimeZoneCapabilities mCapabilities; - private final TestState mConfiguration = new TestState<>(); + private final TestState mConfigurationInternal = new TestState<>(); private final TestState mTimeZoneId = new TestState<>(); - private TimeZoneDetectorStrategyImpl mStrategy; + private ConfigurationChangeListener mConfigChangeListener; - void setStrategyForSettingsCallbacks(TimeZoneDetectorStrategyImpl strategy) { - assertNotNull(strategy); - mStrategy = strategy; - } - - void initializeUser(@UserIdInt int userId, TimeZoneCapabilities capabilities, - TimeZoneConfiguration configuration) { - assertEquals(userId, capabilities.getUserId()); - mCapabilities = capabilities; - assertTrue("Configuration must be complete when initializing, config=" + configuration, - configuration.isComplete()); - mConfiguration.init(new UserConfiguration(userId, configuration)); + void initializeConfig(ConfigurationInternal configurationInternal) { + mConfigurationInternal.init(configurationInternal); } void initializeTimeZoneSetting(String zoneId) { @@ -1095,43 +924,22 @@ public class TimeZoneDetectorStrategyImplTest { } @Override - public TimeZoneCapabilities getCapabilities(@UserIdInt int userId) { - assertEquals(userId, mCapabilities.getUserId()); - return mCapabilities; + public void setConfigChangeListener(ConfigurationChangeListener listener) { + mConfigChangeListener = listener; } @Override - public TimeZoneConfiguration getConfiguration(@UserIdInt int userId) { - UserConfiguration latest = mConfiguration.getLatest(); - assertEquals(userId, latest.userId); - return latest.configuration; - } - - @Override - public void setConfiguration(@UserIdInt int userId, TimeZoneConfiguration newConfig) { - assertNotNull(newConfig); - assertTrue(newConfig.isComplete()); - - UserConfiguration latestUserConfig = mConfiguration.getLatest(); - assertEquals(userId, latestUserConfig.userId); - TimeZoneConfiguration oldConfig = latestUserConfig.configuration; - - mConfiguration.set(new UserConfiguration(userId, newConfig)); - - if (!newConfig.equals(oldConfig)) { - // Simulate what happens when the auto detection configuration is changed. - mStrategy.handleAutoTimeZoneConfigChanged(); + public ConfigurationInternal getConfigurationInternal(int userId) { + ConfigurationInternal configuration = mConfigurationInternal.getLatest(); + if (userId != configuration.getUserId()) { + fail("FakeCallback does not support multiple users."); } + return configuration; } @Override - public boolean isAutoDetectionEnabled() { - return mConfiguration.getLatest().configuration.isAutoDetectionEnabled(); - } - - @Override - public boolean isGeoDetectionEnabled() { - return mConfiguration.getLatest().configuration.isGeoDetectionEnabled(); + public int getCurrentUserId() { + return mConfigurationInternal.getLatest().getUserId(); } @Override @@ -1149,9 +957,25 @@ public class TimeZoneDetectorStrategyImplTest { mTimeZoneId.set(zoneId); } + @Override + public void storeConfiguration(TimeZoneConfiguration newConfiguration) { + ConfigurationInternal oldConfiguration = mConfigurationInternal.getLatest(); + if (newConfiguration.getUserId() != oldConfiguration.getUserId()) { + fail("FakeCallback does not support multiple users"); + } + + ConfigurationInternal mergedConfiguration = oldConfiguration.merge(newConfiguration); + if (!mergedConfiguration.equals(oldConfiguration)) { + mConfigurationInternal.set(mergedConfiguration); + + // Note: Unlike the real callback impl, the listener is invoked synchronously. + mConfigChangeListener.onChange(); + } + } + void assertKnownUser(int userId) { - assertEquals(userId, mCapabilities.getUserId()); - assertEquals(userId, mConfiguration.getLatest().userId); + assertEquals("FakeCallback does not support multiple users", + mConfigurationInternal.getLatest().getUserId(), userId); } void assertTimeZoneNotChanged() { @@ -1166,43 +990,7 @@ public class TimeZoneDetectorStrategyImplTest { void commitAllChanges() { mTimeZoneId.commitLatest(); - mConfiguration.commitLatest(); - } - } - - private static final class UserConfiguration { - public final @UserIdInt int userId; - public final TimeZoneConfiguration configuration; - - UserConfiguration(int userId, TimeZoneConfiguration configuration) { - this.userId = userId; - this.configuration = configuration; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - UserConfiguration that = (UserConfiguration) o; - return userId == that.userId - && Objects.equals(configuration, that.configuration); - } - - @Override - public int hashCode() { - return Objects.hash(userId, configuration); - } - - @Override - public String toString() { - return "UserConfiguration{" - + "userId=" + userId - + ", configuration=" + configuration - + '}'; + mConfigurationInternal.commitLatest(); } } @@ -1255,64 +1043,14 @@ public class TimeZoneDetectorStrategyImplTest { } } - /** Simulated user test cases. */ - enum UserCase { - /** A catch-all for users that can set auto time zone config. */ - UNRESTRICTED, - /** A catch-all for users that can't set auto time zone config. */ - RESTRICTED, - /** - * Like {@link #UNRESTRICTED}, but auto tz detection is not - * supported on the device. - */ - AUTO_DETECT_NOT_SUPPORTED, - } - - /** - * Creates a {@link TimeZoneCapabilities} object for a user in the specific role with the - * supplied configuration. - */ - private static TimeZoneCapabilities createCapabilities( - 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(userCase + " not recognized"); - } - } - /** * A "fluent" class allows reuse of code in tests: initialization, simulation and verification * logic. */ private class Script { - Script initializeUser( - @UserIdInt int userId, UserCase userCase, TimeZoneConfiguration configuration) { - TimeZoneCapabilities capabilities = createCapabilities(userId, userCase, configuration); - mFakeCallback.initializeUser(userId, capabilities, configuration); + Script initializeConfig(ConfigurationInternal configuration) { + mFakeCallback.initializeConfig(configuration); return this; } @@ -1326,10 +1064,9 @@ public class TimeZoneDetectorStrategyImplTest { * the return value. */ Script simulateUpdateConfiguration( - @UserIdInt int userId, TimeZoneConfiguration configuration, - boolean expectedResult) { + TimeZoneConfiguration configuration, boolean expectedResult) { assertEquals(expectedResult, - mTimeZoneDetectorStrategy.updateConfiguration(userId, configuration)); + mTimeZoneDetectorStrategy.updateConfiguration(configuration)); return this; } @@ -1392,16 +1129,14 @@ public class TimeZoneDetectorStrategyImplTest { /** * Verifies that the configuration has been changed to the expected value. */ - Script verifyConfigurationChangedAndReset( - @UserIdInt int userId, TimeZoneConfiguration expected) { - mFakeCallback.mConfiguration.assertHasBeenSet(); - UserConfiguration expectedUserConfig = new UserConfiguration(userId, expected); - assertEquals(expectedUserConfig, mFakeCallback.mConfiguration.getLatest()); + Script verifyConfigurationChangedAndReset(ConfigurationInternal expected) { + mFakeCallback.mConfigurationInternal.assertHasBeenSet(); + assertEquals(expected, mFakeCallback.mConfigurationInternal.getLatest()); mFakeCallback.commitAllChanges(); // Also confirm the listener triggered. - mMockStrategyListener.verifyOnConfigurationChangedCalled(); - mMockStrategyListener.reset(); + mMockConfigChangeListener.verifyOnChangeCalled(); + mMockConfigChangeListener.reset(); return this; } @@ -1410,10 +1145,10 @@ public class TimeZoneDetectorStrategyImplTest { * {@link TimeZoneConfiguration} have been changed. */ Script verifyConfigurationNotChanged() { - mFakeCallback.mConfiguration.assertHasNotBeenSet(); + mFakeCallback.mConfigurationInternal.assertHasNotBeenSet(); // Also confirm the listener did not trigger. - mMockStrategyListener.verifyOnConfigurationChangedNotCalled(); + mMockConfigChangeListener.verifyOnChangeNotCalled(); return this; } @@ -1448,24 +1183,24 @@ public class TimeZoneDetectorStrategyImplTest { return new TelephonyTestCase(matchType, quality, expectedScore); } - private static class MockStrategyListener implements TimeZoneDetectorStrategy.StrategyListener { - private boolean mOnConfigurationChangedCalled; + private static class MockConfigChangeListener implements ConfigurationChangeListener { + private boolean mOnChangeCalled; @Override - public void onConfigurationChanged() { - mOnConfigurationChangedCalled = true; + public void onChange() { + mOnChangeCalled = true; } - void verifyOnConfigurationChangedCalled() { - assertTrue(mOnConfigurationChangedCalled); + void verifyOnChangeCalled() { + assertTrue(mOnChangeCalled); } - void verifyOnConfigurationChangedNotCalled() { - assertFalse(mOnConfigurationChangedCalled); + void verifyOnChangeNotCalled() { + assertFalse(mOnChangeCalled); } void reset() { - mOnConfigurationChangedCalled = false; + mOnChangeCalled = false; } }