| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.chrome.browser.flags; |
| |
| import androidx.annotation.AnyThread; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| |
| import com.google.common.collect.ImmutableMap; |
| |
| import org.chromium.base.FieldTrialList; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.annotations.NativeMethods; |
| import org.chromium.base.library_loader.LibraryLoader; |
| import org.chromium.chrome.browser.preferences.ChromePreferenceKeys; |
| import org.chromium.chrome.browser.preferences.SharedPreferencesManager; |
| |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * A class to cache the state of flags from {@link ChromeFeatureList}. |
| * |
| * It caches certain feature flags that must take effect on startup before native is initialized. |
| * ChromeFeatureList can only be queried through native code. The caching is done in |
| * {@link android.content.SharedPreferences}, which is available in Java immediately. |
| * |
| * To cache a flag from ChromeFeatureList: |
| * - Set its default value by adding an entry to {@link #sDefaults}. |
| * - Add it to the list passed to {@link #cacheNativeFlags(List)}. |
| * - Call {@link #isEnabled(String)} to query whether the cached flag is enabled. |
| * Consider this the source of truth for whether the flag is turned on in the current session. |
| * - When querying whether a cached feature is enabled from native, a @CalledByNative method can be |
| * exposed in this file to allow feature_utilities.cc to retrieve the cached value. |
| * |
| * For cached flags that are queried before native is initialized, when a new experiment |
| * configuration is received the metrics reporting system will record metrics as if the |
| * experiment is enabled despite the experimental behavior not yet taking effect. This will be |
| * remedied on the next process restart, when the static Boolean is reset to the newly cached |
| * value in shared preferences. |
| */ |
| public class CachedFeatureFlags { |
| /** |
| * Stores the default values for each feature flag queried, used as a fallback in case native |
| * isn't loaded, and no value has been previously cached. |
| */ |
| private static Map<String, Boolean> sDefaults = |
| ImmutableMap.<String, Boolean>builder() |
| .put(ChromeFeatureList.ANONYMOUS_UPDATE_CHECKS, true) |
| .put(ChromeFeatureList.BOOKMARK_BOTTOM_SHEET, false) |
| .put(ChromeFeatureList.CONDITIONAL_TAB_STRIP_ANDROID, false) |
| .put(ChromeFeatureList.LENS_CAMERA_ASSISTED_SEARCH, false) |
| .put(ChromeFeatureList.SERVICE_MANAGER_FOR_DOWNLOAD, true) |
| .put(ChromeFeatureList.SERVICE_MANAGER_FOR_BACKGROUND_PREFETCH, true) |
| .put(ChromeFeatureList.COMMAND_LINE_ON_NON_ROOTED, false) |
| .put(ChromeFeatureList.DOWNLOADS_AUTO_RESUMPTION_NATIVE, true) |
| .put(ChromeFeatureList.EARLY_LIBRARY_LOAD, false) |
| .put(ChromeFeatureList.ELASTIC_OVERSCROLL, true) |
| .put(ChromeFeatureList.ELIDE_PRIORITIZATION_OF_PRE_NATIVE_BOOTSTRAP_TASKS, |
| false) |
| .put(ChromeFeatureList.ELIDE_TAB_PRELOAD_AT_STARTUP, false) |
| .put(ChromeFeatureList |
| .GIVE_JAVA_UI_THREAD_DEFAULT_TASK_TRAITS_USER_BLOCKING_PRIORITY, |
| false) |
| .put(ChromeFeatureList.IMMERSIVE_UI_MODE, false) |
| .put(ChromeFeatureList.SWAP_PIXEL_FORMAT_TO_FIX_CONVERT_FROM_TRANSLUCENT, true) |
| .put(ChromeFeatureList.START_SURFACE_ANDROID, false) |
| .put(ChromeFeatureList.PAINT_PREVIEW_DEMO, false) |
| .put(ChromeFeatureList.PAINT_PREVIEW_SHOW_ON_STARTUP, false) |
| .put(ChromeFeatureList.PREFETCH_NOTIFICATION_SCHEDULING_INTEGRATION, false) |
| .put(ChromeFeatureList.STORE_HOURS, false) |
| .put(ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID, true) |
| .put(ChromeFeatureList.TAB_GROUPS_ANDROID, true) |
| .put(ChromeFeatureList.TAB_GROUPS_CONTINUATION_ANDROID, false) |
| .put(ChromeFeatureList.TOOLBAR_USE_HARDWARE_BITMAP_DRAW, false) |
| .put(ChromeFeatureList.CLOSE_TAB_SUGGESTIONS, false) |
| .put(ChromeFeatureList.CRITICAL_PERSISTED_TAB_DATA, false) |
| .put(ChromeFeatureList.DYNAMIC_COLOR_ANDROID, true) |
| .put(ChromeFeatureList.INSTANT_START, false) |
| .put(ChromeFeatureList.TAB_TO_GTS_ANIMATION, true) |
| .put(ChromeFeatureList.TEST_DEFAULT_DISABLED, false) |
| .put(ChromeFeatureList.TEST_DEFAULT_ENABLED, true) |
| .put(ChromeFeatureList.INTEREST_FEED_V2, true) |
| .put(ChromeFeatureList.THEME_REFACTOR_ANDROID, true) |
| .put(ChromeFeatureList.USE_CHIME_ANDROID_SDK, false) |
| .put(ChromeFeatureList.CCT_INCOGNITO_AVAILABLE_TO_THIRD_PARTY, false) |
| .put(ChromeFeatureList.READ_LATER, false) |
| .put(ChromeFeatureList.CCT_REMOVE_REMOTE_VIEW_IDS, true) |
| .put(ChromeFeatureList.OFFLINE_MEASUREMENTS_BACKGROUND_TASK, false) |
| .put(ChromeFeatureList.CCT_INCOGNITO, true) |
| .put(ChromeFeatureList.EXPERIMENTS_FOR_AGSA, true) |
| .put(ChromeFeatureList.APP_MENU_MOBILE_SITE_OPTION, false) |
| .put(ChromeFeatureList.OPTIMIZATION_GUIDE_PUSH_NOTIFICATIONS, false) |
| .put(ChromeFeatureList.APP_TO_WEB_ATTRIBUTION, false) |
| .put(ChromeFeatureList.NEW_WINDOW_APP_MENU, true) |
| .put(ChromeFeatureList.CCT_RESIZABLE_90_MAXIMUM_HEIGHT, false) |
| .put(ChromeFeatureList.CCT_RESIZABLE_ALLOW_RESIZE_BY_USER_GESTURE, false) |
| .put(ChromeFeatureList.CCT_RESIZABLE_FOR_FIRST_PARTIES, true) |
| .put(ChromeFeatureList.CCT_RESIZABLE_FOR_THIRD_PARTIES, false) |
| .put(ChromeFeatureList.INSTANCE_SWITCHER, true) |
| .put(ChromeFeatureList.WEB_APK_TRAMPOLINE_ON_INITIAL_INTENT, true) |
| .put(ChromeFeatureList.FEED_LOADING_PLACEHOLDER, false) |
| .put(ChromeFeatureList.GRID_TAB_SWITCHER_FOR_TABLETS, false) |
| .put(ChromeFeatureList.TAB_GROUPS_FOR_TABLETS, false) |
| .build(); |
| |
| /** |
| * Non-dynamic preference keys used historically for specific features. |
| * |
| * Do not add new values to this list. To add a new cached feature flag, just follow the |
| * instructions in the class javadoc. |
| */ |
| private static final Map<String, String> sNonDynamicPrefKeys = |
| ImmutableMap.<String, String>builder() |
| .put(ChromeFeatureList.SERVICE_MANAGER_FOR_DOWNLOAD, |
| ChromePreferenceKeys |
| .FLAGS_CACHED_SERVICE_MANAGER_FOR_DOWNLOAD_RESUMPTION) |
| .put(ChromeFeatureList.SERVICE_MANAGER_FOR_BACKGROUND_PREFETCH, |
| ChromePreferenceKeys |
| .FLAGS_CACHED_SERVICE_MANAGER_FOR_BACKGROUND_PREFETCH) |
| .put(ChromeFeatureList.COMMAND_LINE_ON_NON_ROOTED, |
| ChromePreferenceKeys.FLAGS_CACHED_COMMAND_LINE_ON_NON_ROOTED_ENABLED) |
| .put(ChromeFeatureList.DOWNLOADS_AUTO_RESUMPTION_NATIVE, |
| ChromePreferenceKeys.FLAGS_CACHED_DOWNLOAD_AUTO_RESUMPTION_IN_NATIVE) |
| .put(ChromeFeatureList.IMMERSIVE_UI_MODE, |
| ChromePreferenceKeys.FLAGS_CACHED_IMMERSIVE_UI_MODE_ENABLED) |
| .put(ChromeFeatureList.SWAP_PIXEL_FORMAT_TO_FIX_CONVERT_FROM_TRANSLUCENT, |
| ChromePreferenceKeys |
| .FLAGS_CACHED_SWAP_PIXEL_FORMAT_TO_FIX_CONVERT_FROM_TRANSLUCENT) |
| .put(ChromeFeatureList.START_SURFACE_ANDROID, |
| ChromePreferenceKeys.FLAGS_CACHED_START_SURFACE_ENABLED) |
| .put(ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID, |
| ChromePreferenceKeys.FLAGS_CACHED_GRID_TAB_SWITCHER_ENABLED) |
| .put(ChromeFeatureList.TAB_GROUPS_ANDROID, |
| ChromePreferenceKeys.FLAGS_CACHED_TAB_GROUPS_ANDROID_ENABLED) |
| .build(); |
| |
| private static ValuesReturned sValuesReturned = new ValuesReturned(); |
| private static ValuesOverridden sValuesOverridden = new ValuesOverridden(); |
| private static CachedFlagsSafeMode sSafeMode = new CachedFlagsSafeMode(); |
| |
| private static String sReachedCodeProfilerTrialGroup; |
| |
| /** |
| * Checks if a cached feature flag is enabled. |
| * |
| * Requires that the feature be registered in {@link #sDefaults}. |
| * |
| * Rules from highest to lowest priority: |
| * 1. If the flag has been forced by {@link #setForTesting}, the forced value is returned. |
| * 2. If a value was previously returned in the same run, the same value is returned for |
| * consistency. |
| * 3. If native is loaded, the value from {@link ChromeFeatureList} is returned. |
| * 4. If in a previous run, the value from {@link ChromeFeatureList} was cached to SharedPrefs, |
| * it is returned. |
| * 5. The default value defined in {@link #sDefaults} is returned. |
| * |
| * @param featureName the feature name from ChromeFeatureList. |
| * @return whether the cached feature should be considered enabled. |
| */ |
| @CalledByNative |
| @AnyThread |
| public static boolean isEnabled(String featureName) { |
| // All cached feature flags should have a default value. |
| if (!sDefaults.containsKey(featureName)) { |
| throw new IllegalArgumentException( |
| "Feature " + featureName + " has no default in CachedFeatureFlags."); |
| } |
| |
| sSafeMode.onFlagChecked(); |
| |
| String preferenceName = getPrefForFeatureFlag(featureName); |
| |
| Boolean flag; |
| synchronized (sValuesReturned.boolValues) { |
| flag = sValuesReturned.boolValues.get(preferenceName); |
| if (flag != null) { |
| return flag; |
| } |
| |
| SharedPreferencesManager prefs = SharedPreferencesManager.getInstance(); |
| if (prefs.contains(preferenceName)) { |
| flag = prefs.readBoolean(preferenceName, false); |
| } else { |
| flag = sDefaults.get(featureName); |
| } |
| sValuesReturned.boolValues.put(preferenceName, flag); |
| } |
| return flag; |
| } |
| |
| /** |
| * Caches the value of a feature from {@link ChromeFeatureList} to SharedPrefs. |
| * |
| * @param featureName the feature name from ChromeFeatureList. |
| */ |
| private static void cacheFeature(String featureName) { |
| String preferenceName = getPrefForFeatureFlag(featureName); |
| boolean isEnabledInNative = ChromeFeatureList.isEnabled(featureName); |
| SharedPreferencesManager.getInstance().writeBoolean(preferenceName, isEnabledInNative); |
| } |
| |
| /** |
| * Forces a feature to be enabled or disabled for testing. |
| * |
| * @param featureName the feature name from ChromeFeatureList. |
| * @param value the value that {@link #isEnabled(String)} will be forced to return. If null, |
| * remove any values previously forced. |
| */ |
| public static void setForTesting(String featureName, @Nullable Boolean value) { |
| String preferenceName = getPrefForFeatureFlag(featureName); |
| synchronized (sValuesReturned.boolValues) { |
| sValuesReturned.boolValues.put(preferenceName, value); |
| } |
| } |
| |
| /** |
| * Sets the feature flags to use in JUnit and instrumentation tests. |
| */ |
| @VisibleForTesting |
| public static void setFeaturesForTesting(Map<String, Boolean> features) { |
| assert features != null; |
| |
| sValuesOverridden.enableOverrides(); |
| |
| for (Map.Entry<String, Boolean> entry : features.entrySet()) { |
| String key = entry.getKey(); |
| |
| if (!sDefaults.containsKey(key)) { |
| continue; |
| } |
| |
| setForTesting(key, entry.getValue()); |
| } |
| } |
| |
| /** |
| * Caches flags that must take effect on startup but are set via native code. |
| */ |
| public static void cacheNativeFlags(List<String> featuresToCache) { |
| for (String featureName : featuresToCache) { |
| if (!sDefaults.containsKey(featureName)) { |
| throw new IllegalArgumentException( |
| "Feature " + featureName + " has no default in CachedFeatureFlags."); |
| } |
| cacheFeature(featureName); |
| } |
| } |
| |
| /** |
| * Caches a predetermined list of flags that must take effect on startup but are set via native |
| * code. |
| * |
| * Do not add new simple boolean flags here, use {@link #cacheNativeFlags} instead. |
| */ |
| public static void cacheAdditionalNativeFlags() { |
| cacheNetworkServiceWarmUpEnabled(); |
| cacheReachedCodeProfilerTrialGroup(); |
| |
| // Propagate REACHED_CODE_PROFILER feature value to LibraryLoader. This can't be done in |
| // LibraryLoader itself because it lives in //base and can't depend on ChromeFeatureList. |
| LibraryLoader.setReachedCodeProfilerEnabledOnNextRuns( |
| ChromeFeatureList.isEnabled(ChromeFeatureList.REACHED_CODE_PROFILER), |
| ChromeFeatureList.getFieldTrialParamByFeatureAsInt( |
| ChromeFeatureList.REACHED_CODE_PROFILER, "sampling_interval_us", 0)); |
| |
| // Similarly, propagate the BACKGROUND_THREAD_POOL feature value to LibraryLoader. |
| LibraryLoader.setBackgroundThreadPoolEnabledOnNextRuns( |
| ChromeFeatureList.isEnabled(ChromeFeatureList.BACKGROUND_THREAD_POOL)); |
| } |
| |
| /** |
| * Caches flags that must take effect on startup but are set via native code. |
| */ |
| public static void cacheFieldTrialParameters(List<CachedFieldTrialParameter> parameters) { |
| for (CachedFieldTrialParameter parameter : parameters) { |
| parameter.cacheToDisk(); |
| } |
| } |
| |
| public static void cacheMinimalBrowserFlagsTimeFromNativeTime() { |
| SharedPreferencesManager.getInstance().writeLong( |
| ChromePreferenceKeys.FLAGS_LAST_CACHED_MINIMAL_BROWSER_FLAGS_TIME_MILLIS, |
| System.currentTimeMillis()); |
| } |
| |
| /** |
| * Returns the time (in millis) the minimal browser flags were cached. |
| */ |
| public static long getLastCachedMinimalBrowserFlagsTimeMillis() { |
| return SharedPreferencesManager.getInstance().readLong( |
| ChromePreferenceKeys.FLAGS_LAST_CACHED_MINIMAL_BROWSER_FLAGS_TIME_MILLIS, 0); |
| } |
| |
| /** |
| * Cache whether warming up network service process is enabled, so that the value |
| * can be made available immediately on next start up. |
| */ |
| private static void cacheNetworkServiceWarmUpEnabled() { |
| SharedPreferencesManager.getInstance().writeBoolean( |
| ChromePreferenceKeys.FLAGS_CACHED_NETWORK_SERVICE_WARM_UP_ENABLED, |
| CachedFeatureFlagsJni.get().isNetworkServiceWarmUpEnabled()); |
| } |
| |
| /** |
| * @return whether warming up network service is enabled. |
| */ |
| public static boolean isNetworkServiceWarmUpEnabled() { |
| return getConsistentBooleanValue( |
| ChromePreferenceKeys.FLAGS_CACHED_NETWORK_SERVICE_WARM_UP_ENABLED, false); |
| } |
| |
| /** |
| * Caches the trial group of the reached code profiler feature to be using on next startup. |
| */ |
| private static void cacheReachedCodeProfilerTrialGroup() { |
| // Make sure that the existing value is saved in a static variable before overwriting it. |
| if (sReachedCodeProfilerTrialGroup == null) { |
| getReachedCodeProfilerTrialGroup(); |
| } |
| |
| SharedPreferencesManager.getInstance().writeString( |
| ChromePreferenceKeys.REACHED_CODE_PROFILER_GROUP, |
| FieldTrialList.findFullName(ChromeFeatureList.REACHED_CODE_PROFILER)); |
| } |
| |
| /** |
| * @return The trial group of the reached code profiler. |
| */ |
| @CalledByNative |
| public static String getReachedCodeProfilerTrialGroup() { |
| if (sReachedCodeProfilerTrialGroup == null) { |
| sReachedCodeProfilerTrialGroup = SharedPreferencesManager.getInstance().readString( |
| ChromePreferenceKeys.REACHED_CODE_PROFILER_GROUP, ""); |
| } |
| |
| return sReachedCodeProfilerTrialGroup; |
| } |
| |
| /** |
| * Call when entering an initialization flow that should result in caching flags. |
| */ |
| public static void onStartOrResumeCheckpoint() { |
| sSafeMode.onStartOrResumeCheckpoint(); |
| } |
| |
| /** |
| * Call when aborting an initialization flow that would have resulted in caching flags. |
| */ |
| public static void onPauseCheckpoint() { |
| sSafeMode.onPauseCheckpoint(); |
| } |
| |
| /** |
| * Call when finishing an initialization flow with flags having been cached successfully. |
| */ |
| public static void onEndCheckpoint() { |
| sSafeMode.onEndCheckpoint(sValuesReturned); |
| } |
| |
| public static @CachedFlagsSafeMode.Behavior int getSafeModeBehaviorForTesting() { |
| return sSafeMode.getBehaviorForTesting(); |
| } |
| |
| @AnyThread |
| static boolean getConsistentBooleanValue(String preferenceName, boolean defaultValue) { |
| sSafeMode.onFlagChecked(); |
| |
| if (sValuesOverridden.isEnabled()) { |
| return sValuesOverridden.getBool(preferenceName, defaultValue); |
| } |
| |
| Boolean flag; |
| synchronized (sValuesReturned.boolValues) { |
| flag = sValuesReturned.boolValues.get(preferenceName); |
| if (flag == null) { |
| flag = SharedPreferencesManager.getInstance().readBoolean( |
| preferenceName, defaultValue); |
| sValuesReturned.boolValues.put(preferenceName, flag); |
| } |
| } |
| return flag; |
| } |
| |
| @AnyThread |
| static String getConsistentStringValue(String preferenceName, String defaultValue) { |
| sSafeMode.onFlagChecked(); |
| |
| if (sValuesOverridden.isEnabled()) { |
| return sValuesOverridden.getString(preferenceName, defaultValue); |
| } |
| |
| String value; |
| synchronized (sValuesReturned.stringValues) { |
| value = sValuesReturned.stringValues.get(preferenceName); |
| if (value == null) { |
| value = SharedPreferencesManager.getInstance().readString( |
| preferenceName, defaultValue); |
| sValuesReturned.stringValues.put(preferenceName, value); |
| } |
| } |
| return value; |
| } |
| |
| @AnyThread |
| static int getConsistentIntValue(String preferenceName, int defaultValue) { |
| sSafeMode.onFlagChecked(); |
| |
| if (sValuesOverridden.isEnabled()) { |
| return sValuesOverridden.getInt(preferenceName, defaultValue); |
| } |
| |
| Integer value; |
| synchronized (sValuesReturned.intValues) { |
| value = sValuesReturned.intValues.get(preferenceName); |
| if (value == null) { |
| value = SharedPreferencesManager.getInstance().readInt( |
| preferenceName, defaultValue); |
| sValuesReturned.intValues.put(preferenceName, value); |
| } |
| } |
| return value; |
| } |
| |
| @AnyThread |
| static double getConsistentDoubleValue(String preferenceName, double defaultValue) { |
| sSafeMode.onFlagChecked(); |
| |
| if (sValuesOverridden.isEnabled()) { |
| return sValuesOverridden.getDouble(preferenceName, defaultValue); |
| } |
| |
| Double value; |
| synchronized (sValuesReturned.doubleValues) { |
| value = sValuesReturned.doubleValues.get(preferenceName); |
| if (value == null) { |
| value = SharedPreferencesManager.getInstance().readDouble( |
| preferenceName, defaultValue); |
| sValuesReturned.doubleValues.put(preferenceName, value); |
| } |
| } |
| return value; |
| } |
| |
| @AnyThread |
| private static String getPrefForFeatureFlag(String featureName) { |
| String legacyPrefKey = sNonDynamicPrefKeys.get(featureName); |
| if (legacyPrefKey == null) { |
| return ChromePreferenceKeys.FLAGS_CACHED.createKey(featureName); |
| } else { |
| return legacyPrefKey; |
| } |
| } |
| |
| @VisibleForTesting |
| public static void resetFlagsForTesting() { |
| sValuesReturned = new ValuesReturned(); |
| sValuesOverridden.clear(); |
| sSafeMode.clearMemoryForTesting(); |
| } |
| |
| @VisibleForTesting |
| public static void resetDiskForTesting() { |
| for (Map.Entry<String, Boolean> e : sDefaults.entrySet()) { |
| String prefKey = ChromePreferenceKeys.FLAGS_CACHED.createKey(e.getKey()); |
| SharedPreferencesManager.getInstance().removeKey(prefKey); |
| } |
| for (Map.Entry<String, String> e : sNonDynamicPrefKeys.entrySet()) { |
| String prefKey = e.getValue(); |
| SharedPreferencesManager.getInstance().removeKey(prefKey); |
| } |
| } |
| |
| @VisibleForTesting |
| static void setOverrideTestValue(String preferenceKey, String overrideValue) { |
| sValuesOverridden.setOverrideTestValue(preferenceKey, overrideValue); |
| } |
| |
| @VisibleForTesting |
| public static Map<String, Boolean> swapDefaultsForTesting(Map<String, Boolean> testDefaults) { |
| Map<String, Boolean> swapped = sDefaults; |
| sDefaults = testDefaults; |
| return swapped; |
| } |
| |
| @NativeMethods |
| interface Natives { |
| boolean isNetworkServiceWarmUpEnabled(); |
| } |
| } |