blob: fe977f8b39217ef1768472b55638ad74a5ad0212 [file] [log] [blame]
/*
* Copyright 2021 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.timedetector;
import static android.provider.DeviceConfig.NAMESPACE_SYSTEM_TIME;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
import android.content.Context;
import android.provider.DeviceConfig;
import android.util.ArrayMap;
import com.android.internal.annotations.GuardedBy;
import com.android.server.timezonedetector.ConfigurationChangeListener;
import com.android.server.timezonedetector.ServiceConfigAccessor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
/**
* A helper class for reading / monitoring the {@link DeviceConfig#NAMESPACE_SYSTEM_TIME} namespace
* for server-configured flags.
*/
public final class ServerFlags {
private static final Optional<Boolean> OPTIONAL_TRUE = Optional.of(true);
private static final Optional<Boolean> OPTIONAL_FALSE = Optional.of(false);
/**
* An annotation used to indicate when a {@link DeviceConfig#NAMESPACE_SYSTEM_TIME} key is
* required.
*/
@StringDef(prefix = "KEY_", value = {
KEY_LOCATION_TIME_ZONE_DETECTION_FEATURE_SUPPORTED,
KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE,
KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE,
KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_FUZZ_MILLIS,
KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_MILLIS,
KEY_LOCATION_TIME_ZONE_DETECTION_UNCERTAINTY_DELAY_MILLIS,
KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE,
KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT,
KEY_TIME_DETECTOR_LOWER_BOUND_MILLIS_OVERRIDE,
KEY_TIME_DETECTOR_ORIGIN_PRIORITIES_OVERRIDE,
})
@Retention(RetentionPolicy.SOURCE)
@interface DeviceConfigKey {}
/**
* Controls whether the location time zone manager service will be started. Only observed if
* the device build is configured to support location-based time zone detection. See
* {@link ServiceConfigAccessor#isGeoTimeZoneDetectionFeatureSupportedInConfig()} and {@link
* ServiceConfigAccessor#isGeoTimeZoneDetectionFeatureSupported()}.
*/
@DeviceConfigKey
public static final String KEY_LOCATION_TIME_ZONE_DETECTION_FEATURE_SUPPORTED =
"location_time_zone_detection_feature_supported";
/**
* The key for the server flag that can override the device config for whether the primary
* location time zone provider is enabled, disabled, or (for testing) in simulation mode.
*/
@DeviceConfigKey
public static final String KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE =
"primary_location_time_zone_provider_mode_override";
/**
* The key for the server flag that can override the device config for whether the secondary
* location time zone provider is enabled or disabled, or (for testing) in simulation mode.
*/
@DeviceConfigKey
public static final String KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE =
"secondary_location_time_zone_provider_mode_override";
/**
* The key for the minimum delay after location time zone detection has been enabled before the
* location time zone manager can report it is uncertain about the time zone.
*/
@DeviceConfigKey
public static final String KEY_LOCATION_TIME_ZONE_DETECTION_UNCERTAINTY_DELAY_MILLIS =
"location_time_zone_detection_uncertainty_delay_millis";
/**
* The key for the timeout passed to a location time zone provider that tells it how long it has
* to provide an explicit first suggestion without being declared uncertain.
*/
@DeviceConfigKey
public static final String KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_MILLIS =
"ltpz_init_timeout_millis";
/**
* The key for the extra time added to {@link
* #KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_MILLIS} by the location time zone
* manager before the location time zone provider will actually be declared uncertain.
*/
@DeviceConfigKey
public static final String KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_FUZZ_MILLIS =
"ltpz_init_timeout_fuzz_millis";
/**
* The key for the server flag that can override location time zone detection being enabled for
* a user. Only intended for use during release testing with droidfooders. The user can still
* disable the feature by turning off the master location switch, or by disabling automatic time
* zone detection.
*/
@DeviceConfigKey
public static final String KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE =
"location_time_zone_detection_setting_enabled_override";
/**
* The key for the default value used to determine whether location time zone detection is
* enabled when the user hasn't explicitly set it yet.
*/
@DeviceConfigKey
public static final String KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT =
"location_time_zone_detection_setting_enabled_default";
/**
* The key to override the time detector origin priorities configuration. A comma-separated list
* of strings that will be passed to {@link TimeDetectorStrategy#stringToOrigin(String)}.
* All values must be recognized or the override value will be ignored.
*/
@DeviceConfigKey
public static final String KEY_TIME_DETECTOR_ORIGIN_PRIORITIES_OVERRIDE =
"time_detector_origin_priorities_override";
/**
* The key to override the time detector lower bound configuration. The values is the number of
* milliseconds since the beginning of the Unix epoch.
*/
@DeviceConfigKey
public static final String KEY_TIME_DETECTOR_LOWER_BOUND_MILLIS_OVERRIDE =
"time_detector_lower_bound_millis_override";
@GuardedBy("mListeners")
private final ArrayMap<ConfigurationChangeListener, Set<String>> mListeners = new ArrayMap<>();
private static final Object SLOCK = new Object();
@GuardedBy("SLOCK")
@Nullable
private static ServerFlags sInstance;
private ServerFlags(Context context) {
DeviceConfig.addOnPropertiesChangedListener(
NAMESPACE_SYSTEM_TIME,
context.getMainExecutor(),
this::handlePropertiesChanged);
}
/** Returns the singleton instance. */
public static ServerFlags getInstance(Context context) {
synchronized (SLOCK) {
if (sInstance == null) {
sInstance = new ServerFlags(context);
}
return sInstance;
}
}
private void handlePropertiesChanged(@NonNull DeviceConfig.Properties properties) {
synchronized (mListeners) {
for (Map.Entry<ConfigurationChangeListener, Set<String>> listenerEntry
: mListeners.entrySet()) {
if (intersects(listenerEntry.getValue(), properties.getKeyset())) {
listenerEntry.getKey().onChange();
}
}
}
}
private static boolean intersects(@NonNull Set<String> one, @NonNull Set<String> two) {
for (String toFind : one) {
if (two.contains(toFind)) {
return true;
}
}
return false;
}
/**
* Adds a listener for the system_time namespace that will trigger if any of the specified keys
* change. Listener callbacks are delivered on the main looper thread.
*
* <p>Note: Only for use by long-lived objects like other singletons. There is deliberately no
* associated remove method.
*/
public void addListener(@NonNull ConfigurationChangeListener listener,
@NonNull Set<String> keys) {
Objects.requireNonNull(listener);
Objects.requireNonNull(keys);
synchronized (mListeners) {
mListeners.put(listener, keys);
}
}
/**
* Returns an optional string value from {@link DeviceConfig} from the system_time
* namespace, returns {@link Optional#empty()} if there is no explicit value set.
*/
@NonNull
public Optional<String> getOptionalString(@DeviceConfigKey String key) {
String value = DeviceConfig.getProperty(NAMESPACE_SYSTEM_TIME, key);
return Optional.ofNullable(value);
}
/**
* Returns an optional string array value from {@link DeviceConfig} from the system_time
* namespace, returns {@link Optional#empty()} if there is no explicit value set.
*/
@NonNull
public Optional<String[]> getOptionalStringArray(@DeviceConfigKey String key) {
Optional<String> string = getOptionalString(key);
if (!string.isPresent()) {
return Optional.empty();
}
return Optional.of(string.get().split(","));
}
/**
* Returns an {@link Instant} from {@link DeviceConfig} from the system_time
* namespace, returns the {@code defaultValue} if the value is missing or invalid.
*/
@NonNull
public Optional<Instant> getOptionalInstant(@DeviceConfigKey String key) {
String value = DeviceConfig.getProperty(NAMESPACE_SYSTEM_TIME, key);
if (value == null) {
return Optional.empty();
}
try {
long millis = Long.parseLong(value);
return Optional.of(Instant.ofEpochMilli(millis));
} catch (DateTimeException | NumberFormatException e) {
return Optional.empty();
}
}
/**
* Returns an optional boolean value from {@link DeviceConfig} from the system_time
* namespace, returns {@link Optional#empty()} if there is no explicit value set.
*/
@NonNull
public Optional<Boolean> getOptionalBoolean(@DeviceConfigKey String key) {
String value = DeviceConfig.getProperty(NAMESPACE_SYSTEM_TIME, key);
return parseOptionalBoolean(value);
}
@NonNull
private static Optional<Boolean> parseOptionalBoolean(@Nullable String value) {
if (value == null) {
return Optional.empty();
} else {
return Boolean.parseBoolean(value) ? OPTIONAL_TRUE : OPTIONAL_FALSE;
}
}
/**
* Returns a boolean value from {@link DeviceConfig} from the system_time
* namespace, or {@code defaultValue} if there is no explicit value set.
*/
public boolean getBoolean(@DeviceConfigKey String key, boolean defaultValue) {
return DeviceConfig.getBoolean(NAMESPACE_SYSTEM_TIME, key, defaultValue);
}
/**
* Returns a positive duration from {@link DeviceConfig} from the system_time
* namespace, or {@code defaultValue} if there is no explicit value set.
*/
@Nullable
public Duration getDurationFromMillis(
@DeviceConfigKey String key, @Nullable Duration defaultValue) {
long deviceConfigValue = DeviceConfig.getLong(NAMESPACE_SYSTEM_TIME, key, -1);
if (deviceConfigValue < 0) {
return defaultValue;
}
return Duration.ofMillis(deviceConfigValue);
}
}