1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.appsearch; 18 19 import android.annotation.NonNull; 20 import android.os.Bundle; 21 import android.provider.DeviceConfig; 22 import android.provider.DeviceConfig.OnPropertiesChangedListener; 23 24 import com.android.internal.annotations.GuardedBy; 25 import com.android.internal.annotations.VisibleForTesting; 26 27 import java.util.Objects; 28 import java.util.concurrent.Executor; 29 30 /** 31 * It contains all the keys for the flags, as well as caches some of latest flag values from 32 * DeviceConfig. 33 * 34 * <p>Though the latest flag values can always be retrieved by calling {@code 35 * DeviceConfig.getProperty}, we want to cache some of those values. For example, the sampling 36 * intervals for logging, they are needed for each api call and it would be a little expensive to 37 * call 38 * {@code DeviceConfig.getProperty} every time. 39 * 40 * <p>Listener is registered to DeviceConfig keep the cached value up to date. 41 * 42 * <p>This class is thread-safe. 43 * 44 * @hide 45 */ 46 public final class AppSearchConfig implements AutoCloseable { 47 private static volatile AppSearchConfig sConfig; 48 49 /** 50 * It would be used as default min time interval between samples in millis if there is no value 51 * set for {@link AppSearchConfig#KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS} in DeviceConfig. 52 */ 53 @VisibleForTesting 54 static final long DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS = 50; 55 56 /** 57 * It would be used as default sampling interval if there is no value 58 * set for {@link AppSearchConfig#KEY_SAMPLING_INTERVAL_DEFAULT} in DeviceConfig. 59 */ 60 @VisibleForTesting 61 static final int DEFAULT_SAMPLING_INTERVAL = 10; 62 63 @VisibleForTesting 64 static final int DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES = 512 * 1024; // 512KiB 65 @VisibleForTesting 66 static final int DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT = 20_000; 67 @VisibleForTesting 68 static final int DEFAULT_BYTES_OPTIMIZE_THRESHOLD = 1 * 1024 * 1024; // 1 MiB 69 @VisibleForTesting 70 static final int DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS = Integer.MAX_VALUE; 71 @VisibleForTesting 72 static final int DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD = 10_000; 73 74 /* 75 * Keys for ALL the flags stored in DeviceConfig. 76 */ 77 public static final String KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS = 78 "min_time_interval_between_samples_millis"; 79 public static final String KEY_SAMPLING_INTERVAL_DEFAULT = "sampling_interval_default"; 80 public static final String KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS = 81 "sampling_interval_for_batch_call_stats"; 82 public static final String KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS = 83 "sampling_interval_for_put_document_stats"; 84 public static final String KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS = 85 "sampling_interval_for_initialize_stats"; 86 public static final String KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS = 87 "sampling_interval_for_search_stats"; 88 public static final String KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS = 89 "sampling_interval_for_global_search_stats"; 90 public static final String KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS = 91 "sampling_interval_for_optimize_stats"; 92 public static final String KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES = 93 "limit_config_max_document_size_bytes"; 94 public static final String KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT = 95 "limit_config_max_document_docunt"; 96 public static final String KEY_BYTES_OPTIMIZE_THRESHOLD = "bytes_optimize_threshold"; 97 public static final String KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS = "time_optimize_threshold"; 98 public static final String KEY_DOC_COUNT_OPTIMIZE_THRESHOLD = "doc_count_optimize_threshold"; 99 100 // Array contains all the corresponding keys for the cached values. 101 private static final String[] KEYS_TO_ALL_CACHED_VALUES = { 102 KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS, 103 KEY_SAMPLING_INTERVAL_DEFAULT, 104 KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS, 105 KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS, 106 KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS, 107 KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS, 108 KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS, 109 KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS, 110 KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES, 111 KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT, 112 KEY_BYTES_OPTIMIZE_THRESHOLD, 113 KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS, 114 KEY_DOC_COUNT_OPTIMIZE_THRESHOLD 115 }; 116 117 // Lock needed for all the operations in this class. 118 private final Object mLock = new Object(); 119 120 /** 121 * Bundle to hold all the cached flag values corresponding to 122 * {@link AppSearchConfig#KEYS_TO_ALL_CACHED_VALUES}. 123 */ 124 @GuardedBy("mLock") 125 private final Bundle mBundleLocked = new Bundle(); 126 127 128 @GuardedBy("mLock") 129 private boolean mIsClosedLocked = false; 130 131 /** Listener to update cached flag values from DeviceConfig. */ 132 private final OnPropertiesChangedListener mOnDeviceConfigChangedListener = 133 properties -> { 134 if (!properties.getNamespace().equals(DeviceConfig.NAMESPACE_APPSEARCH)) { 135 return; 136 } 137 138 updateCachedValues(properties); 139 }; 140 AppSearchConfig()141 private AppSearchConfig() { 142 } 143 144 /** 145 * Creates an instance of {@link AppSearchConfig}. 146 * 147 * @param executor used to fetch and cache the flag values from DeviceConfig during creation or 148 * config change. 149 */ 150 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) 151 @NonNull create(@onNull Executor executor)152 public static AppSearchConfig create(@NonNull Executor executor) { 153 Objects.requireNonNull(executor); 154 AppSearchConfig configManager = new AppSearchConfig(); 155 configManager.initialize(executor); 156 return configManager; 157 } 158 159 /** 160 * Gets an instance of {@link AppSearchConfig} to be used. 161 * 162 * <p>If no instance has been initialized yet, a new one will be created. Otherwise, the 163 * existing instance will be returned. 164 */ 165 @NonNull getInstance(@onNull Executor executor)166 public static AppSearchConfig getInstance(@NonNull Executor executor) { 167 Objects.requireNonNull(executor); 168 if (sConfig == null) { 169 synchronized (AppSearchConfig.class) { 170 if (sConfig == null) { 171 sConfig = create(executor); 172 } 173 } 174 } 175 return sConfig; 176 } 177 178 /** 179 * Initializes the {@link AppSearchConfig} 180 * 181 * <p>It fetches the custom properties from DeviceConfig if available. 182 * 183 * @param executor listener would be run on to handle P/H flag change. 184 */ initialize(@onNull Executor executor)185 private void initialize(@NonNull Executor executor) { 186 executor.execute(() -> { 187 // Attach the callback to get updates on those properties. 188 DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APPSEARCH, 189 executor, 190 mOnDeviceConfigChangedListener); 191 192 DeviceConfig.Properties properties = DeviceConfig.getProperties( 193 DeviceConfig.NAMESPACE_APPSEARCH, KEYS_TO_ALL_CACHED_VALUES); 194 updateCachedValues(properties); 195 }); 196 } 197 198 // TODO(b/173532925) check this will be called. If we have a singleton instance for this 199 // class, probably we don't need it. 200 @Override close()201 public void close() { 202 synchronized (mLock) { 203 if (mIsClosedLocked) { 204 return; 205 } 206 207 DeviceConfig.removeOnPropertiesChangedListener(mOnDeviceConfigChangedListener); 208 mIsClosedLocked = true; 209 } 210 } 211 212 /** Returns cached value for minTimeIntervalBetweenSamplesMillis. */ getCachedMinTimeIntervalBetweenSamplesMillis()213 public long getCachedMinTimeIntervalBetweenSamplesMillis() { 214 synchronized (mLock) { 215 throwIfClosedLocked(); 216 return mBundleLocked.getLong(KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS, 217 DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS); 218 } 219 } 220 221 /** 222 * Returns cached value for default sampling interval for all the stats NOT listed in 223 * the configuration. 224 * 225 * <p>For example, sampling_interval=10 means that one out of every 10 stats was logged. 226 */ getCachedSamplingIntervalDefault()227 public int getCachedSamplingIntervalDefault() { 228 synchronized (mLock) { 229 throwIfClosedLocked(); 230 return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_DEFAULT, DEFAULT_SAMPLING_INTERVAL); 231 } 232 } 233 234 /** 235 * Returns cached value for sampling interval for batch calls. 236 * 237 * <p>For example, sampling_interval=10 means that one out of every 10 stats was logged. 238 */ getCachedSamplingIntervalForBatchCallStats()239 public int getCachedSamplingIntervalForBatchCallStats() { 240 synchronized (mLock) { 241 throwIfClosedLocked(); 242 return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS, 243 getCachedSamplingIntervalDefault()); 244 } 245 } 246 247 /** 248 * Returns cached value for sampling interval for putDocument. 249 * 250 * <p>For example, sampling_interval=10 means that one out of every 10 stats was logged. 251 */ getCachedSamplingIntervalForPutDocumentStats()252 public int getCachedSamplingIntervalForPutDocumentStats() { 253 synchronized (mLock) { 254 throwIfClosedLocked(); 255 return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS, 256 getCachedSamplingIntervalDefault()); 257 } 258 } 259 260 /** 261 * Returns cached value for sampling interval for initialize. 262 * 263 * <p>For example, sampling_interval=10 means that one out of every 10 stats was logged. 264 */ getCachedSamplingIntervalForInitializeStats()265 public int getCachedSamplingIntervalForInitializeStats() { 266 synchronized (mLock) { 267 throwIfClosedLocked(); 268 return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS, 269 getCachedSamplingIntervalDefault()); 270 } 271 } 272 273 /** 274 * Returns cached value for sampling interval for search. 275 * 276 * <p>For example, sampling_interval=10 means that one out of every 10 stats was logged. 277 */ getCachedSamplingIntervalForSearchStats()278 public int getCachedSamplingIntervalForSearchStats() { 279 synchronized (mLock) { 280 throwIfClosedLocked(); 281 return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS, 282 getCachedSamplingIntervalDefault()); 283 } 284 } 285 286 /** 287 * Returns cached value for sampling interval for globalSearch. 288 * 289 * <p>For example, sampling_interval=10 means that one out of every 10 stats was logged. 290 */ getCachedSamplingIntervalForGlobalSearchStats()291 public int getCachedSamplingIntervalForGlobalSearchStats() { 292 synchronized (mLock) { 293 throwIfClosedLocked(); 294 return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS, 295 getCachedSamplingIntervalDefault()); 296 } 297 } 298 299 /** 300 * Returns cached value for sampling interval for optimize. 301 * 302 * <p>For example, sampling_interval=10 means that one out of every 10 stats was logged. 303 */ getCachedSamplingIntervalForOptimizeStats()304 public int getCachedSamplingIntervalForOptimizeStats() { 305 synchronized (mLock) { 306 throwIfClosedLocked(); 307 return mBundleLocked.getInt(KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS, 308 getCachedSamplingIntervalDefault()); 309 } 310 } 311 312 /** Returns the maximum serialized size an indexed document can be, in bytes. */ getCachedLimitConfigMaxDocumentSizeBytes()313 public int getCachedLimitConfigMaxDocumentSizeBytes() { 314 synchronized (mLock) { 315 throwIfClosedLocked(); 316 return mBundleLocked.getInt(KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES, 317 DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES); 318 } 319 } 320 321 /** Returns the maximum number of active docs allowed per package. */ getCachedLimitConfigMaxDocumentCount()322 public int getCachedLimitConfigMaxDocumentCount() { 323 synchronized (mLock) { 324 throwIfClosedLocked(); 325 return mBundleLocked.getInt(KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT, 326 DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT); 327 } 328 } 329 330 /** 331 * Returns the cached optimize byte size threshold. 332 * 333 * An AppSearch Optimize job will be triggered if the bytes size of garbage resource exceeds 334 * this threshold. 335 */ getCachedBytesOptimizeThreshold()336 int getCachedBytesOptimizeThreshold() { 337 synchronized (mLock) { 338 throwIfClosedLocked(); 339 return mBundleLocked.getInt(KEY_BYTES_OPTIMIZE_THRESHOLD, 340 DEFAULT_BYTES_OPTIMIZE_THRESHOLD); 341 } 342 } 343 344 /** 345 * Returns the cached optimize time interval threshold. 346 * 347 * An AppSearch Optimize job will be triggered if the time since last optimize job exceeds 348 * this threshold. 349 */ getCachedTimeOptimizeThresholdMs()350 int getCachedTimeOptimizeThresholdMs() { 351 synchronized (mLock) { 352 throwIfClosedLocked(); 353 return mBundleLocked.getInt(KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS, 354 DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS); 355 } 356 } 357 358 /** 359 * Returns the cached optimize document count threshold threshold. 360 * 361 * An AppSearch Optimize job will be triggered if the number of document of garbage resource 362 * exceeds this threshold. 363 */ getCachedDocCountOptimizeThreshold()364 int getCachedDocCountOptimizeThreshold() { 365 synchronized (mLock) { 366 throwIfClosedLocked(); 367 return mBundleLocked.getInt(KEY_DOC_COUNT_OPTIMIZE_THRESHOLD, 368 DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD); 369 } 370 } 371 372 @GuardedBy("mLock") throwIfClosedLocked()373 private void throwIfClosedLocked() { 374 if (mIsClosedLocked) { 375 throw new IllegalStateException("Trying to use a closed AppSearchConfig instance."); 376 } 377 } 378 updateCachedValues(@onNull DeviceConfig.Properties properties)379 private void updateCachedValues(@NonNull DeviceConfig.Properties properties) { 380 for (String key : properties.getKeyset()) { 381 updateCachedValue(key, properties); 382 } 383 } 384 updateCachedValue(@onNull String key, @NonNull DeviceConfig.Properties properties)385 private void updateCachedValue(@NonNull String key, 386 @NonNull DeviceConfig.Properties properties) { 387 if (properties.getString(key, /*defaultValue=*/ null) == null) { 388 // Key is missing or value is just null. That is not expected if the key is 389 // defined in the configuration. 390 // 391 // We choose NOT to put the default value in the bundle. 392 // Instead, we let the getters handle what default value should be returned. 393 // 394 // Also we keep the old value in the bundle. So getters can still 395 // return last valid value. 396 return; 397 } 398 399 switch (key) { 400 case KEY_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS: 401 synchronized (mLock) { 402 mBundleLocked.putLong(key, 403 properties.getLong(key, 404 DEFAULT_MIN_TIME_INTERVAL_BETWEEN_SAMPLES_MILLIS)); 405 } 406 break; 407 case KEY_SAMPLING_INTERVAL_DEFAULT: 408 case KEY_SAMPLING_INTERVAL_FOR_BATCH_CALL_STATS: 409 case KEY_SAMPLING_INTERVAL_FOR_PUT_DOCUMENT_STATS: 410 case KEY_SAMPLING_INTERVAL_FOR_INITIALIZE_STATS: 411 case KEY_SAMPLING_INTERVAL_FOR_SEARCH_STATS: 412 case KEY_SAMPLING_INTERVAL_FOR_GLOBAL_SEARCH_STATS: 413 case KEY_SAMPLING_INTERVAL_FOR_OPTIMIZE_STATS: 414 synchronized (mLock) { 415 mBundleLocked.putInt(key, properties.getInt(key, DEFAULT_SAMPLING_INTERVAL)); 416 } 417 break; 418 case KEY_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES: 419 synchronized (mLock) { 420 mBundleLocked.putInt( 421 key, 422 properties.getInt(key, DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_SIZE_BYTES)); 423 } 424 break; 425 case KEY_LIMIT_CONFIG_MAX_DOCUMENT_COUNT: 426 synchronized (mLock) { 427 mBundleLocked.putInt( 428 key, 429 properties.getInt(key, DEFAULT_LIMIT_CONFIG_MAX_DOCUMENT_COUNT)); 430 } 431 break; 432 case KEY_BYTES_OPTIMIZE_THRESHOLD: 433 synchronized (mLock) { 434 mBundleLocked.putInt(key, properties.getInt(key, 435 DEFAULT_BYTES_OPTIMIZE_THRESHOLD)); 436 } 437 break; 438 case KEY_TIME_OPTIMIZE_THRESHOLD_MILLIS: 439 synchronized (mLock) { 440 mBundleLocked.putInt(key, properties.getInt(key, 441 DEFAULT_TIME_OPTIMIZE_THRESHOLD_MILLIS)); 442 } 443 break; 444 case KEY_DOC_COUNT_OPTIMIZE_THRESHOLD: 445 synchronized (mLock) { 446 mBundleLocked.putInt(key, properties.getInt(key, 447 DEFAULT_DOC_COUNT_OPTIMIZE_THRESHOLD)); 448 } 449 break; 450 default: 451 break; 452 } 453 } 454 } 455