chromium/base/android/java/src/org/chromium/base/cached_flags/CachedFlag.java

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.base.cached_flags;

import android.content.SharedPreferences;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.FeatureMap;
import org.chromium.base.Flag;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.build.BuildConfig;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * CachedFlags are Flags that may be used before native is loaded and the FeatureList is
 * initialized.
 *
 * <p>They return a flag value read from native in a previous run, using SharedPreferences as
 * persistence.
 *
 * <p>@see {@link #isEnabled()} for more details about the logic.
 *
 * <p>To cache a flag from a {@link FeatureMap}, e.g. FooFeatureMap:
 *
 * <ul>
 *   <li>Create a static CachedFlag object in FooFeatureMap "sMyFlag"
 *   <li>Add it to the list FooFeatureMap#sFlagsCachedFullBrowser
 *   <li>Call {@code FooFeatureMap.sMyFlag.isEnabled()} 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.
 * </ul>
 *
 * <p>Metrics caveat: 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.
 */
public class CachedFlag extends Flag {
    private final boolean mDefaultValue;
    private String mPreferenceKey;

    public CachedFlag(FeatureMap featureMap, String featureName, boolean defaultValue) {
        super(featureMap, featureName);
        mDefaultValue = defaultValue;
    }

    /**
     * Constructor to use when a |defaultValueInTests| is specified to mimic reading from
     * field_trial_config.json.
     */
    public CachedFlag(
            FeatureMap featureMap,
            String featureName,
            boolean defaultValue,
            boolean defaultValueInTests) {
        super(featureMap, featureName);
        mDefaultValue = BuildConfig.IS_FOR_TEST ? defaultValueInTests : defaultValue;
    }

    /**
     * Rules from highest to lowest priority:
     *
     * <ul>
     *   <li>1. If the flag has been forced by @EnableFeatures/@DisableFeatures or {@link
     *       CachedFlag#setForTesting}, the forced value is returned.
     *   <li>2. If a value was previously returned in the same run, the same value is returned for
     *       consistency.
     *   <li>3. If in a previous run, the value from {@link FeatureMap} was cached to SharedPrefs,
     *       it is returned.
     *   <li>4. The |defaultValue| passed as a constructor parameter is returned.
     * </ul>
     */
    @Override
    public boolean isEnabled() {
        CachedFlagsSafeMode.getInstance().onFlagChecked();

        String preferenceName = getSharedPreferenceKey();

        Boolean flag;
        synchronized (ValuesReturned.sBoolValues) {
            Map<String, Boolean> boolValuesReturned = ValuesReturned.sBoolValues;
            flag = boolValuesReturned.get(preferenceName);
            if (flag != null) {
                return flag;
            }

            flag =
                    CachedFlagsSafeMode.getInstance()
                            .isEnabled(mFeatureName, preferenceName, mDefaultValue);
            if (flag == null) {
                SharedPreferencesManager prefs = CachedFlagsSharedPreferences.getInstance();
                if (prefs.contains(preferenceName)) {
                    flag = prefs.readBoolean(preferenceName, false);
                } else {
                    flag = mDefaultValue;
                }
            }

            boolValuesReturned.put(preferenceName, flag);
        }
        return flag;
    }

    /**
     * @return the default value to be returned if no value is cached.
     */
    public boolean getDefaultValue() {
        return mDefaultValue;
    }

    @Override
    protected void clearInMemoryCachedValueForTesting() {
        // ValuesReturned is cleared by CachedFlagUtils#resetFlagsForTesting().
    }

    /**
     * Forces a feature to be enabled or disabled for testing.
     *
     * @deprecated do not call this from tests; use @EnableFeatures/@DisableFeatures instead,
     * since batched tests need to be split by feature flag configuration.
     */
    @VisibleForTesting
    @Deprecated
    public void setForTesting(@Nullable Boolean value) {
        synchronized (ValuesReturned.sBoolValues) {
            ValuesReturned.sBoolValues.put(getSharedPreferenceKey(), value);
        }
    }

    /**
     * Writes the value of the feature from {@link FeatureMap} to the provided SharedPrefs Editor
     * for caching. Does not apply or commit the change - that is left up to the caller to perform.
     */
    void writeCacheValueToEditor(final SharedPreferences.Editor editor) {
        final boolean isEnabledInNative = mFeatureMap.isEnabledInNative(mFeatureName);
        editor.putBoolean(getSharedPreferenceKey(), isEnabledInNative);
    }

    String getSharedPreferenceKey() {
        // Create the key only once to avoid String concatenation every flag check.
        if (mPreferenceKey == null) {
            mPreferenceKey = CachedFlagsSharedPreferences.FLAGS_CACHED.createKey(mFeatureName);
        }
        return mPreferenceKey;
    }

    /**
     * Sets the feature flags to use in JUnit and instrumentation tests.
     *
     * @deprecated Do not call this from tests; use @EnableFeatures/@DisableFeatures annotations
     *     instead.
     */
    @Deprecated
    public static void setFeaturesForTesting(Map<String, Boolean> features) {
        synchronized (ValuesReturned.sBoolValues) {
            for (Map.Entry<String, Boolean> entry : features.entrySet()) {
                String featureName = entry.getKey();
                Boolean flagValue = entry.getValue();
                String sharedPreferencesKey =
                        CachedFlagsSharedPreferences.FLAGS_CACHED.createKey(featureName);
                ValuesReturned.sBoolValues.put(sharedPreferencesKey, flagValue);
            }
        }
    }

    /** Create a Map of feature names -> {@link CachedFlag} from multiple lists of CachedFlags. */
    public static Map<String, CachedFlag> createCachedFlagMap(
            List<List<CachedFlag>> allCachedFlagsLists) {
        HashMap<String, CachedFlag> cachedFlagMap = new HashMap<>();
        for (List<CachedFlag> cachedFlagsList : allCachedFlagsLists) {
            for (CachedFlag cachedFlag : cachedFlagsList) {
                cachedFlagMap.put(cachedFlag.getFeatureName(), cachedFlag);
            }
        }
        return cachedFlagMap;
    }
}