chromium/components/metrics/android/java/src/org/chromium/components/metrics/LowEntropySource.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.components.metrics;

import android.content.SharedPreferences;

import androidx.annotation.MainThread;

import org.jni_zero.CalledByNative;

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

import java.util.Random;

/**
 * Generates a new non-identifying entropy source used to seed persistent activities. Has a static
 * cache so that the new low entropy source value will only be generated on first access.
 * Low entropy source is queried by entropy_source.cc that caches it in prefs. On Android, it is
 * generated in Java so that it can be used by FRE experiments when the native is not available yet.
 */
@MainThread
public class LowEntropySource {
    // Should be equal to the value of EntropyState::kMaxLowEntropySize in C++.
    public static final int MAX_LOW_ENTROPY_SIZE = 8000;

    private static final class LazyHolder {
        private static final int LOW_ENTROPY_SOURCE_STATIC_CACHE = generateInternal();
    }

    private static final class LazyHolderForPseudo {
        private static final int PSEUDO_LOW_ENTROPY_SOURCE_STATIC_CACHE = generateInternal();
    }

    private static final String KEY_LOW_ENTROPY_SOURCE_FRE_COMPLETED =
            "low_entropy_source_fre_completed";

    /**
     * Provides access to non-identifying entropy source for FRE trials.
     * See {@link #generateLowEntropySource()} for details.
     *
     * WARNING: This should only be used on first run, as otherwise the value generated by this code
     * will not correspond to what is used on the C++ side. Calling this method after FRE is
     * completed will throw an exception.
     */
    public static int generateLowEntropySourceForFirstRunTrial() {
        ThreadUtils.assertOnUiThread();
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        if (prefs.getBoolean(KEY_LOW_ENTROPY_SOURCE_FRE_COMPLETED, false)) {
            throw new IllegalStateException(
                    "LowEntropySource can't be used from Java after FRE has been completed!");
        }

        return generateLowEntropySource();
    }

    /**
     * Should be invoked when the FRE is completed to notify LowEntropySource that
     * {@link #generateLowEntropySourceForFirstRunTrial} can no longer be used.
     */
    public static void markFirstRunComplete() {
        ThreadUtils.assertOnUiThread();
        SharedPreferences.Editor editor = ContextUtils.getAppSharedPreferences().edit();
        editor.putBoolean(KEY_LOW_ENTROPY_SOURCE_FRE_COMPLETED, true);
        editor.apply();
    }

    /**
     * Generates a new non-identifying entropy source. Has a static cache, so subsequent calls will
     * return the same value during the process lifetime.
     */
    @CalledByNative
    private static int generateLowEntropySource() {
        return LazyHolder.LOW_ENTROPY_SOURCE_STATIC_CACHE;
    }

    /**
     * Generates a new non-identifying low entropy source using the same method that's used for the
     * actual low entropy source. This one, however, is only used for statistical validation, and
     * *not* for randomization or experiment assignment.
     */
    @CalledByNative
    private static int generatePseudoLowEntropySource() {
        return LazyHolderForPseudo.PSEUDO_LOW_ENTROPY_SOURCE_STATIC_CACHE;
    }

    private static int generateInternal() {
        return new Random().nextInt(MAX_LOW_ENTROPY_SIZE);
    }
}