1 // Copyright 2020 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base; 6 7 import android.util.ArrayMap; 8 9 import androidx.annotation.NonNull; 10 import androidx.annotation.Nullable; 11 import androidx.annotation.VisibleForTesting; 12 13 import org.jni_zero.JNINamespace; 14 import org.jni_zero.NativeMethods; 15 16 import org.chromium.base.library_loader.LibraryLoader; 17 18 import java.util.HashMap; 19 import java.util.Map; 20 21 /** Provides shared capabilities for feature flag support. */ 22 @JNINamespace("base::android") 23 public class FeatureList { 24 /** Test value overrides for tests without native. */ 25 public static class TestValues { 26 private Map<String, Boolean> mFeatureFlags = new HashMap<>(); 27 private Map<String, Map<String, String>> mFieldTrialParams = new HashMap<>(); 28 29 /** Constructor. */ TestValues()30 public TestValues() {} 31 32 /** Set overrides for feature flags. */ setFeatureFlagsOverride(Map<String, Boolean> featureFlags)33 public void setFeatureFlagsOverride(Map<String, Boolean> featureFlags) { 34 mFeatureFlags = featureFlags; 35 } 36 37 /** Add an override for a feature flag. */ addFeatureFlagOverride(String featureName, boolean testValue)38 public void addFeatureFlagOverride(String featureName, boolean testValue) { 39 mFeatureFlags.put(featureName, testValue); 40 } 41 42 /** Add an override for a field trial parameter. */ addFieldTrialParamOverride( String featureName, String paramName, String testValue)43 public void addFieldTrialParamOverride( 44 String featureName, String paramName, String testValue) { 45 Map<String, String> featureParams = mFieldTrialParams.get(featureName); 46 if (featureParams == null) { 47 featureParams = new ArrayMap<>(); 48 mFieldTrialParams.put(featureName, featureParams); 49 } 50 featureParams.put(paramName, testValue); 51 } 52 getFeatureFlagOverride(String featureName)53 Boolean getFeatureFlagOverride(String featureName) { 54 return mFeatureFlags.get(featureName); 55 } 56 getFieldTrialParamOverride(String featureName, String paramName)57 String getFieldTrialParamOverride(String featureName, String paramName) { 58 Map<String, String> featureParams = mFieldTrialParams.get(featureName); 59 if (featureParams == null) return null; 60 return featureParams.get(paramName); 61 } 62 getAllFieldTrialParamOverridesForFeature(String featureName)63 Map<String, String> getAllFieldTrialParamOverridesForFeature(String featureName) { 64 return mFieldTrialParams.get(featureName); 65 } 66 } 67 68 /** Map that stores substitution feature flags for tests. */ 69 private static @Nullable TestValues sTestFeatures; 70 71 /** Access to default values of the native feature flag. */ 72 private static boolean sTestCanUseDefaults; 73 FeatureList()74 private FeatureList() {} 75 76 /** 77 * @return Whether the native FeatureList has been initialized. If this method returns false, 78 * none of the methods in this class that require native access should be called (except 79 * in tests if test features have been set). 80 */ isInitialized()81 public static boolean isInitialized() { 82 return hasTestFeatures() || isNativeInitialized(); 83 } 84 85 /** 86 * @return Whether the native FeatureList is initialized or not. 87 */ isNativeInitialized()88 public static boolean isNativeInitialized() { 89 if (!LibraryLoader.getInstance().isInitialized()) return false; 90 // Even if the native library is loaded, the C++ FeatureList might not be initialized yet. 91 // In that case, accessing it will not immediately fail, but instead cause a crash later 92 // when it is initialized. Return whether the native FeatureList has been initialized, 93 // so the return value can be tested, or asserted for a more actionable stack trace 94 // on failure. 95 // 96 // The FeatureList is however guaranteed to be initialized by the time 97 // AsyncInitializationActivity#finishNativeInitialization is called. 98 return FeatureListJni.get().isInitialized(); 99 } 100 101 /** 102 * This is called explicitly for instrumentation tests via Features#applyForInstrumentation(). 103 * Unit tests and Robolectric tests must not invoke this and should rely on the {@link Features} 104 * annotations to enable or disable any feature flags. 105 */ setTestCanUseDefaultsForTesting()106 public static void setTestCanUseDefaultsForTesting() { 107 sTestCanUseDefaults = true; 108 ResettersForTesting.register(() -> sTestCanUseDefaults = false); 109 } 110 111 /** 112 * We reset the value to false after the instrumentation test to avoid any unwanted 113 * persistence of the state. This is invoked by Features#reset(). 114 */ resetTestCanUseDefaultsForTesting()115 public static void resetTestCanUseDefaultsForTesting() { 116 sTestCanUseDefaults = false; 117 } 118 119 /** Sets the feature flags to use in JUnit tests, since native calls are not available there. */ 120 @VisibleForTesting setTestFeatures(Map<String, Boolean> testFeatures)121 public static void setTestFeatures(Map<String, Boolean> testFeatures) { 122 if (testFeatures == null) { 123 setTestValues(null); 124 } else { 125 TestValues testValues = new TestValues(); 126 testValues.setFeatureFlagsOverride(testFeatures); 127 setTestValues(testValues); 128 } 129 } 130 131 /** 132 * Sets the feature flags and field trial parameters to use in JUnit tests, since native calls 133 * are not available there. 134 */ 135 @VisibleForTesting setTestValues(TestValues testFeatures)136 public static void setTestValues(TestValues testFeatures) { 137 sTestFeatures = testFeatures; 138 ResettersForTesting.register(() -> sTestFeatures = null); 139 } 140 141 /** 142 * Adds overrides to feature flags and field trial parameters in addition to existing ones. 143 * 144 * @param testValuesToMerge the TestValues to merge into existing ones 145 * @param replace if true, replaces existing values (e.g. from @EnableFeatures annotations) 146 */ mergeTestValues(@onNull TestValues testValuesToMerge, boolean replace)147 public static void mergeTestValues(@NonNull TestValues testValuesToMerge, boolean replace) { 148 TestValues newTestValues; 149 if (sTestFeatures == null) { 150 newTestValues = new TestValues(); 151 } else { 152 newTestValues = sTestFeatures; 153 } 154 155 if (replace) { 156 newTestValues.mFeatureFlags.putAll(testValuesToMerge.mFeatureFlags); 157 } else { 158 for (Map.Entry<String, Boolean> toMerge : testValuesToMerge.mFeatureFlags.entrySet()) { 159 newTestValues.mFeatureFlags.putIfAbsent(toMerge.getKey(), toMerge.getValue()); 160 } 161 } 162 163 for (Map.Entry<String, Map<String, String>> e : 164 testValuesToMerge.mFieldTrialParams.entrySet()) { 165 String featureName = e.getKey(); 166 var fieldTrialParamsForFeature = newTestValues.mFieldTrialParams.get(featureName); 167 if (fieldTrialParamsForFeature == null) { 168 fieldTrialParamsForFeature = new ArrayMap<>(); 169 newTestValues.mFieldTrialParams.put(featureName, fieldTrialParamsForFeature); 170 } 171 172 if (replace) { 173 fieldTrialParamsForFeature.putAll(e.getValue()); 174 } else { 175 for (Map.Entry<String, String> toMerge : e.getValue().entrySet()) { 176 fieldTrialParamsForFeature.putIfAbsent(toMerge.getKey(), toMerge.getValue()); 177 } 178 } 179 } 180 181 setTestValues(newTestValues); 182 } 183 184 /** 185 * @return Whether test feature values have been configured. 186 */ hasTestFeatures()187 public static boolean hasTestFeatures() { 188 return sTestFeatures != null; 189 } 190 191 /** 192 * @param featureName The name of the feature to query. 193 * @return Whether the feature has a test value configured. 194 */ hasTestFeature(String featureName)195 public static boolean hasTestFeature(String featureName) { 196 // TODO(crbug.com/1434471)): Copy into a local reference to avoid race conditions 197 // like crbug.com/1494095 unsetting the test features. Locking down flag state will allow 198 // this mitigation to be removed. 199 TestValues testValues = sTestFeatures; 200 return testValues != null && testValues.mFeatureFlags.containsKey(featureName); 201 } 202 203 /** 204 * Returns the test value of the feature with the given name. 205 * 206 * @param featureName The name of the feature to query. 207 * @return The test value set for the feature, or null if no test value has been set. 208 * @throws IllegalArgumentException if no test value was set and default values aren't allowed. 209 */ getTestValueForFeature(String featureName)210 public static Boolean getTestValueForFeature(String featureName) { 211 // TODO(crbug.com/1434471)): Copy into a local reference to avoid race conditions 212 // like crbug.com/1494095 unsetting the test features. Locking down flag state will allow 213 // this mitigation to be removed. 214 TestValues testValues = sTestFeatures; 215 if (testValues != null) { 216 Boolean override = testValues.getFeatureFlagOverride(featureName); 217 if (override != null) { 218 return override; 219 } 220 if (!sTestCanUseDefaults) { 221 throw new IllegalArgumentException( 222 "No test value configured for " 223 + featureName 224 + " and native is not available to provide a default value. Use" 225 + " @EnableFeatures or @DisableFeatures to provide test values for" 226 + " the flag."); 227 } 228 } 229 return null; 230 } 231 232 /** 233 * Returns the test value of the field trial parameter. 234 * 235 * @param featureName The name of the feature to query. 236 * @param paramName The name of the field trial parameter to query. 237 * @return The test value set for the parameter, or null if no test value has been set. 238 */ getTestValueForFieldTrialParam(String featureName, String paramName)239 public static String getTestValueForFieldTrialParam(String featureName, String paramName) { 240 // TODO(crbug.com/1434471)): Copy into a local reference to avoid race conditions 241 // like crbug.com/1494095 unsetting the test features. Locking down flag state will allow 242 // this mitigation to be removed. 243 TestValues testValues = sTestFeatures; 244 if (testValues != null) { 245 return testValues.getFieldTrialParamOverride(featureName, paramName); 246 } 247 return null; 248 } 249 250 /** 251 * Returns the test value of the all field trial parameters of a given feature. 252 * 253 * @param featureName The name of the feature to query all parameters. 254 * @return The test values set for the parameter, or null if no test values have been set (if 255 * test values were set for other features, an empty Map will be returned, not null). 256 */ getTestValuesForAllFieldTrialParamsForFeature( String featureName)257 public static Map<String, String> getTestValuesForAllFieldTrialParamsForFeature( 258 String featureName) { 259 // TODO(crbug.com/1434471)): Copy into a local reference to avoid race conditions 260 // like crbug.com/1494095 unsetting the test features. Locking down flag state will allow 261 // this mitigation to be removed. 262 TestValues testValues = sTestFeatures; 263 if (testValues != null) { 264 return testValues.getAllFieldTrialParamOverridesForFeature(featureName); 265 } 266 return null; 267 } 268 269 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) 270 @NativeMethods 271 public interface Natives { isInitialized()272 boolean isInitialized(); 273 } 274 } 275