blob: 29048b25ad6f0ac2b07c5a105496240c347a8dc0 [file] [log] [blame]
/*
* Copyright (C) 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.appsearch;
import android.annotation.NonNull;
import android.os.Bundle;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.OnPropertiesChangedListener;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* It contains all the keys for the flags, as well as caches some of latest flag values from
* DeviceConfig.
*
* <p>Though the latest flag values can always be retrieved by calling {@code
* DeviceConfig.getProperty}, we want to cache some of those values. For example, the sampling
* intervals for logging, they are needed for each api call and it would be a little expensive to
* call
* {@code DeviceConfig.getProperty} every time.
*
* <p>Listener is registered to DeviceConfig keep the cached value up to date.
*
* <p>This class is thread-safe.
*
* @hide
*/
public final class AppSearchConfig implements AutoCloseable {
private static volatile AppSearchConfig sConfig;
/**
* It would be used as default min time interval between samples in millis if there is no value
* set for {@link AppSearchConfig#KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS} in DeviceConfig.
*/
@VisibleForTesting
static final long DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS = 50;
/**
* It would be used as default sampling interval if there is no value
* set for {@link AppSearchConfig#KEY_SAMPLING_INTERVAL_DEFAULT} in DeviceConfig.
*/
@VisibleForTesting
static final int DEFAULT_SAMPLING_INTERVAL = 10;
@VisibleForTesting
static final int DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES = 512 * 1024; // 512KiB
@VisibleForTesting
static final int DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT = 20_000;
@VisibleForTesting
static final int DEFAULT_BYTES_OPTIMIZE_THRESHOLD = 1 * 1024 * 1024; // 1 MiB
@VisibleForTesting
static final int DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS = Integer.MAX_VALUE;
@VisibleForTesting
static final int DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD = 10_000;
/*
* Keys for ALL the flags stored in DeviceConfig.
*/
public static final String KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS =
"min_time_interval_between_samples_millis";
public static final String KEY_SAMPLING_INTERVAL_DEFAULT = "sampling_interval_default";
public static final String KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS =
"sampling_interval_for_batch_call_stats";
public static final String KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS =
"sampling_interval_for_put_document_stats";
public static final String KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS =
"sampling_interval_for_initialize_stats";
public static final String KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS =
"sampling_interval_for_search_stats";
public static final String KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS =
"sampling_interval_for_global_search_stats";
public static final String KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS =
"sampling_interval_for_optimize_stats";
public static final String KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES =
"limit_config_max_document_size_bytes";
public static final String KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT =
"limit_config_max_document_docunt";
public static final String KEY_BYTES_OPTIMIZE_THRESHOLD = "bytes_optimize_threshold";
public static final String KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS = "time_optimize_threshold";
public static final String KEY_DOC_COUNT_OPTIMIZE_THRESHOLD = "doc_count_optimize_threshold";
// Array contains all the corresponding keys for the cached values.
private static final String[] KEYS_TO_ALL_CACHED_VALUES = {
KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
KEY_SAMPLING_INTERVAL_DEFAULT,
KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS,
KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS,
KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS,
KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS,
KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS,
KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS,
KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES,
KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT,
KEY_BYTES_OPTIMIZE_THRESHOLD,
KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS,
KEY_DOC_COUNT_OPTIMIZE_THRESHOLD
};
// Lock needed for all the operations in this class.
private final Object mLock = new Object();
/**
* Bundle to hold all the cached flag values corresponding to
* {@link AppSearchConfig#KEYS_TO_ALL_CACHED_VALUES}.
*/
@GuardedBy("mLock")
private final Bundle mBundleLocked = new Bundle();
@GuardedBy("mLock")
private boolean mIsClosedLocked = false;
/** Listener to update cached flag values from DeviceConfig. */
private final OnPropertiesChangedListener mOnDeviceConfigChangedListener =
properties -> {
if (!properties.getNamespace().equals(DeviceConfig.NAMESPACE_APPSEARCH)) {
return;
}
updateCachedValues(properties);
};
private AppSearchConfig() {
}
/**
* Creates an instance of {@link AppSearchConfig}.
*
* @param executor used to fetch and cache the flag values from DeviceConfig during creation or
* config change.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@NonNull
public static AppSearchConfig create(@NonNull Executor executor) {
Objects.requireNonNull(executor);
AppSearchConfig configManager = new AppSearchConfig();
configManager.initialize(executor);
return configManager;
}
/**
* Gets an instance of {@link AppSearchConfig} to be used.
*
* <p>If no instance has been initialized yet, a new one will be created. Otherwise, the
* existing instance will be returned.
*/
@NonNull
public static AppSearchConfig getInstance(@NonNull Executor executor) {
Objects.requireNonNull(executor);
if (sConfig == null) {
synchronized (AppSearchConfig.class) {
if (sConfig == null) {
sConfig = create(executor);
}
}
}
return sConfig;
}
/**
* Initializes the {@link AppSearchConfig}
*
* <p>It fetches the custom properties from DeviceConfig if available.
*
* @param executor listener would be run on to handle P/H flag change.
*/
private void initialize(@NonNull Executor executor) {
executor.execute(() -> {
// Attach the callback to get updates on those properties.
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APPSEARCH,
executor,
mOnDeviceConfigChangedListener);
DeviceConfig.Properties properties = DeviceConfig.getProperties(
DeviceConfig.NAMESPACE_APPSEARCH, KEYS_TO_ALL_CACHED_VALUES);
updateCachedValues(properties);
});
}
// TODO(b/173532925) check this will be called. If we have a singleton instance for this
// class, probably we don't need it.
@Override
public void close() {
synchronized (mLock) {
if (mIsClosedLocked) {
return;
}
DeviceConfig.removeOnPropertiesChangedListener(mOnDeviceConfigChangedListener);
mIsClosedLocked = true;
}
}
/** Returns cached value for minTimeIntervalBetweenSamplesMillis. */
public long getCachedMinTimeIntervalBetweenSamplesMillis() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getLong(KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS,
DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS);
}
}
/**
* Returns cached value for default sampling interval for all the stats NOT listed in
* the configuration.
*
* <p>For example, sampling_interval=10 means that one out of every 10 stats was logged.
*/
public int getCachedSamplingIntervalDefault() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_DEFAULT, DEFAULT_SAMPLING_INTERVAL);
}
}
/**
* Returns cached value for sampling interval for batch calls.
*
* <p>For example, sampling_interval=10 means that one out of every 10 stats was logged.
*/
public int getCachedSamplingIntervalForBatchCallStats() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS,
getCachedSamplingIntervalDefault());
}
}
/**
* Returns cached value for sampling interval for putDocument.
*
* <p>For example, sampling_interval=10 means that one out of every 10 stats was logged.
*/
public int getCachedSamplingIntervalForPutDocumentStats() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS,
getCachedSamplingIntervalDefault());
}
}
/**
* Returns cached value for sampling interval for initialize.
*
* <p>For example, sampling_interval=10 means that one out of every 10 stats was logged.
*/
public int getCachedSamplingIntervalForInitializeStats() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS,
getCachedSamplingIntervalDefault());
}
}
/**
* Returns cached value for sampling interval for search.
*
* <p>For example, sampling_interval=10 means that one out of every 10 stats was logged.
*/
public int getCachedSamplingIntervalForSearchStats() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS,
getCachedSamplingIntervalDefault());
}
}
/**
* Returns cached value for sampling interval for globalSearch.
*
* <p>For example, sampling_interval=10 means that one out of every 10 stats was logged.
*/
public int getCachedSamplingIntervalForGlobalSearchStats() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS,
getCachedSamplingIntervalDefault());
}
}
/**
* Returns cached value for sampling interval for optimize.
*
* <p>For example, sampling_interval=10 means that one out of every 10 stats was logged.
*/
public int getCachedSamplingIntervalForOptimizeStats() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS,
getCachedSamplingIntervalDefault());
}
}
/** Returns the maximum serialized size an indexed document can be, in bytes. */
public int getCachedLimitConfigMaxDocumentSizeBytes() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES,
DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES);
}
}
/** Returns the maximum number of active docs allowed per package. */
public int getCachedLimitConfigMaxDocumentCount() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT,
DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT);
}
}
/**
* Returns the cached optimize byte size threshold.
*
* An AppSearch Optimize job will be triggered if the bytes size of garbage resource exceeds
* this threshold.
*/
int getCachedBytesOptimizeThreshold() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_BYTES_OPTIMIZE_THRESHOLD,
DEFAULT_BYTES_OPTIMIZE_THRESHOLD);
}
}
/**
* Returns the cached optimize time interval threshold.
*
* An AppSearch Optimize job will be triggered if the time since last optimize job exceeds
* this threshold.
*/
int getCachedTimeOptimizeThresholdMs() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS,
DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS);
}
}
/**
* Returns the cached optimize document count threshold threshold.
*
* An AppSearch Optimize job will be triggered if the number of document of garbage resource
* exceeds this threshold.
*/
int getCachedDocCountOptimizeThreshold() {
synchronized (mLock) {
throwIfClosedLocked();
return mBundleLocked.getInt(KEY_DOC_COUNT_OPTIMIZE_THRESHOLD,
DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD);
}
}
@GuardedBy("mLock")
private void throwIfClosedLocked() {
if (mIsClosedLocked) {
throw new IllegalStateException("Trying to use a closed AppSearchConfig instance.");
}
}
private void updateCachedValues(@NonNull DeviceConfig.Properties properties) {
for (String key : properties.getKeyset()) {
updateCachedValue(key, properties);
}
}
private void updateCachedValue(@NonNull String key,
@NonNull DeviceConfig.Properties properties) {
if (properties.getString(key, /*defaultValue=*/ null) == null) {
// Key is missing or value is just null. That is not expected if the key is
// defined in the configuration.
//
// We choose NOT to put the default value in the bundle.
// Instead, we let the getters handle what default value should be returned.
//
// Also we keep the old value in the bundle. So getters can still
// return last valid value.
return;
}
switch (key) {
case KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS:
synchronized (mLock) {
mBundleLocked.putLong(key,
properties.getLong(key,
DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS));
}
break;
case KEY_SAMPLING_INTERVAL_DEFAULT:
case KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS:
case KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS:
case KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS:
case KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS:
case KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS:
case KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS:
synchronized (mLock) {
mBundleLocked.putInt(key, properties.getInt(key, DEFAULT_SAMPLING_INTERVAL));
}
break;
case KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES:
synchronized (mLock) {
mBundleLocked.putInt(
key,
properties.getInt(key, DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES));
}
break;
case KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT:
synchronized (mLock) {
mBundleLocked.putInt(
key,
properties.getInt(key, DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT));
}
break;
case KEY_BYTES_OPTIMIZE_THRESHOLD:
synchronized (mLock) {
mBundleLocked.putInt(key, properties.getInt(key,
DEFAULT_BYTES_OPTIMIZE_THRESHOLD));
}
break;
case KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS:
synchronized (mLock) {
mBundleLocked.putInt(key, properties.getInt(key,
DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS));
}
break;
case KEY_DOC_COUNT_OPTIMIZE_THRESHOLD:
synchronized (mLock) {
mBundleLocked.putInt(key, properties.getInt(key,
DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD));
}
break;
default:
break;
}
}
}