diff options
author | 2020-08-24 19:47:49 +0000 | |
---|---|---|
committer | 2020-08-24 19:47:49 +0000 | |
commit | 1bb318e04e6d9b9c3957e01e63ee8a60e98df4d6 (patch) | |
tree | 476a293dc6fb3aa8d2c6da32da8d46b7ce69ea96 | |
parent | ee43e2f0581425d1a274fddb545e07da4aebfa95 (diff) | |
parent | a223581eee79357f71254dd41099f2a54f9cef87 (diff) |
Merge "Implement user-scoped geolocation configuration"
24 files changed, 1634 insertions, 1139 deletions
diff --git a/core/java/android/app/timezonedetector/ITimeZoneConfigurationListener.aidl b/core/java/android/app/timezonedetector/ITimeZoneConfigurationListener.aidl index af77fe05b902..6d0fe72b9de1 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 6e93af6a053b..4f7e1f62928a 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 cc0af3f97e49..09fffe9f4f25 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; * * <p>Actions have associated methods, see the documentation for each action for details. * - * <p>For configuration capabilities, the associated current configuration value can be retrieved - * using {@link TimeZoneDetector#getConfiguration()} and may be changed using - * {@link TimeZoneDetector#updateConfiguration(TimeZoneConfiguration)}. + * <p>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). * * <p>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)}. * * <p>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 6f84ee22a985..95db0a26cc6e 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. * - * <p>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. + * <p>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. * - * <p>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}. + * <p>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. * - * <p>See {@link #isComplete()} to tell if all known properties are present, and {@link - * #hasProperty(String)} with {@code PROPERTY_} constants for testing individual properties. + * <p>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. */ + /** Returns the ID of the user this configuration is associated with. */ + public @UserIdInt int getUserId() { + return mUserId; + } + + /** Returns {@code true} if all known settings are present. */ public boolean isComplete() { - return hasProperty(PROPERTY_AUTO_DETECTION_ENABLED) - && hasProperty(PROPERTY_GEO_DETECTION_ENABLED); + return hasSetting(SETTING_AUTO_DETECTION_ENABLED) + && hasSetting(SETTING_GEO_DETECTION_ENABLED); } - /** Returns true if the specified property is set. */ - public boolean hasProperty(@Property String property) { - return mBundle.containsKey(property); + /** 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. + * + * <p>This setting is global and can be updated by some users. * - * @throws IllegalStateException if the field has not been set + * @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. + * + * <p>This setting is user-scoped and can be updated by some users. + * See {@link TimeZoneCapabilities#getConfigureGeoDetectionEnabled()}. * - * @throws IllegalStateException if the field has not been set + * @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 7885613bfb59..2b1cbf259c55 100644 --- a/core/java/android/app/timezonedetector/TimeZoneDetector.java +++ b/core/java/android/app/timezonedetector/TimeZoneDetector.java @@ -37,37 +37,29 @@ public interface TimeZoneDetector { 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. * - * <p>Configuration properties vary in scope: some may be device-wide, others may be specific to - * the current user. + * <p>Configuration settings vary in scope: some may be global (affect all users), others may be + * specific to the current user. * - * <p>The ability to modify configuration properties can be subject to restrictions. For + * <p>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. * - * <p>Attempts to set configuration with capabilities that are {@link + * <p>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. * - * <p>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()}. + * <p>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 0770aff4e9bb..4c69732abec9 100644 --- a/core/java/android/app/timezonedetector/TimeZoneDetectorImpl.java +++ b/core/java/android/app/timezonedetector/TimeZoneDetectorImpl.java @@ -57,19 +57,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) { Log.d(TAG, "updateConfiguration called: " + configuration); @@ -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<TimeZoneConfigurationListener> 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 c302def19298..03cf0cf2ca78 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6404,6 +6404,17 @@ public final class Settings { 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 72391f4d7dec..db127c6cb9ed 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 00dc73ed269f..faf908de8d4a 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()); - - builder.setAutoDetectionEnabled(true); - 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.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 000000000000..1500cfaeb3a2 --- /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 000000000000..4c7b1f38dd5a --- /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 000000000000..aee3d8d3499b --- /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 0ca36e0fc258..d64032325539 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; - - TimeZoneDetectorCallbackImpl(Context context) { - mContext = context; + @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(@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); } - @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); + private void handleConfigChangeOnHandlerThread() { + if (mConfigChangeListener == null) { + Slog.wtf(LOG_TAG, "mConfigChangeListener is unexpectedly null"); } - return builder.build(); + mConfigChangeListener.onChange(); } @Override - public TimeZoneConfiguration getConfiguration(@UserIdInt int userId) { - return new TimeZoneConfiguration.Builder() - .setAutoDetectionEnabled(isAutoDetectionEnabled()) - .setGeoDetectionEnabled(isGeoDetectionEnabled()) - .build(); + public void setConfigChangeListener(@NonNull ConfigurationChangeListener listener) { + mConfigChangeListener = Objects.requireNonNull(listener); } @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; + public ConfigurationInternal getConfigurationInternal(@UserIdInt int userId) { + return new ConfigurationInternal.Builder(userId) + .setUserConfigAllowed(isUserConfigAllowed(userId)) + .setAutoDetectionSupported(isAutoDetectionSupported()) + .setAutoDetectionEnabled(isAutoDetectionEnabled()) + .setLocationEnabled(isLocationEnabled(userId)) + .setGeoDetectionEnabled(isGeoDetectionEnabled(userId)) + .build(); } @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 fb7a73d12632..2d50390c27a9 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 15412a0d14a1..f0ce827cec5e 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<ConfigurationChangeListener> 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 @@ -54,6 +62,19 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter } @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) { Objects.requireNonNull(timeZoneSuggestion); diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java index d81f949742bd..7501d9fe6a7c 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); } } @@ -103,79 +100,51 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub 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<ArrayList<ITimeZoneConfigurationListener>> mConfigurationListeners = - new SparseArray<>(); + private final ArrayList<ITimeZoneConfigurationListener> 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(); - } - }); - } - - @Override - @NonNull - public TimeZoneCapabilities getCapabilities() { - enforceManageTimeZoneDetectorConfigurationPermission(); - int userId = UserHandle.getCallingUserId(); - long token = Binder.clearCallingIdentity(); - try { - return mTimeZoneDetectorStrategy.getCapabilities(userId); - } finally { - Binder.restoreCallingIdentity(token); - } + // Wire up a change listener so that ITimeZoneConfigurationListeners can be notified when + // the configuration changes for any reason. + mTimeZoneDetectorStrategy.addConfigChangeListener(this::handleConfigurationChanged); } @Override @NonNull - public TimeZoneConfiguration getConfiguration() { + public TimeZoneCapabilities getCapabilities() { enforceManageTimeZoneDetectorConfigurationPermission(); - int userId = UserHandle.getCallingUserId(); - long token = Binder.clearCallingIdentity(); + int userId = mCallerIdentityInjector.getCallingUserId(); + long token = mCallerIdentityInjector.clearCallingIdentity(); try { - return mTimeZoneDetectorStrategy.getConfiguration(userId); + return mTimeZoneDetectorStrategy.getConfigurationInternal(userId).createCapabilities(); } 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<ITimeZoneConfigurationListener> 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<ITimeZoneConfigurationListener> 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<ITimeZoneConfigurationListener> userListeners = - mConfigurationListeners.valueAt(i); - Iterator<ITimeZoneConfigurationListener> 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<ITimeZoneConfigurationListener> 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 c5b7e39f4fef..f944c5638fa9 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}. * - * <p>The strategy uses suggestions to decide whether to modify the device's time zone setting - * and what to set it to. + * <p>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}). * - * <p>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. + * <p>Devices can have zero, one or two automatic time zone detection algorithm available at any + * point in time. + * + * <p>The two automatic detection algorithms supported are "telephony" and "geolocation". Algorithm + * availability and use depends on several factors: + * <ul> + * <li>Telephony is only available on devices with a telephony stack. + * <li>Geolocation is also optional and configured at image creation time. When enabled on a + * device, its availability depends on the current user's settings, so switching between users can + * change the automatic algorithm used by the device.</li> + * </ul> + * + * <p>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. + * + * <p>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. + * + * <p>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. + * + * <p>Threading: + * + * <p>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 the listener that enables the strategy to communicate with the surrounding service. */ - void setStrategyListener(@NonNull StrategyListener listener); + /** + * Sets a listener that will be triggered whenever time zone detection configuration is + * changed. + */ + void addConfigChangeListener(@NonNull ConfigurationChangeListener 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. + * + * <p>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 d1369a289428..8a42b18b514f 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. - * - * <p>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}. * * <p>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. * * <p>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. - */ - @NonNull - TimeZoneCapabilities getCapabilities(@UserIdInt int userId); - - /** - * Returns the configuration for the user. - * @param userId + * 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 - TimeZoneConfiguration getConfiguration(int userId); + void setConfigChangeListener(@NonNull ConfigurationChangeListener listener); - /** - * 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 the current user at the instant it is called. */ + @UserIdInt int getCurrentUserId(); - /** - * 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<ConfigurationChangeListener> 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<GeolocationTimeZoneSuggestion> 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); - - // 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)) { + @NonNull TimeZoneConfiguration requestedConfiguration) { + Objects.requireNonNull(requestedConfiguration); + + 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(); - - // 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(); + // Store the configuration / notify as needed. This will cause the mCallback to invoke + // handleConfigChanged() asynchronously. + mCallback.storeConfiguration(newConfiguration); + + 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()"); + } + + 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("handleAutoTimeZoneConfigChanged()"); + 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 000000000000..d7ed96fd5833 --- /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 e5e931115c05..4ef20829f2dc 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<Dumpable> 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 ConfigurationInternal getConfigurationInternal(int userId) { + if (mConfigurationInternal.getUserId() != userId) { + fail("Fake only supports one user"); + } + return mConfigurationInternal; } @Override - public TimeZoneCapabilities getCapabilities(@UserIdInt int userId) { - return mCapabilities; + public ConfigurationInternal getCurrentUserConfigurationInternal() { + return mConfigurationInternal; } @Override - public boolean updateConfiguration( - @UserIdInt int userId, @NonNull TimeZoneConfiguration configuration) { - assertNotNull(mConfiguration); - assertNotNull(configuration); - - // 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(); - - if (newConfiguration.equals(oldConfiguration)) { + public boolean updateConfiguration(@NonNull TimeZoneConfiguration requestedChanges) { + assertNotNull(mConfigurationInternal); + assertNotNull(requestedChanges); + + // 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 @@ -103,11 +112,6 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy { } @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 000000000000..f45b3a822f1a --- /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 e9d57e52ce69..918babca677e 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 3a1ec4f90d7a..27b04b6ab17d 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 a6caa4299ef6..2bee5e5e7295 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() + private static final ConfigurationInternal CONFIG_INT_USER_RESTRICTED_AUTO_DISABLED = + new ConfigurationInternal.Builder(USER_ID) + .setUserConfigAllowed(false) + .setAutoDetectionSupported(true) + .setAutoDetectionEnabled(false) + .setLocationEnabled(true) + .setGeoDetectionEnabled(false) + .build(); + + 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 TimeZoneConfiguration CONFIG_AUTO_DISABLED = - new TimeZoneConfiguration.Builder() + 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 TimeZoneConfiguration CONFIG_GEO_DETECTION_DISABLED = - new TimeZoneConfiguration.Builder() + 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 TimeZoneConfiguration CONFIG_GEO_DETECTION_ENABLED = - new TimeZoneConfiguration.Builder() + 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); - } - - @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)); + mTimeZoneDetectorStrategy.addConfigChangeListener(mMockConfigChangeListener); } @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(); - - // 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"); + // Assert internal service state. + assertEquals(suggestion, mTimeZoneDetectorStrategy.getLatestGeolocationSuggestion()); - // 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<UserConfiguration> mConfiguration = new TestState<>(); + private final TestState<ConfigurationInternal> mConfigurationInternal = new TestState<>(); private final TestState<String> mTimeZoneId = new TestState<>(); - private TimeZoneDetectorStrategyImpl mStrategy; - - void setStrategyForSettingsCallbacks(TimeZoneDetectorStrategyImpl strategy) { - assertNotNull(strategy); - mStrategy = strategy; - } + private ConfigurationChangeListener mConfigChangeListener; - 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; } } |