blob: fc659c5cb627182143ea5cc08c35fcc507fc3e94 [file] [log] [blame]
/*
* 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.time.Capabilities.CAPABILITY_NOT_ALLOWED;
import static android.app.time.Capabilities.CAPABILITY_NOT_APPLICABLE;
import static android.app.time.Capabilities.CAPABILITY_NOT_SUPPORTED;
import static android.app.time.Capabilities.CAPABILITY_POSSESSED;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.time.Capabilities.CapabilityState;
import android.app.time.TimeZoneCapabilities;
import android.app.time.TimeZoneConfiguration;
import android.os.UserHandle;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Objects;
/**
* Holds configuration values that affect user-facing time zone behavior and some associated logic.
* Some configuration is global, some is user scoped, but this class deliberately doesn't make a
* distinction for simplicity.
*/
public final class ConfigurationInternal {
@IntDef(prefix = "DETECTION_MODE_",
value = { DETECTION_MODE_UNKNOWN, DETECTION_MODE_MANUAL, DETECTION_MODE_GEO,
DETECTION_MODE_TELEPHONY }
)
@Retention(RetentionPolicy.SOURCE)
@Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER })
@interface DetectionMode {};
public static final @DetectionMode int DETECTION_MODE_UNKNOWN = 0;
public static final @DetectionMode int DETECTION_MODE_MANUAL = 1;
public static final @DetectionMode int DETECTION_MODE_GEO = 2;
public static final @DetectionMode int DETECTION_MODE_TELEPHONY = 3;
private final boolean mTelephonyDetectionSupported;
private final boolean mGeoDetectionSupported;
private final boolean mTelephonyFallbackSupported;
private final boolean mGeoDetectionRunInBackgroundEnabled;
private final boolean mEnhancedMetricsCollectionEnabled;
private final boolean mAutoDetectionEnabledSetting;
private final @UserIdInt int mUserId;
private final boolean mUserConfigAllowed;
private final boolean mLocationEnabledSetting;
private final boolean mGeoDetectionEnabledSetting;
private ConfigurationInternal(Builder builder) {
mTelephonyDetectionSupported = builder.mTelephonyDetectionSupported;
mGeoDetectionSupported = builder.mGeoDetectionSupported;
mTelephonyFallbackSupported = builder.mTelephonyFallbackSupported;
mGeoDetectionRunInBackgroundEnabled = builder.mGeoDetectionRunInBackgroundEnabled;
mEnhancedMetricsCollectionEnabled = builder.mEnhancedMetricsCollectionEnabled;
mAutoDetectionEnabledSetting = builder.mAutoDetectionEnabledSetting;
mUserId = Objects.requireNonNull(builder.mUserId, "userId must be set");
mUserConfigAllowed = builder.mUserConfigAllowed;
mLocationEnabledSetting = builder.mLocationEnabledSetting;
mGeoDetectionEnabledSetting = builder.mGeoDetectionEnabledSetting;
}
/** Returns true if the device supports any form of auto time zone detection. */
public boolean isAutoDetectionSupported() {
return mTelephonyDetectionSupported || mGeoDetectionSupported;
}
/** Returns true if the device supports telephony time zone detection. */
public boolean isTelephonyDetectionSupported() {
return mTelephonyDetectionSupported;
}
/** Returns true if the device supports geolocation time zone detection. */
public boolean isGeoDetectionSupported() {
return mGeoDetectionSupported;
}
/**
* Returns true if the device supports time zone detection falling back to telephony detection
* under certain circumstances.
*/
public boolean isTelephonyFallbackSupported() {
return mTelephonyFallbackSupported;
}
/**
* Returns {@code true} if location time zone detection should run when auto time zone detection
* is enabled on supported devices, even when the user has not enabled the algorithm explicitly
* in settings. Enabled for internal testing only. See {@link #isGeoDetectionExecutionEnabled()}
* and {@link #getDetectionMode()} for details.
*/
boolean getGeoDetectionRunInBackgroundEnabledSetting() {
return mGeoDetectionRunInBackgroundEnabled;
}
/**
* Returns {@code true} if the device can collect / report extra metrics information for QA
* / testers. These metrics might involve logging more expensive or more revealing data that
* would not be collected from the set of public users.
*/
public boolean isEnhancedMetricsCollectionEnabled() {
return mEnhancedMetricsCollectionEnabled;
}
/** Returns the value of the auto time zone detection enabled setting. */
public boolean getAutoDetectionEnabledSetting() {
return mAutoDetectionEnabledSetting;
}
/**
* Returns true if auto time zone detection behavior is actually enabled, which can be distinct
* from the raw setting value.
*/
public boolean getAutoDetectionEnabledBehavior() {
return isAutoDetectionSupported() && getAutoDetectionEnabledSetting();
}
/** Returns the ID of the user this configuration is associated with. */
public @UserIdInt int getUserId() {
return mUserId;
}
/** Returns the handle of the user this configuration is associated with. */
@NonNull
public UserHandle getUserHandle() {
return UserHandle.of(mUserId);
}
/**
* Returns true if the user is allowed to modify time zone configuration, e.g. can be false due
* to device policy (enterprise).
*
* <p>See also {@link #asCapabilities(boolean)} for situations where this value is ignored.
*/
public boolean isUserConfigAllowed() {
return mUserConfigAllowed;
}
/** Returns true if user's location can be used generally. */
public boolean getLocationEnabledSetting() {
return mLocationEnabledSetting;
}
/** Returns the value of the geolocation time zone detection enabled setting. */
public boolean getGeoDetectionEnabledSetting() {
return mGeoDetectionEnabledSetting;
}
/**
* Returns the detection mode to use, i.e. which suggestions to use to determine the device's
* time zone.
*/
public @DetectionMode int getDetectionMode() {
if (!isAutoDetectionSupported()) {
// Handle the easy case first: No auto detection algorithms supported must mean manual.
return DETECTION_MODE_MANUAL;
} else if (!getAutoDetectionEnabledSetting()) {
// Auto detection algorithms are supported, but disabled by the user.
return DETECTION_MODE_MANUAL;
} else if (getGeoDetectionEnabledBehavior()) {
return DETECTION_MODE_GEO;
} else if (isTelephonyDetectionSupported()) {
return DETECTION_MODE_TELEPHONY;
} else {
// On devices with telephony detection support, telephony is used instead of geo when
// geo cannot be used. This "unknown" case can occur on devices with only the location
// detection algorithm supported when the user's master location setting prevents its
// use.
return DETECTION_MODE_UNKNOWN;
}
}
private boolean getGeoDetectionEnabledBehavior() {
// isAutoDetectionSupported() should already have been checked before calling this method.
if (isGeoDetectionSupported() && getLocationEnabledSetting()) {
if (isTelephonyDetectionSupported()) {
// This is the "normal" case for smartphones that have both telephony and geo
// detection: the user chooses which type of detection to use.
return getGeoDetectionEnabledSetting();
} else {
// When only geo detection is supported then there is no choice for the user to
// make between detection modes, so no user setting is consulted.
return true;
}
}
return false;
}
/**
* Returns true if geolocation time zone detection behavior can execute. Typically, this will
* agree with {@link #getDetectionMode()}, but under rare circumstances the geolocation detector
* may be run in the background if the user's settings allow.
*/
public boolean isGeoDetectionExecutionEnabled() {
return getDetectionMode() == DETECTION_MODE_GEO
|| getGeoDetectionRunInBackgroundEnabledBehavior();
}
private boolean getGeoDetectionRunInBackgroundEnabledBehavior() {
return isGeoDetectionSupported()
&& getLocationEnabledSetting()
&& getAutoDetectionEnabledSetting()
&& getGeoDetectionRunInBackgroundEnabledSetting();
}
@NonNull
public TimeZoneCapabilities asCapabilities(boolean bypassUserPolicyChecks) {
UserHandle userHandle = UserHandle.of(mUserId);
TimeZoneCapabilities.Builder builder = new TimeZoneCapabilities.Builder(userHandle);
boolean allowConfigDateTime = isUserConfigAllowed() || bypassUserPolicyChecks;
// Automatic time zone detection is only supported on devices if there is a telephony
// network available or geolocation time zone detection is possible.
boolean deviceHasAutoTimeZoneDetection = isAutoDetectionSupported();
final @CapabilityState int configureAutoDetectionEnabledCapability;
if (!deviceHasAutoTimeZoneDetection) {
configureAutoDetectionEnabledCapability = CAPABILITY_NOT_SUPPORTED;
} else if (!allowConfigDateTime) {
configureAutoDetectionEnabledCapability = CAPABILITY_NOT_ALLOWED;
} else {
configureAutoDetectionEnabledCapability = CAPABILITY_POSSESSED;
}
builder.setConfigureAutoDetectionEnabledCapability(configureAutoDetectionEnabledCapability);
builder.setUseLocationEnabled(mLocationEnabledSetting);
boolean deviceHasLocationTimeZoneDetection = isGeoDetectionSupported();
boolean deviceHasTelephonyDetection = isTelephonyDetectionSupported();
// Note: allowConfigDateTime does not restrict the ability to change location time zone
// detection enabled. This is intentional as it has user privacy implications and so it
// makes sense to leave this under a user's control. The only time this is not true is
// on devices that only support location-based detection and the main auto detection setting
// is used to influence whether location can be used.
final @CapabilityState int configureGeolocationDetectionEnabledCapability;
if (!deviceHasLocationTimeZoneDetection || !deviceHasTelephonyDetection) {
// If the device doesn't have geolocation detection support OR it ONLY has geolocation
// detection support (no telephony) then the user doesn't need the ability to toggle the
// location-based detection on and off (the auto detection toggle is considered
// sufficient).
configureGeolocationDetectionEnabledCapability = CAPABILITY_NOT_SUPPORTED;
} else if (!mAutoDetectionEnabledSetting || !getLocationEnabledSetting()) {
configureGeolocationDetectionEnabledCapability = CAPABILITY_NOT_APPLICABLE;
} else {
configureGeolocationDetectionEnabledCapability = CAPABILITY_POSSESSED;
}
builder.setConfigureGeoDetectionEnabledCapability(
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 @CapabilityState int suggestManualTimeZoneCapability;
if (!allowConfigDateTime) {
suggestManualTimeZoneCapability = CAPABILITY_NOT_ALLOWED;
} else if (getAutoDetectionEnabledBehavior()) {
suggestManualTimeZoneCapability = CAPABILITY_NOT_APPLICABLE;
} else {
suggestManualTimeZoneCapability = CAPABILITY_POSSESSED;
}
builder.setSetManualTimeZoneCapability(suggestManualTimeZoneCapability);
return builder.build();
}
/** Returns a {@link TimeZoneConfiguration} from the configuration values. */
public TimeZoneConfiguration asConfiguration() {
return new TimeZoneConfiguration.Builder()
.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.hasIsAutoDetectionEnabled()) {
builder.setAutoDetectionEnabledSetting(newConfiguration.isAutoDetectionEnabled());
}
if (newConfiguration.hasIsGeoDetectionEnabled()) {
builder.setGeoDetectionEnabledSetting(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
&& mTelephonyDetectionSupported == that.mTelephonyDetectionSupported
&& mGeoDetectionSupported == that.mGeoDetectionSupported
&& mTelephonyFallbackSupported == that.mTelephonyFallbackSupported
&& mGeoDetectionRunInBackgroundEnabled == that.mGeoDetectionRunInBackgroundEnabled
&& mEnhancedMetricsCollectionEnabled == that.mEnhancedMetricsCollectionEnabled
&& mAutoDetectionEnabledSetting == that.mAutoDetectionEnabledSetting
&& mLocationEnabledSetting == that.mLocationEnabledSetting
&& mGeoDetectionEnabledSetting == that.mGeoDetectionEnabledSetting;
}
@Override
public int hashCode() {
return Objects.hash(mUserId, mUserConfigAllowed, mTelephonyDetectionSupported,
mGeoDetectionSupported, mTelephonyFallbackSupported,
mGeoDetectionRunInBackgroundEnabled, mEnhancedMetricsCollectionEnabled,
mAutoDetectionEnabledSetting, mLocationEnabledSetting, mGeoDetectionEnabledSetting);
}
@Override
public String toString() {
return "ConfigurationInternal{"
+ "mUserId=" + mUserId
+ ", mUserConfigAllowed=" + mUserConfigAllowed
+ ", mTelephonyDetectionSupported=" + mTelephonyDetectionSupported
+ ", mGeoDetectionSupported=" + mGeoDetectionSupported
+ ", mTelephonyFallbackSupported=" + mTelephonyFallbackSupported
+ ", mGeoDetectionRunInBackgroundEnabled=" + mGeoDetectionRunInBackgroundEnabled
+ ", mEnhancedMetricsCollectionEnabled=" + mEnhancedMetricsCollectionEnabled
+ ", mAutoDetectionEnabledSetting=" + mAutoDetectionEnabledSetting
+ ", mLocationEnabledSetting=" + mLocationEnabledSetting
+ ", mGeoDetectionEnabledSetting=" + mGeoDetectionEnabledSetting
+ '}';
}
/**
* A Builder for {@link ConfigurationInternal}.
*/
public static class Builder {
private @UserIdInt Integer mUserId;
private boolean mUserConfigAllowed;
private boolean mTelephonyDetectionSupported;
private boolean mGeoDetectionSupported;
private boolean mTelephonyFallbackSupported;
private boolean mGeoDetectionRunInBackgroundEnabled;
private boolean mEnhancedMetricsCollectionEnabled;
private boolean mAutoDetectionEnabledSetting;
private boolean mLocationEnabledSetting;
private boolean mGeoDetectionEnabledSetting;
/**
* Creates a new Builder.
*/
public Builder() {}
/**
* Creates a new Builder by copying values from an existing instance.
*/
public Builder(ConfigurationInternal toCopy) {
this.mUserId = toCopy.mUserId;
this.mUserConfigAllowed = toCopy.mUserConfigAllowed;
this.mTelephonyDetectionSupported = toCopy.mTelephonyDetectionSupported;
this.mTelephonyFallbackSupported = toCopy.mTelephonyFallbackSupported;
this.mGeoDetectionSupported = toCopy.mGeoDetectionSupported;
this.mGeoDetectionRunInBackgroundEnabled = toCopy.mGeoDetectionRunInBackgroundEnabled;
this.mEnhancedMetricsCollectionEnabled = toCopy.mEnhancedMetricsCollectionEnabled;
this.mAutoDetectionEnabledSetting = toCopy.mAutoDetectionEnabledSetting;
this.mLocationEnabledSetting = toCopy.mLocationEnabledSetting;
this.mGeoDetectionEnabledSetting = toCopy.mGeoDetectionEnabledSetting;
}
/**
* Sets the user ID the configuration is for.
*/
public Builder setUserId(@UserIdInt int userId) {
mUserId = userId;
return this;
}
/**
* 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 telephony time zone detection is supported on this device.
*/
public Builder setTelephonyDetectionFeatureSupported(boolean supported) {
mTelephonyDetectionSupported = supported;
return this;
}
/**
* Sets whether geolocation time zone detection is supported on this device.
*/
public Builder setGeoDetectionFeatureSupported(boolean supported) {
mGeoDetectionSupported = supported;
return this;
}
/**
* Sets whether time zone detection supports falling back to telephony detection under
* certain circumstances.
*/
public Builder setTelephonyFallbackSupported(boolean supported) {
mTelephonyFallbackSupported = supported;
return this;
}
/**
* Sets whether location time zone detection should run when auto time zone detection is
* enabled on supported devices, even when the user has not enabled the algorithm explicitly
* in settings. Enabled for internal testing only.
*/
public Builder setGeoDetectionRunInBackgroundEnabled(boolean enabled) {
mGeoDetectionRunInBackgroundEnabled = enabled;
return this;
}
/**
* Sets the value for enhanced metrics collection.
*/
public Builder setEnhancedMetricsCollectionEnabled(boolean enabled) {
mEnhancedMetricsCollectionEnabled = enabled;
return this;
}
/**
* Sets the value of the automatic time zone detection enabled setting for this device.
*/
public Builder setAutoDetectionEnabledSetting(boolean enabled) {
mAutoDetectionEnabledSetting = enabled;
return this;
}
/**
* Sets the value of the location mode setting for this user.
*/
public Builder setLocationEnabledSetting(boolean enabled) {
mLocationEnabledSetting = enabled;
return this;
}
/**
* Sets the value of the geolocation time zone detection setting for this user.
*/
public Builder setGeoDetectionEnabledSetting(boolean enabled) {
mGeoDetectionEnabledSetting = enabled;
return this;
}
/** Returns a new {@link ConfigurationInternal}. */
@NonNull
public ConfigurationInternal build() {
return new ConfigurationInternal(this);
}
}
}