Merge "Implement user-scoped geolocation configuration"

This commit is contained in:
Neil Fuller
2020-08-24 19:47:49 +00:00
committed by Android (Google) Code Review
24 changed files with 1636 additions and 1141 deletions

View File

@@ -20,5 +20,5 @@ import android.app.timezonedetector.TimeZoneConfiguration;
/** {@hide} */
oneway interface ITimeZoneConfigurationListener {
void onChange(in TimeZoneConfiguration configuration);
void onChange();
}

View File

@@ -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);
}

View File

@@ -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. */

View File

@@ -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. */
public boolean isComplete() {
return hasProperty(PROPERTY_AUTO_DETECTION_ENABLED)
&& hasProperty(PROPERTY_GEO_DETECTION_ENABLED);
/** Returns the ID of the user this configuration is associated with. */
public @UserIdInt int getUserId() {
return mUserId;
}
/** Returns true if the specified property is set. */
public boolean hasProperty(@Property String property) {
return mBundle.containsKey(property);
/** Returns {@code true} if all known settings are present. */
public boolean isComplete() {
return hasSetting(SETTING_AUTO_DETECTION_ENABLED)
&& hasSetting(SETTING_GEO_DETECTION_ENABLED);
}
/** Returns true if the specified setting is set. */
public boolean hasSetting(@Setting String setting) {
return mBundle.containsKey(setting);
}
/**
* Returns the value of the {@link #PROPERTY_AUTO_DETECTION_ENABLED} property. This
* Returns the value of the {@link #SETTING_AUTO_DETECTION_ENABLED} setting. This
* controls whether a device will attempt to determine the time zone automatically using
* contextual information.
* contextual information if the device supports auto detection.
*
* @throws IllegalStateException if the field has not been set
* <p>This setting is global and can be updated by some users.
*
* @throws IllegalStateException if the setting has not been set
*/
public boolean isAutoDetectionEnabled() {
if (!mBundle.containsKey(PROPERTY_AUTO_DETECTION_ENABLED)) {
throw new IllegalStateException(PROPERTY_AUTO_DETECTION_ENABLED + " is not set");
}
return mBundle.getBoolean(PROPERTY_AUTO_DETECTION_ENABLED);
enforceSettingPresent(SETTING_AUTO_DETECTION_ENABLED);
return mBundle.getBoolean(SETTING_AUTO_DETECTION_ENABLED);
}
/**
* Returns the value of the {@link #PROPERTY_GEO_DETECTION_ENABLED} property. This
* controls whether a device can use location to determine time zone. Only used when
* {@link #isAutoDetectionEnabled()} is true.
* Returns the value of the {@link #SETTING_GEO_DETECTION_ENABLED} setting. This
* controls whether a device can use geolocation to determine time zone. Only used when
* {@link #isAutoDetectionEnabled()} is {@code true} and when the user has allowed their
* location to be used.
*
* @throws IllegalStateException if the field has not been set
* <p>This setting is user-scoped and can be updated by some users.
* See {@link TimeZoneCapabilities#getConfigureGeoDetectionEnabled()}.
*
* @throws IllegalStateException if the setting has not been set
*/
public boolean isGeoDetectionEnabled() {
if (!mBundle.containsKey(PROPERTY_GEO_DETECTION_ENABLED)) {
throw new IllegalStateException(PROPERTY_GEO_DETECTION_ENABLED + " is not set");
}
return mBundle.getBoolean(PROPERTY_GEO_DETECTION_ENABLED);
}
/**
* Convenience method to merge this with another. The argument configuration properties have
* precedence.
*/
public TimeZoneConfiguration with(TimeZoneConfiguration other) {
return new Builder(this).mergeProperties(other).build();
enforceSettingPresent(SETTING_GEO_DETECTION_ENABLED);
return mBundle.getBoolean(SETTING_GEO_DETECTION_ENABLED);
}
@Override
@@ -149,43 +151,61 @@ public final class TimeZoneConfiguration implements Parcelable {
return false;
}
TimeZoneConfiguration that = (TimeZoneConfiguration) o;
return mBundle.kindofEquals(that.mBundle);
return mUserId == that.mUserId
&& mBundle.kindofEquals(that.mBundle);
}
@Override
public int hashCode() {
return Objects.hash(mBundle);
return Objects.hash(mUserId, mBundle);
}
@Override
public String toString() {
return "TimeZoneDetectorConfiguration{"
+ "mUserId=" + mUserId
+ "mBundle=" + mBundle
+ '}';
}
private void enforceSettingPresent(@Setting String setting) {
if (!mBundle.containsKey(setting)) {
throw new IllegalStateException(setting + " is not set");
}
}
/** @hide */
public static class Builder {
private Bundle mBundle = new Bundle();
private final @UserIdInt int mUserId;
private final Bundle mBundle = new Bundle();
/**
* Creates a new Builder with no properties set.
* Creates a new Builder for a userId with no settings held.
*/
public Builder() {}
public Builder(@UserIdInt int userId) {
mUserId = userId;
}
/**
* Creates a new Builder by copying properties from an existing instance.
* Creates a new Builder by copying the user ID and settings from an existing instance.
*/
public Builder(TimeZoneConfiguration toCopy) {
this.mUserId = toCopy.mUserId;
mergeProperties(toCopy);
}
/**
* Merges {@code other} properties into this instances, replacing existing values in this
* where the properties appear in both.
* Merges {@code other} settings into this instances, replacing existing values in this
* where the settings appear in both.
*/
public Builder mergeProperties(TimeZoneConfiguration other) {
if (mUserId != other.mUserId) {
throw new IllegalArgumentException(
"Cannot merge configurations for different user IDs."
+ " this.mUserId=" + this.mUserId
+ ", other.mUserId=" + other.mUserId);
}
this.mBundle.putAll(other.mBundle);
return this;
}
@@ -195,15 +215,19 @@ public final class TimeZoneConfiguration implements Parcelable {
return this;
}
/** Sets the desired state of the automatic time zone detection property. */
/**
* Sets the state of the {@link #SETTING_AUTO_DETECTION_ENABLED} setting.
*/
public Builder setAutoDetectionEnabled(boolean enabled) {
this.mBundle.putBoolean(PROPERTY_AUTO_DETECTION_ENABLED, enabled);
this.mBundle.putBoolean(SETTING_AUTO_DETECTION_ENABLED, enabled);
return this;
}
/** Sets the desired state of the geolocation time zone detection enabled property. */
/**
* Sets the state of the {@link #SETTING_GEO_DETECTION_ENABLED} setting.
*/
public Builder setGeoDetectionEnabled(boolean enabled) {
this.mBundle.putBoolean(PROPERTY_GEO_DETECTION_ENABLED, enabled);
this.mBundle.putBoolean(SETTING_GEO_DETECTION_ENABLED, enabled);
return this;
}

View File

@@ -36,38 +36,30 @@ public interface TimeZoneDetector {
@NonNull
TimeZoneCapabilities getCapabilities();
/**
* Returns the current user's complete time zone configuration. See {@link
* TimeZoneConfiguration}.
*/
@RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
@NonNull
TimeZoneConfiguration getConfiguration();
/**
* Modifies the time zone detection configuration.
*
* <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);

View File

@@ -56,19 +56,6 @@ public final class TimeZoneDetectorImpl implements TimeZoneDetector {
}
}
@Override
@NonNull
public TimeZoneConfiguration getConfiguration() {
if (DEBUG) {
Log.d(TAG, "getConfiguration called");
}
try {
return mITimeZoneDetectorService.getConfiguration();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@Override
public boolean updateConfiguration(@NonNull TimeZoneConfiguration configuration) {
if (DEBUG) {
@@ -94,8 +81,8 @@ public final class TimeZoneDetectorImpl implements TimeZoneDetector {
ITimeZoneConfigurationListener iListener =
new ITimeZoneConfigurationListener.Stub() {
@Override
public void onChange(@NonNull TimeZoneConfiguration configuration) {
notifyConfigurationListeners(configuration);
public void onChange() {
notifyConfigurationListeners();
}
};
mConfigurationReceiver = iListener;
@@ -116,14 +103,14 @@ public final class TimeZoneDetectorImpl implements TimeZoneDetector {
}
}
private void notifyConfigurationListeners(@NonNull TimeZoneConfiguration configuration) {
private void notifyConfigurationListeners() {
final ArraySet<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();
}
}

View File

@@ -6403,6 +6403,17 @@ public final class Settings {
@SystemApi
public static final int LOCATION_MODE_ON = LOCATION_MODE_HIGH_ACCURACY;
/**
* The current location time zone detection enabled state for the user.
*
* See {@link
* android.app.timezonedetector.TimeZoneDetector#getCapabilities} for access. See {@link
* android.app.timezonedetector.TimeZoneDetector#updateConfiguration} to update.
* @hide
*/
public static final String LOCATION_TIME_ZONE_DETECTION_ENABLED =
"location_time_zone_detection_enabled";
/**
* The accuracy in meters used for coarsening location for clients with only the coarse
* location permission.

View File

@@ -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));
}
}

View File

@@ -27,11 +27,14 @@ import org.junit.Test;
public class TimeZoneConfigurationTest {
private static final int ARBITRARY_USER_ID = 9876;
@Test
public void testBuilder_copyConstructor() {
TimeZoneConfiguration.Builder builder1 = new TimeZoneConfiguration.Builder()
.setAutoDetectionEnabled(true)
.setGeoDetectionEnabled(true);
TimeZoneConfiguration.Builder builder1 =
new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID)
.setAutoDetectionEnabled(true)
.setGeoDetectionEnabled(true);
TimeZoneConfiguration configuration1 = builder1.build();
TimeZoneConfiguration configuration2 =
@@ -41,28 +44,28 @@ public class TimeZoneConfigurationTest {
}
@Test
public void testIsComplete() {
TimeZoneConfiguration.Builder builder =
new TimeZoneConfiguration.Builder();
assertFalse(builder.build().isComplete());
public void testIntrospectionMethods() {
TimeZoneConfiguration empty = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID).build();
assertFalse(empty.isComplete());
assertFalse(empty.hasSetting(TimeZoneConfiguration.SETTING_AUTO_DETECTION_ENABLED));
builder.setAutoDetectionEnabled(true);
assertFalse(builder.build().isComplete());
builder.setGeoDetectionEnabled(true);
assertTrue(builder.build().isComplete());
TimeZoneConfiguration completeConfig = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID)
.setAutoDetectionEnabled(true)
.setGeoDetectionEnabled(true)
.build();
assertTrue(completeConfig.isComplete());
assertTrue(completeConfig.hasSetting(TimeZoneConfiguration.SETTING_AUTO_DETECTION_ENABLED));
}
@Test
public void testBuilder_mergeProperties() {
TimeZoneConfiguration configuration1 =
new TimeZoneConfiguration.Builder()
.setAutoDetectionEnabled(true)
.build();
TimeZoneConfiguration configuration1 = new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID)
.setAutoDetectionEnabled(true)
.build();
{
TimeZoneConfiguration mergedEmptyAnd1 =
new TimeZoneConfiguration.Builder()
new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID)
.mergeProperties(configuration1)
.build();
assertEquals(configuration1, mergedEmptyAnd1);
@@ -70,7 +73,7 @@ public class TimeZoneConfigurationTest {
{
TimeZoneConfiguration configuration2 =
new TimeZoneConfiguration.Builder()
new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID)
.setAutoDetectionEnabled(false)
.build();
@@ -87,14 +90,22 @@ public class TimeZoneConfigurationTest {
@Test
public void testEquals() {
TimeZoneConfiguration.Builder builder1 =
new TimeZoneConfiguration.Builder();
new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID);
{
TimeZoneConfiguration one = builder1.build();
assertEquals(one, one);
}
{
TimeZoneConfiguration.Builder differentUserBuilder =
new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID + 1);
TimeZoneConfiguration one = builder1.build();
TimeZoneConfiguration two = differentUserBuilder.build();
assertNotEquals(one, two);
}
TimeZoneConfiguration.Builder builder2 =
new TimeZoneConfiguration.Builder();
new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID);
{
TimeZoneConfiguration one = builder1.build();
TimeZoneConfiguration two = builder2.build();
@@ -148,7 +159,7 @@ public class TimeZoneConfigurationTest {
@Test
public void testParcelable() {
TimeZoneConfiguration.Builder builder =
new TimeZoneConfiguration.Builder();
new TimeZoneConfiguration.Builder(ARBITRARY_USER_ID);
assertRoundTripParcelable(builder.build());
builder.setAutoDetectionEnabled(true);

View File

@@ -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);
}
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}
}

View File

@@ -16,24 +16,30 @@
package com.android.server.timezonedetector;
import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_ALLOWED;
import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_APPLICABLE;
import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_NOT_SUPPORTED;
import static android.app.timezonedetector.TimeZoneCapabilities.CAPABILITY_POSSESSED;
import static android.content.Intent.ACTION_USER_SWITCHED;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.AlarmManager;
import android.app.timezonedetector.TimeZoneCapabilities;
import android.app.timezonedetector.TimeZoneConfiguration;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.location.LocationManager;
import android.net.ConnectivityManager;
import android.os.Handler;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.util.Slog;
import com.android.server.LocalServices;
import java.util.Objects;
@@ -42,103 +48,87 @@ import java.util.Objects;
*/
public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrategyImpl.Callback {
private static final String LOG_TAG = "TimeZoneDetectorCallbackImpl";
private static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
private final Context mContext;
private final ContentResolver mCr;
private final UserManager mUserManager;
@NonNull private final Context mContext;
@NonNull private final Handler mHandler;
@NonNull private final ContentResolver mCr;
@NonNull private final UserManager mUserManager;
@NonNull private final boolean mGeoDetectionFeatureEnabled;
@NonNull private final LocationManager mLocationManager;
// @NonNull after setConfigChangeListener() is called.
private ConfigurationChangeListener mConfigChangeListener;
TimeZoneDetectorCallbackImpl(Context context) {
mContext = context;
TimeZoneDetectorCallbackImpl(@NonNull Context context, @NonNull Handler handler,
boolean geoDetectionFeatureEnabled) {
mContext = Objects.requireNonNull(context);
mHandler = Objects.requireNonNull(handler);
mCr = context.getContentResolver();
mUserManager = context.getSystemService(UserManager.class);
mLocationManager = context.getSystemService(LocationManager.class);
mGeoDetectionFeatureEnabled = geoDetectionFeatureEnabled;
// Wire up the change listener. All invocations are performed on the mHandler thread.
// Listen for the user changing / the user's location mode changing.
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_USER_SWITCHED);
filter.addAction(LocationManager.MODE_CHANGED_ACTION);
mContext.registerReceiverForAllUsers(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
handleConfigChangeOnHandlerThread();
}
}, filter, null, mHandler);
// Add async callbacks for global settings being changed.
ContentResolver contentResolver = mContext.getContentResolver();
contentResolver.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
new ContentObserver(mHandler) {
public void onChange(boolean selfChange) {
handleConfigChangeOnHandlerThread();
}
});
// Add async callbacks for user scoped location settings being changed.
contentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.LOCATION_TIME_ZONE_DETECTION_ENABLED),
true,
new ContentObserver(mHandler) {
public void onChange(boolean selfChange) {
handleConfigChangeOnHandlerThread();
}
}, UserHandle.USER_ALL);
}
private void handleConfigChangeOnHandlerThread() {
if (mConfigChangeListener == null) {
Slog.wtf(LOG_TAG, "mConfigChangeListener is unexpectedly null");
}
mConfigChangeListener.onChange();
}
@Override
public TimeZoneCapabilities getCapabilities(@UserIdInt int userId) {
UserHandle userHandle = UserHandle.of(userId);
boolean disallowConfigDateTime =
mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_DATE_TIME, userHandle);
TimeZoneCapabilities.Builder builder = new TimeZoneCapabilities.Builder(userId);
// Automatic time zone detection is only supported (currently) on devices if there is a
// telephony network available.
if (!deviceHasTelephonyNetwork()) {
builder.setConfigureAutoDetectionEnabled(CAPABILITY_NOT_SUPPORTED);
} else if (disallowConfigDateTime) {
builder.setConfigureAutoDetectionEnabled(CAPABILITY_NOT_ALLOWED);
} else {
builder.setConfigureAutoDetectionEnabled(CAPABILITY_POSSESSED);
}
// TODO(b/149014708) Replace this with real logic when the settings storage is fully
// implemented.
builder.setConfigureGeoDetectionEnabled(CAPABILITY_NOT_SUPPORTED);
// The ability to make manual time zone suggestions can also be restricted by policy. With
// the current logic above, this could lead to a situation where a device hardware does not
// support auto detection, the device has been forced into "auto" mode by an admin and the
// user is unable to disable auto detection.
if (disallowConfigDateTime) {
builder.setSuggestManualTimeZone(CAPABILITY_NOT_ALLOWED);
} else if (isAutoDetectionEnabled()) {
builder.setSuggestManualTimeZone(CAPABILITY_NOT_APPLICABLE);
} else {
builder.setSuggestManualTimeZone(CAPABILITY_POSSESSED);
}
return builder.build();
public void setConfigChangeListener(@NonNull ConfigurationChangeListener listener) {
mConfigChangeListener = Objects.requireNonNull(listener);
}
@Override
public TimeZoneConfiguration getConfiguration(@UserIdInt int userId) {
return new TimeZoneConfiguration.Builder()
public ConfigurationInternal getConfigurationInternal(@UserIdInt int userId) {
return new ConfigurationInternal.Builder(userId)
.setUserConfigAllowed(isUserConfigAllowed(userId))
.setAutoDetectionSupported(isAutoDetectionSupported())
.setAutoDetectionEnabled(isAutoDetectionEnabled())
.setGeoDetectionEnabled(isGeoDetectionEnabled())
.setLocationEnabled(isLocationEnabled(userId))
.setGeoDetectionEnabled(isGeoDetectionEnabled(userId))
.build();
}
@Override
public void setConfiguration(
@UserIdInt int userId, @NonNull TimeZoneConfiguration configuration) {
Objects.requireNonNull(configuration);
if (!configuration.isComplete()) {
throw new IllegalArgumentException("configuration=" + configuration + " not complete");
}
// Avoid writing auto detection config for devices that do not support auto time zone
// detection: if we wrote it down then we'd set the default explicitly. That might influence
// what happens on later releases that do support auto detection on the same hardware.
if (isAutoDetectionSupported()) {
final int autoEnabledValue = configuration.isAutoDetectionEnabled() ? 1 : 0;
Settings.Global.putInt(mCr, Settings.Global.AUTO_TIME_ZONE, autoEnabledValue);
final boolean geoTzDetectionEnabledValue = configuration.isGeoDetectionEnabled();
// TODO(b/149014708) Write this down to user-scoped settings once implemented.
}
}
@Override
public boolean isAutoDetectionEnabled() {
// To ensure that TimeZoneConfiguration is "complete" for simplicity, devices that do not
// support auto detection have safe, hard coded configuration values that make it look like
// auto detection is turned off. It is therefore important that false is returned from this
// method for devices that do not support auto time zone detection. Such devices will not
// have a UI to turn the auto detection on/off. Returning true could prevent the user
// entering information manually. On devices that do support auto time detection the default
// is to turn auto detection on.
if (isAutoDetectionSupported()) {
return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE, 1 /* default */) > 0;
}
return false;
}
@Override
public boolean isGeoDetectionEnabled() {
// TODO(b/149014708) Read this from user-scoped settings once implemented. The user's
// location toggle will act as an override for this setting, i.e. so that the setting will
// return false if the location toggle is disabled.
return false;
public @UserIdInt int getCurrentUserId() {
return LocalServices.getService(ActivityManagerInternal.class).getCurrentUserId();
}
@Override
@@ -165,8 +155,55 @@ public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrat
alarmManager.setTimeZone(zoneId);
}
@Override
public void storeConfiguration(TimeZoneConfiguration configuration) {
Objects.requireNonNull(configuration);
// Avoid writing the auto detection enabled setting for devices that do not support auto
// time zone detection: if we wrote it down then we'd set the value explicitly, which would
// prevent detecting "default" later. That might influence what happens on later releases
// that support new types of auto detection on the same hardware.
if (isAutoDetectionSupported()) {
final boolean autoDetectionEnabled = configuration.isAutoDetectionEnabled();
setAutoDetectionEnabled(autoDetectionEnabled);
final int userId = configuration.getUserId();
final boolean geoTzDetectionEnabled = configuration.isGeoDetectionEnabled();
setGeoDetectionEnabled(userId, geoTzDetectionEnabled);
}
}
private boolean isUserConfigAllowed(@UserIdInt int userId) {
UserHandle userHandle = UserHandle.of(userId);
return !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_DATE_TIME, userHandle);
}
private boolean isAutoDetectionSupported() {
return deviceHasTelephonyNetwork();
return deviceHasTelephonyNetwork() || mGeoDetectionFeatureEnabled;
}
private boolean isAutoDetectionEnabled() {
return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE, 1 /* default */) > 0;
}
private void setAutoDetectionEnabled(boolean enabled) {
Settings.Global.putInt(mCr, Settings.Global.AUTO_TIME_ZONE, enabled ? 1 : 0);
}
private boolean isLocationEnabled(@UserIdInt int userId) {
return mLocationManager.isLocationEnabledForUser(UserHandle.of(userId));
}
private boolean isGeoDetectionEnabled(@UserIdInt int userId) {
final boolean locationEnabled = isLocationEnabled(userId);
return Settings.Secure.getIntForUser(mCr,
Settings.Secure.LOCATION_TIME_ZONE_DETECTION_ENABLED,
locationEnabled ? 1 : 0 /* defaultValue */, userId) != 0;
}
private void setGeoDetectionEnabled(@UserIdInt int userId, boolean enabled) {
Settings.Secure.putIntForUser(mCr, Settings.Secure.LOCATION_TIME_ZONE_DETECTION_ENABLED,
enabled ? 1 : 0, userId);
}
private boolean deviceHasTelephonyNetwork() {
@@ -174,4 +211,4 @@ public final class TimeZoneDetectorCallbackImpl implements TimeZoneDetectorStrat
return mContext.getSystemService(ConnectivityManager.class)
.isNetworkSupported(ConnectivityManager.TYPE_MOBILE);
}
}
}

