chromium/components/policy/android/java/src/org/chromium/components/policy/PolicyCache.java

// Copyright 2021 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.components.policy;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Pair;

import androidx.annotation.VisibleForTesting;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import org.chromium.base.ContextUtils;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;

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

/**
 * Manage policy cache that will be used during browser launch stage.
 *
 * Policy loading is async on Android and caching policy values makes them
 * available during launch stage even before native library is ready.
 */
public class PolicyCache {
    @VisibleForTesting static final String POLICY_PREF = "Components.Policy";

    private static PolicyCache sInstance;

    public enum Type {
        Integer,
        Boolean,
        String,
        List,
        Dict,
    }

    private boolean mReadable = true;

    private SharedPreferences mSharedPreferences;

    private ThreadUtils.ThreadChecker mThreadChecker = new ThreadUtils.ThreadChecker();

    /**
     * Creates and returns SharedPreferences instance that is used to cache policy
     * value.
     *
     * @return The SharedPreferences instance that is used for policy caching. Returns null if
     *         application context is not available.
     */
    private SharedPreferences getSharedPreferences() {
        assert mReadable;
        mThreadChecker.assertOnValidThread();
        if (mSharedPreferences == null) {
            Context context = ContextUtils.getApplicationContext();
            // Policy cache is not accessiable without application context.
            if (context == null) return null;
            try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
                mSharedPreferences =
                        context.getSharedPreferences(POLICY_PREF, Context.MODE_PRIVATE);
            }
        }
        return mSharedPreferences;
    }

    private SharedPreferences.Editor getSharedPreferencesEditor() {
        mThreadChecker.assertOnValidThread();
        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
            return ContextUtils.getApplicationContext()
                    .getSharedPreferences(POLICY_PREF, Context.MODE_PRIVATE)
                    .edit();
        }
    }

    public static PolicyCache get() {
        var ret = sInstance;
        if (ret == null) {
            ret = new PolicyCache();
            sInstance = ret;
            ResettersForTesting.register(() -> sInstance = null);
        }
        return ret;
    }

    /**
     * @param policy The name of policy.
     * @return The value of cached integer policy, null if there is no valid
     * cached policy.
     */
    public Integer getIntValue(String policy) {
        SharedPreferences sharedPreferences = getSharedPreferences();
        if (sharedPreferences == null) return null;
        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
            if (!sharedPreferences.contains(policy)) return null;
            return sharedPreferences.getInt(policy, 0);
        }
    }

    /**
     * @param policy The name of policy.
     * @return The value of cached boolean policy, null if there is no valid
     * cached policy.
     */
    public Boolean getBooleanValue(String policy) {
        SharedPreferences sharedPreferences = getSharedPreferences();
        if (sharedPreferences == null) return null;
        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
            if (!sharedPreferences.contains(policy)) return null;
            return sharedPreferences.getBoolean(policy, false);
        }
    }

    /**
     * @param policy The name of policy.
     * @return The value of cached string policy, null if there is no valid
     * cached policy.
     */
    public String getStringValue(String policy) {
        SharedPreferences sharedPreferences = getSharedPreferences();
        if (sharedPreferences == null) return null;
        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
            if (!sharedPreferences.contains(policy)) return null;
            return sharedPreferences.getString(policy, null);
        }
    }

    /**
     * @param policy The name of policy.
     * @return The value of cached list policy, null if there is no valid
     * cached policy.
     */
    public JSONArray getListValue(String policy) {
        SharedPreferences sharedPreferences = getSharedPreferences();
        if (sharedPreferences == null) return null;
        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
            if (!sharedPreferences.contains(policy)) return null;
            try {
                return new JSONArray(sharedPreferences.getString(policy, null));
            } catch (JSONException e) {
                return null;
            }
        }
    }

    /**
     * @param policy The name of policy.
     * @return The value of cached dictionary policy, null if there is no valid
     * cached policy.
     */
    public JSONObject getDictValue(String policy) {
        SharedPreferences sharedPreferences = getSharedPreferences();
        if (sharedPreferences == null) return null;
        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
            if (!sharedPreferences.contains(policy)) return null;
            try {
                return new JSONObject(sharedPreferences.getString(policy, null));
            } catch (JSONException e) {
                return null;
            }
        }
    }

    /** @return All cached policies. */
    public Map<String, ?> getAllPolicies() {
        SharedPreferences sharedPreferences = getSharedPreferences();
        if (sharedPreferences == null) return null;
        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
            return sharedPreferences.getAll();
        }
    }

    public boolean isReadable() {
        return mReadable;
    }

    /**
     * @param policyMap The latest policy value bundle.
     * @param policyNames The list of policies that needs to be cached if available.
     * Caches the policies that are available in both |policyNames| and
     * |policyMap|. It also disables {@link PolicyCache} reading.
     */
    public void cachePolicies(PolicyMap policyMap, List<Pair<String, Type>> policyNames) {
        // TODO(zmin): support policy level while caching policy.
        SharedPreferences.Editor sharedPreferencesEditor = getSharedPreferencesEditor();

        sharedPreferencesEditor.clear();

        for (Pair<String, Type> policy : policyNames) {
            String policyName = policy.first;
            switch (policy.second) {
                case Integer:
                    {
                        Integer value = policyMap.getIntValue(policyName);
                        if (value != null) {
                            sharedPreferencesEditor.putInt(policyName, value.intValue());
                        }
                        break;
                    }
                case Boolean:
                    {
                        Boolean value = policyMap.getBooleanValue(policyName);
                        if (value != null) {
                            sharedPreferencesEditor.putBoolean(policyName, value.booleanValue());
                        }
                        break;
                    }
                case String:
                    {
                        String value = policyMap.getStringValue(policyName);
                        if (value != null) {
                            sharedPreferencesEditor.putString(policyName, value);
                        }
                        break;
                    }
                    // List and Dict policy values are stored in the native library
                    // as base::Value and converted to JSON string to passed through
                    // the JNI. It's stored to the SharedPreferences as String and
                    // will be converted to JSON object when being read.
                case List:
                    {
                        String value = policyMap.getListValueAsString(policyName);
                        if (value != null) {
                            sharedPreferencesEditor.putString(policyName, value);
                        }
                        break;
                    }
                case Dict:
                    {
                        String value = policyMap.getDictValueAsString(policyName);
                        if (value != null) {
                            sharedPreferencesEditor.putString(policyName, value);
                        }
                        break;
                    }
            }
        }

        // Update sharedPreferences. The first round of updating during launch
        // will use the main thread.
        try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
            sharedPreferencesEditor.apply();
        }

        // Policy Service is up and there is no need to get policy from here anymore.
        enableWriteOnlyMode();
    }

    /** Delete all entries from the cache. */
    public void reset() {
        SharedPreferences.Editor sharedPreferencesEditor = getSharedPreferencesEditor();
        sharedPreferencesEditor.clear();
        sharedPreferencesEditor.apply();
    }

    private void enableWriteOnlyMode() {
        mSharedPreferences = null;
        mReadable = false;
    }

    public void setReadableForTesting(boolean readable) {
        mReadable = readable;
    }
}