chromium/chromecast/base/cast_features.cc

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

#include "chromecast/base/cast_features.h"

#include <algorithm>
#include <utility>

#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_param_associator.h"
#include "base/metrics/field_trial_params.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "build/build_config.h"

namespace chromecast {
namespace {

// The name of the default group to use for Cast DCS features.
const char kDefaultDCSFeaturesGroup[] = "default_dcs_features_group";

std::unordered_set<int32_t>& GetExperimentIds() {
  static base::NoDestructor<std::unordered_set<int32_t>> g_experiment_ids;
  return *g_experiment_ids;
}

bool g_experiment_ids_initialized = false;

// The collection of features that have been registered by unit tests
std::vector<const base::Feature*>& GetTestFeatures() {
  static base::NoDestructor<std::vector<const base::Feature*>>
      features_for_test;
  return *features_for_test;
}

void SetExperimentIds(const base::Value::List& list) {
  DCHECK(!g_experiment_ids_initialized);
  std::unordered_set<int32_t> ids;
  for (const auto& it : list) {
    if (it.is_int()) {
      ids.insert(it.GetInt());
    } else {
      LOG(ERROR) << "Non-integer value found in experiment id list!";
    }
  }
  GetExperimentIds().swap(ids);
  g_experiment_ids_initialized = true;
}

}  // namespace

// PLEASE READ!
// Cast Platform Features are listed below. These features may be
// toggled via configs fetched from DCS for devices in the field, or via
// command-line flags set by the developer. For the end-to-end details of the
// system design, please see go/dcs-experiments.
//
// Below are useful steps on how to use these features in your code.
//
// 1) Declaring and defining the feature.
//    All Cast Platform Features should be declared in a common file with other
//    Cast Platform Features (ex. chromecast/base/cast_features.h). When
//    defining your feature, you will need to assign a default value. This is
//    the value that the feature will hold until overriden by the server or the
//    command line. Here's an exmaple:
//
//      BASE_FEATURE(kSuperSecretSauce, "SuperSecretSauce",
//                   base::FEATURE_DISABLED_BY_DEFAULT);
//
//    IMPORTANT NOTE:
//    The first parameter that you pass in the definition is the feature's name.
//    This MUST match the DCS experiment key for this feature.
//
//    While Features elsewhere in Chromium alternatively use dashed-case or
//    PascalCase for their names, Chromecast features should use snake_case
//    (lowercase letters separated by underscores). This will ensure that DCS
//    configs, which are passed around as JSON, remain conformant and readable.
//
// 2) Using the feature in client code.
//    Using these features in your code is easy. Here's an example:
//
//      #include “base/feature_list.h”
//      #include “chromecast/base/chromecast_switches.h”
//
//      std::unique_ptr<Foo> CreateFoo() {
//        if (base::FeatureList::IsEnabled(kSuperSecretSauce))
//          return std::make_unique<SuperSecretFoo>();
//        return std::make_unique<BoringOldFoo>();
//      }
//
//    base::FeatureList can be called from any thread, in any process, at any
//    time after PreCreateThreads(). It will return whether the feature is
//    enabled.
//
// 3) Overriding the default value from the server.
//    For devices in the field, DCS will issue different configs to different
//    groups of devices, allowing us to run experiments on features. These
//    feature settings will manifest on the next boot of cast_shell. In the
//    example, if the latest config for a particular device set the value of
//    kSuperSecretSauce to true, the appropriate code path would be taken.
//    Otherwise, the default value, false, would be used. For more details on
//    setting up experiments, see go/dcs-launch.
//
// 4) Overriding the default and server values from the command-line.
//    While the server value trumps the default values, the command line trumps
//    both. Enable features by passing this command line arg to cast_shell:
//
//      --enable-features=enable_foo,enable_super_secret_sauce
//
//    Features are separated by commas. Disable features by passing:
//
//      --disable-features=enable_foo,enable_bar
//
// 5) If you add a new feature to the system you must include it in kFeatures
//    This is because the system relies on knowing all of the features so
//    it can properly iterate over all features to detect changes.
//

// Begin Chromecast Feature definitions.

// Allows applications to access media capture devices (webcams/microphones)
// through getUserMedia API.
BASE_FEATURE(kAllowUserMediaAccess,
             "allow_user_media_access",
             base::FEATURE_DISABLED_BY_DEFAULT);
// Enables the use of QUIC in Cast-specific NetworkContexts. See
// chromecast/browser/cast_network_contexts.cc for usage.
BASE_FEATURE(kEnableQuic, "enable_quic", base::FEATURE_DISABLED_BY_DEFAULT);
// Enables triple-buffer 720p graphics (overriding default graphics buffer
// settings for a platform).
BASE_FEATURE(kTripleBuffer720,
             "enable_triple_buffer_720",
             base::FEATURE_DISABLED_BY_DEFAULT);
// Enables single-buffered graphics (overriding default graphics buffer
// settings and takes precedence over triple-buffer feature).
BASE_FEATURE(kSingleBuffer,
             "enable_single_buffer",
             base::FEATURE_DISABLED_BY_DEFAULT);
// Disable idle sockets closing on memory pressure. See
// chromecast/browser/cast_network_contexts.cc for usage.
BASE_FEATURE(kDisableIdleSocketsCloseOnMemoryPressure,
             "disable_idle_sockets_close_on_memory_pressure",
             base::FEATURE_DISABLED_BY_DEFAULT);

BASE_FEATURE(kEnableGeneralAudienceBrowsing,
             "enable_general_audience_browsing",
             base::FEATURE_DISABLED_BY_DEFAULT);

BASE_FEATURE(kEnableSideGesturePassThrough,
             "enable_side_gesture_pass_through",
             base::FEATURE_DISABLED_BY_DEFAULT);

// Uses AudioManagerAndroid, instead of CastAudioManagerAndroid. This will
// disable lots of Cast features, so it should only be used for development and
// testing.
BASE_FEATURE(kEnableChromeAudioManagerAndroid,
             "enable_chrome_audio_manager_android",
             base::FEATURE_DISABLED_BY_DEFAULT);

// Enables CastAudioOutputDevice for audio output on Android. When disabled,
// CastAudioManagerAndroid will be used.
BASE_FEATURE(kEnableCastAudioOutputDevice,
             "enable_cast_audio_output_device",
             base::FEATURE_DISABLED_BY_DEFAULT);

// End Chromecast Feature definitions.
const base::Feature* kFeatures[] = {
    &kAllowUserMediaAccess,
    &kEnableQuic,
    &kTripleBuffer720,
    &kSingleBuffer,
    &kDisableIdleSocketsCloseOnMemoryPressure,
    &kEnableGeneralAudienceBrowsing,
    &kEnableSideGesturePassThrough,
    &kEnableChromeAudioManagerAndroid,
    &kEnableCastAudioOutputDevice,
};

std::vector<const base::Feature*> GetInternalFeatures();

const std::vector<const base::Feature*>& GetFeatures() {
  static const base::NoDestructor<std::vector<const base::Feature*>> features(
      [] {
        std::vector<const base::Feature*> features(std::begin(kFeatures),
                                                   std::end(kFeatures));
        auto internal_features = GetInternalFeatures();
        features.insert(features.end(), internal_features.begin(),
                        internal_features.end());
        return features;
      }());
  if (GetTestFeatures().size() > 0)
    return GetTestFeatures();
  return *features;
}

void InitializeFeatureList(const base::Value::Dict& dcs_features,
                           const base::Value::List& dcs_experiment_ids,
                           const std::string& cmd_line_enable_features,
                           const std::string& cmd_line_disable_features,
                           const std::string& extra_enable_features,
                           const std::string& extra_disable_features) {
  DCHECK(!base::FeatureList::GetInstance());

  // Set the experiments.
  SetExperimentIds(dcs_experiment_ids);

  std::string all_enable_features =
      cmd_line_enable_features + "," + extra_enable_features;
  std::string all_disable_features =
      cmd_line_disable_features + "," + extra_disable_features;

  // Initialize the FeatureList from the command line.
  auto feature_list = std::make_unique<base::FeatureList>();
  feature_list->InitFromCommandLine(all_enable_features, all_disable_features);

  // Override defaults from the DCS config.
  for (const auto kv : dcs_features) {
    // Each feature must have its own FieldTrial object. Since experiments are
    // controlled server-side for Chromecast, and this class is designed with a
    // client-side experimentation framework in mind, these parameters are
    // carefully chosen:
    //   - The field trial name is unused for our purposes. However, we need to
    //     maintain a 1:1 mapping with Features in order to properly store and
    //     access parameters associated with each Feature. Therefore, use the
    //     Feature's name as the FieldTrial name to ensure uniqueness.
    //   - We don't care about the group_id.
    //
    const std::string& feature_name = kv.first;
    auto* field_trial = base::FieldTrialList::CreateFieldTrial(
        feature_name, kDefaultDCSFeaturesGroup);

    // |field_trial| is null only if the trial has already been forced to
    // another group. This shouldn't happen, unless we've processed a
    // --force-fieldtrial commandline argument that overrides this to some other
    // group.
    if (!field_trial) {
      LOG(ERROR) << "A trial was already created for a DCS feature: "
                 << feature_name;
      continue;
    }

    if (kv.second.is_bool()) {
      // A boolean entry simply either enables or disables a feature.
      feature_list->RegisterFieldTrialOverride(
          feature_name,
          kv.second.GetBool() ? base::FeatureList::OVERRIDE_ENABLE_FEATURE
                              : base::FeatureList::OVERRIDE_DISABLE_FEATURE,
          field_trial);
      continue;
    }

    if (kv.second.is_dict()) {
      // A dictionary entry implies that the feature is enabled.
      feature_list->RegisterFieldTrialOverride(
          feature_name, base::FeatureList::OVERRIDE_ENABLE_FEATURE,
          field_trial);

      // If the feature has not been overriden from the command line, set its
      // parameters accordingly.
      if (!feature_list->IsFeatureOverriddenFromCommandLine(
              feature_name, base::FeatureList::OVERRIDE_DISABLE_FEATURE)) {
        // Build a map of the FieldTrial parameters and associate it to the
        // FieldTrial.
        base::FieldTrialParams params;
        for (const auto params_kv : kv.second.GetDict()) {
          if (params_kv.second.is_string()) {
            params[params_kv.first] = params_kv.second.GetString();
          } else {
            LOG(ERROR) << "Entry in params dict for \"" << feature_name << "\""
                       << " feature is not a string. Skipping.";
          }
        }

        // Register the params, so that they can be queried by client code.
        bool success = base::AssociateFieldTrialParams(
            feature_name, kDefaultDCSFeaturesGroup, params);
        DCHECK(success);
      }
      continue;
    }

    // Other base::Value types are not supported.
    LOG(ERROR) << "A DCS feature mapped to an unsupported value. key: "
               << feature_name << " type: " << kv.second.type();
  }

  base::FeatureList::SetInstance(std::move(feature_list));
}

bool IsFeatureEnabled(const base::Feature& feature) {
  DCHECK(base::Contains(GetFeatures(), &feature)) << feature.name;
  return base::FeatureList::IsEnabled(feature);
}

base::Value::Dict GetOverriddenFeaturesForStorage(
    const base::Value::Dict& features) {
  base::Value::Dict persistent_dict;

  // |features| maps feature names to either a boolean or a dict of params.
  for (const auto feature : features) {
    if (feature.second.is_bool()) {
      persistent_dict.Set(feature.first, feature.second.GetBool());
      continue;
    }

    if (feature.second.is_dict()) {
      const base::Value* params_dict = &feature.second;
      base::Value::Dict params;

      for (const auto [param_key, param_val] : params_dict->GetDict()) {
        if (param_val.is_bool()) {
          params.Set(param_key, param_val.GetBool() ? "true" : "false");
        } else if (param_val.is_int()) {
          params.Set(param_key, base::NumberToString(param_val.GetInt()));
        } else if (param_val.is_double()) {
          params.Set(param_key, base::NumberToString(param_val.GetDouble()));
        } else if (param_val.is_string()) {
          params.Set(param_key, param_val.GetString());
        } else {
          LOG(ERROR) << "Entry in params dict for \"" << feature.first << "\""
                     << " is not of a supported type (key: " << param_key
                     << ", type: " << param_val.type();
        }
      }
      persistent_dict.Set(feature.first, std::move(params));
      continue;
    }

    // Other base::Value types are not supported.
    LOG(ERROR) << "A DCS feature mapped to an unsupported value. key: "
               << feature.first << " type: " << feature.second.type();
  }

  return persistent_dict;
}

const std::unordered_set<int32_t>& GetDCSExperimentIds() {
  DCHECK(g_experiment_ids_initialized);
  return GetExperimentIds();
}

void ResetCastFeaturesForTesting() {
  g_experiment_ids_initialized = false;
  base::FeatureList::ClearInstanceForTesting();
  GetTestFeatures().clear();
}

void SetFeaturesForTest(std::vector<const base::Feature*> features) {
  GetTestFeatures() = std::move(features);
}

}  // namespace chromecast