• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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