View File

@@ -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

View File

@@ -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
@@ -53,6 +61,19 @@ public final class TimeZoneDetectorInternalImpl implements TimeZoneDetectorInter
mTimeZoneDetectorStrategy.addDumpable(dumpable);
}
@Override
public void addConfigurationListener(ConfigurationChangeListener listener) {
synchronized (mConfigurationListeners) {
mConfigurationListeners.add(Objects.requireNonNull(listener));
}
}
@Override
@NonNull
public ConfigurationInternal getCurrentUserConfigurationInternal() {
return mTimeZoneDetectorStrategy.getCurrentUserConfigurationInternal();
}
@Override
public void suggestGeolocationTimeZone(
@NonNull GeolocationTimeZoneSuggestion timeZoneSuggestion) {

View File

@@ -24,32 +24,24 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion;
import android.app.timezonedetector.TelephonyTimeZoneSuggestion;
import android.app.timezonedetector.TimeZoneCapabilities;
import android.app.timezonedetector.TimeZoneConfiguration;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.server.FgThread;
import com.android.server.SystemService;
import com.android.server.timezonedetector.TimeZoneDetectorStrategy.StrategyListener;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
/**
@@ -65,6 +57,9 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
private static final String TAG = "TimeZoneDetectorService";
/** A compile time switch for enabling / disabling geolocation-based time zone detection. */
private static final boolean GEOLOCATION_TIME_ZONE_DETECTION_ENABLED = false;
/**
* Handles the service lifecycle for {@link TimeZoneDetectorService} and
* {@link TimeZoneDetectorInternalImpl}.
@@ -80,18 +75,20 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
// Obtain / create the shared dependencies.
Context context = getContext();
Handler handler = FgThread.getHandler();
TimeZoneDetectorStrategy timeZoneDetectorStrategy =
TimeZoneDetectorStrategyImpl.create(context);
TimeZoneDetectorStrategyImpl.create(
context, handler, GEOLOCATION_TIME_ZONE_DETECTION_ENABLED);
// Create and publish the local service for use by internal callers.
TimeZoneDetectorInternal internal =
TimeZoneDetectorInternalImpl.create(context, handler, timeZoneDetectorStrategy);
new TimeZoneDetectorInternalImpl(context, handler, timeZoneDetectorStrategy);
publishLocalService(TimeZoneDetectorInternal.class, internal);
// Publish the binder service so it can be accessed from other (appropriately
// permissioned) processes.
TimeZoneDetectorService service =
TimeZoneDetectorService.create(context, handler, timeZoneDetectorStrategy);
TimeZoneDetectorService service = TimeZoneDetectorService.create(
context, handler, timeZoneDetectorStrategy);
publishBinderService(Context.TIME_ZONE_DETECTOR_SERVICE, service);
}
}
@@ -102,53 +99,39 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
@NonNull
private final Handler mHandler;
@NonNull
private final CallerIdentityInjector mCallerIdentityInjector;
@NonNull
private final TimeZoneDetectorStrategy mTimeZoneDetectorStrategy;
/**
* This sparse array acts as a map from userId to listeners running as that userId. User scoped
* as time zone detection configuration is partially user-specific, so different users can
* get different configuration.
*/
@GuardedBy("mConfigurationListeners")
@NonNull
private final SparseArray<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();
}
});
// Wire up a change listener so that ITimeZoneConfigurationListeners can be notified when
// the configuration changes for any reason.
mTimeZoneDetectorStrategy.addConfigChangeListener(this::handleConfigurationChanged);
}
@Override
@@ -156,26 +139,12 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
public TimeZoneCapabilities getCapabilities() {
enforceManageTimeZoneDetectorConfigurationPermission();
int userId = UserHandle.getCallingUserId();
long token = Binder.clearCallingIdentity();
int userId = mCallerIdentityInjector.getCallingUserId();
long token = mCallerIdentityInjector.clearCallingIdentity();
try {
return mTimeZoneDetectorStrategy.getCapabilities(userId);
return mTimeZoneDetectorStrategy.getConfigurationInternal(userId).createCapabilities();
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
@NonNull
public TimeZoneConfiguration getConfiguration() {
enforceManageTimeZoneDetectorConfigurationPermission();
int userId = UserHandle.getCallingUserId();
long token = Binder.clearCallingIdentity();
try {
return mTimeZoneDetectorStrategy.getConfiguration(userId);
} finally {
Binder.restoreCallingIdentity(token);
mCallerIdentityInjector.restoreCallingIdentity(token);
}
}
@@ -184,12 +153,16 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
enforceManageTimeZoneDetectorConfigurationPermission();
Objects.requireNonNull(configuration);
int userId = UserHandle.getCallingUserId();
long token = Binder.clearCallingIdentity();
int callingUserId = mCallerIdentityInjector.getCallingUserId();
if (callingUserId != configuration.getUserId()) {
return false;
}
long token = mCallerIdentityInjector.clearCallingIdentity();
try {
return mTimeZoneDetectorStrategy.updateConfiguration(userId, configuration);
return mTimeZoneDetectorStrategy.updateConfiguration(configuration);
} finally {
Binder.restoreCallingIdentity(token);
mCallerIdentityInjector.restoreCallingIdentity(token);
}
}
@@ -197,25 +170,17 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub
public void addConfigurationListener(@NonNull ITimeZoneConfigurationListener listener) {
enforceManageTimeZoneDetectorConfigurationPermission();
Objects.requireNonNull(listener);
int userId = UserHandle.getCallingUserId();
synchronized (mConfigurationListeners) {
ArrayList<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(

View File

@@ -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 a listener that will be triggered whenever time zone detection configuration is
* changed.
*/
void addConfigChangeListener(@NonNull ConfigurationChangeListener listener);
/** Sets the listener that enables the strategy to communicate with the surrounding service. */
void setStrategyListener(@NonNull StrategyListener listener);
/** Returns the user's time zone capabilities. */
/** Returns the user's time zone configuration. */
@NonNull
TimeZoneCapabilities getCapabilities(@UserIdInt int userId);
ConfigurationInternal getConfigurationInternal(@UserIdInt int userId);
/**
* Returns the configuration that controls time zone detector behavior.
* Returns the configuration that controls time zone detector behavior for the current user.
*/
@NonNull
TimeZoneConfiguration getConfiguration(@UserIdInt int userId);
ConfigurationInternal getCurrentUserConfigurationInternal();
/**
* Updates the configuration settings that control time zone detector behavior.
* Updates the configuration properties that control a device's time zone behavior.
*
* <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();
}

View File

@@ -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.
* Sets a {@link ConfigurationChangeListener} that will be invoked when there are any
* changes that could affect time zone detection. This is invoked during system server
* setup.
*/
@NonNull
TimeZoneCapabilities getCapabilities(@UserIdInt int userId);
void setConfigChangeListener(@NonNull ConfigurationChangeListener listener);
/**
* Returns the configuration for the user.
* @param userId
*/
@NonNull
TimeZoneConfiguration getConfiguration(int userId);
/** Returns the current user at the instant it is called. */
@UserIdInt int getCurrentUserId();
/**
* Sets the configuration for the user. This method handles storage only, the configuration
* must have been validated by the caller and be complete.
*
* @throws IllegalArgumentException if {@link TimeZoneConfiguration#isComplete()}
* returns {@code false}
*/
void setConfiguration(@UserIdInt int userId, @NonNull TimeZoneConfiguration configuration);
/**
* Returns true if automatic time zone detection is currently enabled.
*/
boolean isAutoDetectionEnabled();
/**
* Returns whether geolocation can be used for time zone detection when {@link
* #isAutoDetectionEnabled()} returns {@code true}.
*/
boolean isGeoDetectionEnabled();
/** Returns the {@link ConfigurationInternal} for the specified user. */
ConfigurationInternal getConfigurationInternal(@UserIdInt int userId);
/**
* Returns true if the device has had an explicit time zone set.
@@ -118,6 +87,13 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
* Sets the device's time zone.
*/
void setDeviceTimeZone(@NonNull String zoneId);
/**
* Stores the configuration properties contained in {@code newConfiguration}.
* All checks about user capabilities must be done by the caller and
* {@link TimeZoneConfiguration#isComplete()} must be {@code true}.
*/
void storeConfiguration(TimeZoneConfiguration newConfiguration);
}
private static final String LOG_TAG = "TimeZoneDetectorStrategy";
@@ -189,9 +165,9 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
@NonNull
private final Callback mCallback;
/** Non-null after {@link #setStrategyListener(StrategyListener)} is called. */
@Nullable
private StrategyListener mListener;
@GuardedBy("this")
@NonNull
private List<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);
@NonNull TimeZoneConfiguration requestedConfiguration) {
Objects.requireNonNull(requestedConfiguration);
// Validate the requested configuration changes before applying any of them.
TimeZoneCapabilities capabilities = mCallback.getCapabilities(userId);
boolean canManageTimeZoneDetection =
capabilities.getConfigureAutoDetectionEnabled() >= CAPABILITY_NOT_APPLICABLE;
if (!canManageTimeZoneDetection
&& containsAutoTimeDetectionProperties(configurationChanges)) {
int userId = requestedConfiguration.getUserId();
TimeZoneCapabilities capabilities = getConfigurationInternal(userId).createCapabilities();
// Create a new configuration builder, and copy across the mutable properties users are
// able to modify. Other properties are therefore ignored.
final TimeZoneConfiguration newConfiguration =
capabilities.applyUpdate(requestedConfiguration);
if (newConfiguration == null) {
// The changes could not be made due to
return false;
}
// Create a complete configuration by merging the existing and new (possibly partial)
// configuration.
final TimeZoneConfiguration oldConfiguration = mCallback.getConfiguration(userId);
final TimeZoneConfiguration newConfiguration =
new TimeZoneConfiguration.Builder(oldConfiguration)
.mergeProperties(configurationChanges)
.build();
// Store the configuration / notify as needed. This will cause the mCallback to invoke
// handleConfigChanged() asynchronously.
mCallback.storeConfiguration(newConfiguration);
// Set the configuration / notify as needed.
boolean configurationChanged = !oldConfiguration.equals(newConfiguration);
if (configurationChanged) {
mCallback.setConfiguration(userId, newConfiguration);
String logMsg = "Configuration changed:"
+ "oldConfiguration=" + oldConfiguration
+ ", configuration=" + configurationChanges
+ ", newConfiguration=" + newConfiguration;
mTimeZoneChangesLog.log(logMsg);
if (DBG) {
Slog.d(LOG_TAG, logMsg);
}
mListener.onConfigurationChanged();
TimeZoneConfiguration oldConfiguration = capabilities.getConfiguration();
String logMsg = "Configuration changed:"
+ " oldConfiguration=" + oldConfiguration
+ ", newConfiguration=" + newConfiguration;
mTimeZoneChangesLog.log(logMsg);
if (DBG) {
Slog.d(LOG_TAG, logMsg);
}
return true;
}
private static boolean containsAutoTimeDetectionProperties(
@NonNull TimeZoneConfiguration configuration) {
return configuration.hasProperty(PROPERTY_AUTO_DETECTION_ENABLED)
|| configuration.hasProperty(PROPERTY_GEO_DETECTION_ENABLED);
}
@Override
public synchronized void suggestGeolocationTimeZone(
@NonNull GeolocationTimeZoneSuggestion suggestion) {
int currentUserId = mCallback.getCurrentUserId();
ConfigurationInternal currentUserConfig = mCallback.getConfigurationInternal(currentUserId);
if (DBG) {
Slog.d(LOG_TAG, "Geolocation suggestion received. newSuggestion=" + suggestion);
Slog.d(LOG_TAG, "Geolocation suggestion received."
+ " currentUserConfig=" + currentUserConfig
+ " newSuggestion=" + suggestion);
}
Objects.requireNonNull(suggestion);
mLatestGeoLocationSuggestion.set(suggestion);
// Now perform auto time zone detection. The new suggestion may be used to modify the time
// zone setting.
if (mCallback.isGeoDetectionEnabled()) {
if (currentUserConfig.getGeoDetectionEnabledBehavior()) {
// Only store a geolocation suggestion if geolocation detection is currently enabled.
mLatestGeoLocationSuggestion.set(suggestion);
// Now perform auto time zone detection. The new suggestion may be used to modify the
// time zone setting.
String reason = "New geolocation time zone suggested. suggestion=" + suggestion;
doAutoTimeZoneDetection(reason);
doAutoTimeZoneDetection(currentUserConfig, reason);
}
}
@Override
public synchronized boolean suggestManualTimeZone(
@UserIdInt int userId, @NonNull ManualTimeZoneSuggestion suggestion) {
int currentUserId = mCallback.getCurrentUserId();
if (userId != currentUserId) {
Slog.w(LOG_TAG, "Manual suggestion received but user != current user, userId=" + userId
+ " suggestion=" + suggestion);
// Only listen to changes from the current user.
return false;
}
Objects.requireNonNull(suggestion);
String timeZoneId = suggestion.getZoneId();
String cause = "Manual time suggestion received: suggestion=" + suggestion;
TimeZoneCapabilities capabilities = mCallback.getCapabilities(userId);
TimeZoneCapabilities capabilities = getConfigurationInternal(userId).createCapabilities();
if (capabilities.getSuggestManualTimeZone() != CAPABILITY_POSSESSED) {
Slog.i(LOG_TAG, "User does not have the capability needed to set the time zone manually"
+ ", capabilities=" + capabilities
@@ -345,8 +329,12 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
@Override
public synchronized void suggestTelephonyTimeZone(
@NonNull TelephonyTimeZoneSuggestion suggestion) {
int currentUserId = mCallback.getCurrentUserId();
ConfigurationInternal currentUserConfig = mCallback.getConfigurationInternal(currentUserId);
if (DBG) {
Slog.d(LOG_TAG, "Telephony suggestion received. newSuggestion=" + suggestion);
Slog.d(LOG_TAG, "Telephony suggestion received. currentUserConfig=" + currentUserConfig
+ " newSuggestion=" + suggestion);
}
Objects.requireNonNull(suggestion);
@@ -360,9 +348,9 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
// Now perform auto time zone detection. The new suggestion may be used to modify the time
// zone setting.
if (!mCallback.isGeoDetectionEnabled()) {
if (!currentUserConfig.getGeoDetectionEnabledBehavior()) {
String reason = "New telephony time zone suggested. suggestion=" + suggestion;
doAutoTimeZoneDetection(reason);
doAutoTimeZoneDetection(currentUserConfig, reason);
}
}
@@ -392,15 +380,15 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
* Performs automatic time zone detection.
*/
@GuardedBy("this")
private void doAutoTimeZoneDetection(@NonNull String detectionReason) {
if (!mCallback.isAutoDetectionEnabled()) {
// Avoid doing unnecessary work with this (race-prone) check.
private void doAutoTimeZoneDetection(
@NonNull ConfigurationInternal currentUserConfig, @NonNull String detectionReason) {
if (!currentUserConfig.getAutoDetectionEnabledBehavior()) {
// Avoid doing unnecessary work.
return;
}
// Use the right suggestions based on the current configuration. This check is potentially
// race-prone until this value is set via a call to TimeZoneDetectorStrategy.
if (mCallback.isGeoDetectionEnabled()) {
// Use the right suggestions based on the current configuration.
if (currentUserConfig.getGeoDetectionEnabledBehavior()) {
doGeolocationTimeZoneDetection(detectionReason);
} else {
doTelephonyTimeZoneDetection(detectionReason);
@@ -480,35 +468,18 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
// Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time
// zone ID.
String newZoneId = bestTelephonySuggestion.suggestion.getZoneId();
if (newZoneId == null) {
String zoneId = bestTelephonySuggestion.suggestion.getZoneId();
if (zoneId == null) {
Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:"
+ " bestTelephonySuggestion=" + bestTelephonySuggestion
+ " detectionReason=" + detectionReason);
return;
}
String zoneId = bestTelephonySuggestion.suggestion.getZoneId();
String cause = "Found good suggestion."
+ ", bestTelephonySuggestion=" + bestTelephonySuggestion
+ ", detectionReason=" + detectionReason;
setAutoDeviceTimeZoneIfRequired(zoneId, cause);
}
@GuardedBy("this")
private void setAutoDeviceTimeZoneIfRequired(@NonNull String newZoneId, @NonNull String cause) {
Objects.requireNonNull(newZoneId);
Objects.requireNonNull(cause);
if (!mCallback.isAutoDetectionEnabled()) {
if (DBG) {
Slog.d(LOG_TAG, "Auto time zone detection is not enabled."
+ ", newZoneId=" + newZoneId
+ ", cause=" + cause);
}
return;
}
setDeviceTimeZoneIfRequired(newZoneId, cause);
setDeviceTimeZoneIfRequired(zoneId, cause);
}
@GuardedBy("this")
@@ -582,13 +553,39 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
return findBestTelephonySuggestion();
}
@Override
public synchronized void handleAutoTimeZoneConfigChanged() {
private synchronized void handleConfigChanged() {
if (DBG) {
Slog.d(LOG_TAG, "handleAutoTimeZoneConfigChanged()");
Slog.d(LOG_TAG, "handleConfigChanged()");
}
doAutoTimeZoneDetection("handleAutoTimeZoneConfigChanged()");
clearGeolocationSuggestionIfNeeded();
for (ConfigurationChangeListener listener : mConfigChangeListeners) {
listener.onChange();
}
}
@GuardedBy("this")
private void clearGeolocationSuggestionIfNeeded() {
// This method is called whenever the user changes or the config for any user changes. We
// don't know what happened, so we capture the current user's config, check to see if we
// need to clear state associated with a previous user, and rerun detection.
int currentUserId = mCallback.getCurrentUserId();
ConfigurationInternal currentUserConfig = mCallback.getConfigurationInternal(currentUserId);
GeolocationTimeZoneSuggestion latestGeoLocationSuggestion =
mLatestGeoLocationSuggestion.get();
if (latestGeoLocationSuggestion != null
&& !currentUserConfig.getGeoDetectionEnabledBehavior()) {
// The current user's config has geodetection disabled, so clear the latest suggestion.
// This is done to ensure we only ever keep a geolocation suggestion if the user has
// said it is ok to do so.
mLatestGeoLocationSuggestion.set(null);
mTimeZoneChangesLog.log(
"clearGeolocationSuggestionIfNeeded: Cleared latest Geolocation suggestion.");
}
doAutoTimeZoneDetection(currentUserConfig, "clearGeolocationSuggestionIfNeeded()");
}
@Override
@@ -604,11 +601,14 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat
ipw.println("TimeZoneDetectorStrategy:");
ipw.increaseIndent(); // level 1
ipw.println("mCallback.isAutoDetectionEnabled()=" + mCallback.isAutoDetectionEnabled());
int currentUserId = mCallback.getCurrentUserId();
ipw.println("mCallback.getCurrentUserId()=" + currentUserId);
ConfigurationInternal configuration = mCallback.getConfigurationInternal(currentUserId);
ipw.println("mCallback.getConfiguration(currentUserId)=" + configuration);
ipw.println("[Capabilities=" + configuration.createCapabilities() + "]");
ipw.println("mCallback.isDeviceTimeZoneInitialized()="
+ mCallback.isDeviceTimeZoneInitialized());
ipw.println("mCallback.getDeviceTimeZone()=" + mCallback.getDeviceTimeZone());
ipw.println("mCallback.isGeoDetectionEnabled()=" + mCallback.isGeoDetectionEnabled());
ipw.println("Time zone change log:");
ipw.increaseIndent(); // level 2

View File

@@ -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());
}
}
}

View File

@@ -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 TimeZoneCapabilities getCapabilities(@UserIdInt int userId) {
return mCapabilities;
public ConfigurationInternal getConfigurationInternal(int userId) {
if (mConfigurationInternal.getUserId() != userId) {
fail("Fake only supports one user");
}
return mConfigurationInternal;
}
@Override
public boolean updateConfiguration(
@UserIdInt int userId, @NonNull TimeZoneConfiguration configuration) {
assertNotNull(mConfiguration);
assertNotNull(configuration);
public ConfigurationInternal getCurrentUserConfigurationInternal() {
return mConfigurationInternal;
}
// Simulate the strategy's behavior: the new configuration will be the old configuration
// merged with the new.
TimeZoneConfiguration oldConfiguration = mConfiguration;
TimeZoneConfiguration newConfiguration =
new TimeZoneConfiguration.Builder(mConfiguration)
.mergeProperties(configuration)
.build();
@Override
public boolean updateConfiguration(@NonNull TimeZoneConfiguration requestedChanges) {
assertNotNull(mConfigurationInternal);
assertNotNull(requestedChanges);
if (newConfiguration.equals(oldConfiguration)) {
// Simulate the real strategy's behavior: the new configuration will be updated to be the
// old configuration merged with the new if the user has the capability to up the settings.
// Then, if the configuration changed, the change listener is invoked.
TimeZoneCapabilities capabilities = mConfigurationInternal.createCapabilities();
TimeZoneConfiguration newConfiguration = capabilities.applyUpdate(requestedChanges);
if (newConfiguration == null) {
return false;
}
mConfiguration = newConfiguration;
mListener.onConfigurationChanged();
if (!newConfiguration.equals(capabilities.getConfiguration())) {
mConfigurationInternal = mConfigurationInternal.merge(newConfiguration);
// Note: Unlike the real strategy, the listeners is invoked synchronously.
mConfigurationChangeListener.onChange();
}
return true;
}
@Override
@NonNull
public TimeZoneConfiguration getConfiguration(@UserIdInt int userId) {
return mConfiguration;
public void simulateConfigurationChangeForTests() {
mConfigurationChangeListener.onChange();
}
@Override
@@ -102,11 +111,6 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy {
mLastTelephonySuggestion = timeZoneSuggestion;
}
@Override
public void handleAutoTimeZoneConfigChanged() {
mHandleAutoTimeZoneConfigChangedCalled = true;
}
@Override
public void addDumpable(Dumpable dumpable) {
mDumpables.add(dumpable);
@@ -117,19 +121,14 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy {
mDumpCalled = true;
}
void initializeConfiguration(TimeZoneConfiguration configuration) {
mConfiguration = configuration;
}
void initializeCapabilities(TimeZoneCapabilities capabilities) {
mCapabilities = capabilities;
void initializeConfiguration(ConfigurationInternal configurationInternal) {
mConfigurationInternal = configurationInternal;
}
void resetCallTracking() {
mLastGeolocationSuggestion = null;
mLastManualSuggestion = null;
mLastTelephonySuggestion = null;
mHandleAutoTimeZoneConfigChangedCalled = false;
mDumpCalled = false;
}
@@ -146,10 +145,6 @@ class FakeTimeZoneDetectorStrategy implements TimeZoneDetectorStrategy {
assertEquals(expectedSuggestion, mLastTelephonySuggestion);
}
void verifyHandleAutoTimeZoneConfigChangedCalled() {
assertTrue(mHandleAutoTimeZoneConfigChangedCalled);
}
void verifyDumpCalled() {
assertTrue(mDumpCalled);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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();
}