chromium/chrome/browser/ui/android/hats/java/src/org/chromium/chrome/browser/ui/hats/SurveyConfig.java

// Copyright 2023 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.chrome.browser.ui.hats;

import android.text.TextUtils;

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

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.ResettersForTesting;

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

/**
 * Java equivalent of C++ survey::SurveyConfig. All member variable are kept package protected, as
 * they should not be access outside of survey code. To add a new SurveyConfig, see
 * //chrome/browser/ui/hats/survey_config.*
 */
@JNINamespace("hats")
public class SurveyConfig {

    private static SurveyConfig sConfigForTesting;

    /** Unique key associate with the config. */
    final String mTrigger;

    /**
     * TriggerId used to download & present the associated survey from HaTS service. The triggerId
     * can be configured in survey_config.cc. TriggerIds can be supplied dynamically as well, in
     * which case they temporarily hide the statically configured triggerId. @see
     * Holder#getSurveyConfig.
     */
    final String mTriggerId;

    /** Probability [0,1] of how likely a chosen user will see the survey. */
    final double mProbability;

    /**
     * Whether the survey will prompt every time because the user has explicitly decided to take the
     * survey e.g. clicking a link.
     */
    final boolean mUserPrompted;

    /** Product Specific Bit Data fields which are sent with the survey response. */
    final String[] mPsdBitDataFields;

    /** Product Specific String Data fields which are sent with the survey response. */
    final String[] mPsdStringDataFields;

    /** Not generated from java. */
    @VisibleForTesting
    SurveyConfig(
            String trigger,
            String triggerId,
            double probability,
            boolean userPrompted,
            String[] psdBitDataFields,
            String[] psdStringDataFields) {
        mTrigger = trigger;
        mTriggerId = triggerId;
        mProbability = probability;
        mUserPrompted = userPrompted;
        mPsdBitDataFields = psdBitDataFields;
        mPsdStringDataFields = psdStringDataFields;
    }

    /**
     * Get the survey config with the associate trigger if the survey config exists and enabled;
     * returns null otherwise.
     *
     * @param trigger The trigger associated with the SurveyConfig.
     * @return SurveyConfig if the survey exists and is enabled.
     */
    @Nullable
    public static SurveyConfig get(String trigger) {
        return get(trigger, "");
    }

    /**
     * Get the survey config with the associate trigger if the survey config exists and enabled;
     * returns null otherwise. Allows dynamically supplying triggerIds for survey configs.
     *
     * @param trigger The trigger associated with the SurveyConfig.
     * @param suppliedTriggerId if non-empty, creates an otherwise equivalent config with the
     *     triggerId set to suppliedTriggerId
     * @return SurveyConfig if the survey exists and is enabled.
     */
    @Nullable
    public static SurveyConfig get(String trigger, String suppliedTriggerId) {
        SurveyConfig config;
        if (sConfigForTesting != null && sConfigForTesting.mTrigger.equals(trigger)) {
            config = sConfigForTesting;
        } else {
            config = Holder.getInstance().getSurveyConfig(trigger);
        }
        return getConfigWithSuppliedTriggerIdIfPresent(config, suppliedTriggerId);
    }

    /** Return the dump of input Survey config for debugging purposes. */
    public static String toString(SurveyConfig config) {
        if (config == null) return "";

        StringBuilder sb = new StringBuilder();
        sb.append("Trigger=")
                .append(config.mTrigger)
                .append(" TriggerId=")
                .append(config.mTriggerId)
                .append(" Probability=")
                .append(config.mProbability)
                .append(" UserPrompted=")
                .append(config.mUserPrompted);

        sb.append(" PsdBitFields=");
        for (String field : config.mPsdBitDataFields) {
            sb.append(field).append(",");
        }

        sb.append(" PsdStringFields=");
        for (String field : config.mPsdStringDataFields) {
            sb.append(field).append(",");
        }

        return sb.toString();
    }

    static void setSurveyConfigForTesting(SurveyConfig config) {
        sConfigForTesting = config;
        ResettersForTesting.register(() -> sConfigForTesting = null);
    }

    /** Clear all the initialized configs. */
    static void clearAll() {
        Holder.getInstance().destroy();
    }

    static SurveyConfig getConfigWithSuppliedTriggerIdIfPresent(
            SurveyConfig config, String suppliedTriggerId) {
        if (config != null && !TextUtils.isEmpty(suppliedTriggerId)) {
            return new SurveyConfig(
                    config.mTrigger,
                    suppliedTriggerId,
                    config.mProbability,
                    config.mUserPrompted,
                    config.mPsdBitDataFields,
                    config.mPsdStringDataFields);
        }
        return config;
    }

    @CalledByNative
    private static void addActiveSurveyConfigToHolder(
            Holder holder,
            String trigger,
            String triggerId,
            double probability,
            boolean userPrompted,
            String[] psdBitDataFields,
            String[] psdStringDataFields) {
        holder.mTriggers.put(
                trigger,
                new SurveyConfig(
                        trigger,
                        triggerId,
                        probability,
                        userPrompted,
                        psdBitDataFields,
                        psdStringDataFields));
    }

    /** Holder that stores all the active surveys for Android. */
    static class Holder {

        private static Holder sInstance;
        private final Map<String, SurveyConfig> mTriggers;
        private long mNativeInstance;

        private Holder() {
            mTriggers = new HashMap<>();
            mNativeInstance = SurveyConfigJni.get().initHolder(this);
        }

        static Holder getInstance() {
            if (sInstance == null) {
                sInstance = new Holder();
            }
            return sInstance;
        }

        SurveyConfig getSurveyConfig(String trigger) {
            return mTriggers.get(trigger);
        }

        void destroy() {
            SurveyConfigJni.get().destroy(mNativeInstance);
            mNativeInstance = 0;
        }
    }

    @NativeMethods
    interface Natives {

        long initHolder(Holder caller);

        void destroy(long nativeSurveyConfigHolder);
    }
}