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

#include "media/blink/key_system_config_selector.h"

#include <stddef.h>
#include <utility>

#include "base/bind.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "media/base/cdm_config.h"
#include "media/base/key_systems.h"
#include "media/base/media_permission.h"
#include "media/base/mime_util.h"
#include "media/blink/webmediaplayer_util.h"
#include "third_party/WebKit/public/platform/URLConversion.h"
#include "third_party/WebKit/public/platform/WebMediaKeySystemConfiguration.h"
#include "third_party/WebKit/public/platform/WebSecurityOrigin.h"
#include "third_party/WebKit/public/platform/WebString.h"
#include "third_party/WebKit/public/platform/WebVector.h"
#include "url/gurl.h"

namespace media {

using EmeFeatureRequirement =
    blink::WebMediaKeySystemConfiguration::Requirement;

namespace {

static EmeConfigRule GetSessionTypeConfigRule(EmeSessionTypeSupport support) {
  switch (support) {
    case EmeSessionTypeSupport::INVALID:
      NOTREACHED();
      return EmeConfigRule::NOT_SUPPORTED;
    case EmeSessionTypeSupport::NOT_SUPPORTED:
      return EmeConfigRule::NOT_SUPPORTED;
    case EmeSessionTypeSupport::SUPPORTED_WITH_IDENTIFIER:
      return EmeConfigRule::IDENTIFIER_AND_PERSISTENCE_REQUIRED;
    case EmeSessionTypeSupport::SUPPORTED:
      return EmeConfigRule::PERSISTENCE_REQUIRED;
  }
  NOTREACHED();
  return EmeConfigRule::NOT_SUPPORTED;
}

static EmeConfigRule GetDistinctiveIdentifierConfigRule(
    EmeFeatureSupport support,
    EmeFeatureRequirement requirement) {
  if (support == EmeFeatureSupport::INVALID) {
    NOTREACHED();
    return EmeConfigRule::NOT_SUPPORTED;
  }

  // For NOT_ALLOWED and REQUIRED, the result is as expected. For OPTIONAL, we
  // return the most restrictive rule that is not more restrictive than for
  // NOT_ALLOWED or REQUIRED. Those values will be checked individually when
  // the option is resolved.
  //
  //                   NOT_ALLOWED    OPTIONAL       REQUIRED
  //    NOT_SUPPORTED  I_NOT_ALLOWED  I_NOT_ALLOWED  NOT_SUPPORTED
  //      REQUESTABLE  I_NOT_ALLOWED  SUPPORTED      I_REQUIRED
  //   ALWAYS_ENABLED  NOT_SUPPORTED  I_REQUIRED     I_REQUIRED
  DCHECK(support == EmeFeatureSupport::NOT_SUPPORTED ||
         support == EmeFeatureSupport::REQUESTABLE ||
         support == EmeFeatureSupport::ALWAYS_ENABLED);
  DCHECK(requirement == EmeFeatureRequirement::NotAllowed ||
         requirement == EmeFeatureRequirement::Optional ||
         requirement == EmeFeatureRequirement::Required);
  if ((support == EmeFeatureSupport::NOT_SUPPORTED &&
       requirement == EmeFeatureRequirement::Required) ||
      (support == EmeFeatureSupport::ALWAYS_ENABLED &&
       requirement == EmeFeatureRequirement::NotAllowed)) {
    return EmeConfigRule::NOT_SUPPORTED;
  }
  if (support == EmeFeatureSupport::REQUESTABLE &&
      requirement == EmeFeatureRequirement::Optional) {
    return EmeConfigRule::SUPPORTED;
  }
  if (support == EmeFeatureSupport::NOT_SUPPORTED ||
      requirement == EmeFeatureRequirement::NotAllowed) {
    return EmeConfigRule::IDENTIFIER_NOT_ALLOWED;
  }
  return EmeConfigRule::IDENTIFIER_REQUIRED;
}

static EmeConfigRule GetPersistentStateConfigRule(
    EmeFeatureSupport support,
    EmeFeatureRequirement requirement) {
  if (support == EmeFeatureSupport::INVALID) {
    NOTREACHED();
    return EmeConfigRule::NOT_SUPPORTED;
  }

  // For NOT_ALLOWED and REQUIRED, the result is as expected. For OPTIONAL, we
  // return the most restrictive rule that is not more restrictive than for
  // NOT_ALLOWED or REQUIRED. Those values will be checked individually when
  // the option is resolved.
  //
  // Note that even though a distinctive identifier can not be required for
  // persistent state, it may still be required for persistent sessions.
  //
  //                   NOT_ALLOWED    OPTIONAL       REQUIRED
  //    NOT_SUPPORTED  P_NOT_ALLOWED  P_NOT_ALLOWED  NOT_SUPPORTED
  //      REQUESTABLE  P_NOT_ALLOWED  SUPPORTED      P_REQUIRED
  //   ALWAYS_ENABLED  NOT_SUPPORTED  P_REQUIRED     P_REQUIRED
  DCHECK(support == EmeFeatureSupport::NOT_SUPPORTED ||
         support == EmeFeatureSupport::REQUESTABLE ||
         support == EmeFeatureSupport::ALWAYS_ENABLED);
  DCHECK(requirement == EmeFeatureRequirement::NotAllowed ||
         requirement == EmeFeatureRequirement::Optional ||
         requirement == EmeFeatureRequirement::Required);
  if ((support == EmeFeatureSupport::NOT_SUPPORTED &&
       requirement == EmeFeatureRequirement::Required) ||
      (support == EmeFeatureSupport::ALWAYS_ENABLED &&
       requirement == EmeFeatureRequirement::NotAllowed)) {
    return EmeConfigRule::NOT_SUPPORTED;
  }
  if (support == EmeFeatureSupport::REQUESTABLE &&
      requirement == EmeFeatureRequirement::Optional) {
    return EmeConfigRule::SUPPORTED;
  }
  if (support == EmeFeatureSupport::NOT_SUPPORTED ||
      requirement == EmeFeatureRequirement::NotAllowed) {
    return EmeConfigRule::PERSISTENCE_NOT_ALLOWED;
  }
  return EmeConfigRule::PERSISTENCE_REQUIRED;
}

}  // namespace

struct KeySystemConfigSelector::SelectionRequest {
  std::string key_system;
  blink::WebVector<blink::WebMediaKeySystemConfiguration>
      candidate_configurations;
  blink::WebSecurityOrigin security_origin;
  base::Callback<void(const blink::WebMediaKeySystemConfiguration&,
                      const CdmConfig&)> succeeded_cb;
  base::Callback<void(const blink::WebString&)> not_supported_cb;
  bool was_permission_requested = false;
  bool is_permission_granted = false;
  bool are_secure_codecs_supported = false;
};

// Accumulates configuration rules to determine if a feature (additional
// configuration rule) can be added to an accumulated configuration.
class KeySystemConfigSelector::ConfigState {
 public:
  ConfigState(bool was_permission_requested, bool is_permission_granted)
      : was_permission_requested_(was_permission_requested),
        is_permission_granted_(is_permission_granted) {}

  bool IsPermissionGranted() const { return is_permission_granted_; }

  // Permission is possible if it has not been denied.
  bool IsPermissionPossible() const {
    return is_permission_granted_ || !was_permission_requested_;
  }

  bool IsIdentifierRequired() const { return is_identifier_required_; }

  bool IsIdentifierRecommended() const { return is_identifier_recommended_; }

  bool AreHwSecureCodecsRequired() const {
    return are_hw_secure_codecs_required_;
  }

  // Checks whether a rule is compatible with all previously added rules.
  bool IsRuleSupported(EmeConfigRule rule) const {
    switch (rule) {
      case EmeConfigRule::NOT_SUPPORTED:
        return false;
      case EmeConfigRule::IDENTIFIER_NOT_ALLOWED:
        return !is_identifier_required_;
      case EmeConfigRule::IDENTIFIER_REQUIRED:
        // TODO(sandersd): Confirm if we should be refusing these rules when
        // permission has been denied (as the spec currently says).
        return !is_identifier_not_allowed_ && IsPermissionPossible();
      case EmeConfigRule::IDENTIFIER_RECOMMENDED:
        return true;
      case EmeConfigRule::PERSISTENCE_NOT_ALLOWED:
        return !is_persistence_required_;
      case EmeConfigRule::PERSISTENCE_REQUIRED:
        return !is_persistence_not_allowed_;
      case EmeConfigRule::IDENTIFIER_AND_PERSISTENCE_REQUIRED:
        return (!is_identifier_not_allowed_ && IsPermissionPossible() &&
                !is_persistence_not_allowed_);
      case EmeConfigRule::HW_SECURE_CODECS_NOT_ALLOWED:
        return !are_hw_secure_codecs_required_;
      case EmeConfigRule::HW_SECURE_CODECS_REQUIRED:
        return !are_hw_secure_codecs_not_allowed_;
      case EmeConfigRule::SUPPORTED:
        return true;
    }
    NOTREACHED();
    return false;
  }

  // Add a rule to the accumulated configuration state.
  void AddRule(EmeConfigRule rule) {
    DCHECK(IsRuleSupported(rule));
    switch (rule) {
      case EmeConfigRule::NOT_SUPPORTED:
        NOTREACHED();
        return;
      case EmeConfigRule::IDENTIFIER_NOT_ALLOWED:
        is_identifier_not_allowed_ = true;
        return;
      case EmeConfigRule::IDENTIFIER_REQUIRED:
        is_identifier_required_ = true;
        return;
      case EmeConfigRule::IDENTIFIER_RECOMMENDED:
        is_identifier_recommended_ = true;
        return;
      case EmeConfigRule::PERSISTENCE_NOT_ALLOWED:
        is_persistence_not_allowed_ = true;
        return;
      case EmeConfigRule::PERSISTENCE_REQUIRED:
        is_persistence_required_ = true;
        return;
      case EmeConfigRule::IDENTIFIER_AND_PERSISTENCE_REQUIRED:
        is_identifier_required_ = true;
        is_persistence_required_ = true;
        return;
      case EmeConfigRule::HW_SECURE_CODECS_NOT_ALLOWED:
        are_hw_secure_codecs_not_allowed_ = true;
        return;
      case EmeConfigRule::HW_SECURE_CODECS_REQUIRED:
        are_hw_secure_codecs_required_ = true;
        return;
      case EmeConfigRule::SUPPORTED:
        return;
    }
    NOTREACHED();
  }

 private:
  // Whether permission to use a distinctive identifier was requested. If set,
  // |is_permission_granted_| represents the final decision.
  // (Not changed by adding rules.)
  bool was_permission_requested_;

  // Whether permission to use a distinctive identifier has been granted.
  // (Not changed by adding rules.)
  bool is_permission_granted_;

  // Whether a rule has been added that requires or blocks a distinctive
  // identifier.
  bool is_identifier_required_ = false;
  bool is_identifier_not_allowed_ = false;

  // Whether a rule has been added that recommends a distinctive identifier.
  bool is_identifier_recommended_ = false;

  // Whether a rule has been added that requires or blocks persistent state.
  bool is_persistence_required_ = false;
  bool is_persistence_not_allowed_ = false;

  // Whether a rule has been added that requires or blocks hardware-secure
  // codecs.
  bool are_hw_secure_codecs_required_ = false;
  bool are_hw_secure_codecs_not_allowed_ = false;
};

KeySystemConfigSelector::KeySystemConfigSelector(
    const KeySystems* key_systems,
    MediaPermission* media_permission)
    : key_systems_(key_systems),
      media_permission_(media_permission),
      weak_factory_(this) {
  DCHECK(key_systems_);
  DCHECK(media_permission_);
}

KeySystemConfigSelector::~KeySystemConfigSelector() {
}

bool IsSupportedMediaFormat(const std::string& container_mime_type,
                            const std::string& codecs,
                            bool use_aes_decryptor) {
  std::vector<std::string> codec_vector;
  ParseCodecString(codecs, &codec_vector, false);
  // AesDecryptor decrypts the stream in the demuxer before it reaches the
  // decoder so check whether the media format is supported when clear.
  SupportsType support_result =
      use_aes_decryptor
          ? IsSupportedMediaFormat(container_mime_type, codec_vector)
          : IsSupportedEncryptedMediaFormat(container_mime_type, codec_vector);
  switch (support_result) {
    case IsSupported:
      return true;
    case MayBeSupported:
      // If no codecs were specified, the best possible result is
      // MayBeSupported, indicating support for the container.
      return codec_vector.empty();
    case IsNotSupported:
      return false;
  }
  NOTREACHED();
  return false;
}

// TODO(sandersd): Move contentType parsing from Blink to here so that invalid
// parameters can be rejected. http://crbug.com/417561
bool KeySystemConfigSelector::IsSupportedContentType(
    const std::string& key_system,
    EmeMediaType media_type,
    const std::string& container_mime_type,
    const std::string& codecs,
    KeySystemConfigSelector::ConfigState* config_state) {
  // From RFC6838: "Both top-level type and subtype names are case-insensitive."
  std::string container_lower = base::ToLowerASCII(container_mime_type);

  // contentTypes must provide a codec string unless the container implies a
  // particular codec. For EME, none of the currently supported containers
  // imply a codec, so |codecs| must be provided.
  if (codecs.empty()) {
    // Since the spec didn't initially require this, add an exemption for
    // some existing containers to give clients time to adapt.
    // TODO(jrummell): Remove this exemption once the number of contentTypes
    // without codecs drops low enough (UMA added in the blink code).
    // http://crbug.com/605661.
    if (container_lower != "audio/webm" && container_lower != "video/webm" &&
        container_lower != "audio/mp4" && container_lower != "video/mp4") {
      return false;
    }
  }

  // Check that |container_mime_type| and |codecs| are supported by Chrome. This
  // is done primarily to validate extended codecs, but it also ensures that the
  // CDM cannot support codecs that Chrome does not (which could complicate the
  // robustness algorithm).
  if (!IsSupportedMediaFormat(container_lower, codecs,
                              CanUseAesDecryptor(key_system))) {
    return false;
  }

  // Check that |container_mime_type| and |codecs| are supported by the CDM.
  // This check does not handle extended codecs, so extended codec information
  // is stripped (extended codec information was checked above).
  std::vector<std::string> stripped_codec_vector;
  ParseCodecString(codecs, &stripped_codec_vector, true);
  EmeConfigRule codecs_rule = key_systems_->GetContentTypeConfigRule(
      key_system, media_type, container_lower, stripped_codec_vector);
  if (!config_state->IsRuleSupported(codecs_rule))
    return false;
  config_state->AddRule(codecs_rule);

  return true;
}

bool KeySystemConfigSelector::GetSupportedCapabilities(
    const std::string& key_system,
    EmeMediaType media_type,
    const blink::WebVector<blink::WebMediaKeySystemMediaCapability>&
        requested_media_capabilities,
    KeySystemConfigSelector::ConfigState* config_state,
    std::vector<blink::WebMediaKeySystemMediaCapability>*
        supported_media_capabilities) {
  // From
  // https://w3c.github.io/encrypted-media/#get-supported-capabilities-for-media-type
  // 1. Let local accumulated capabilities be a local copy of partial
  //    configuration.
  //    (Skipped as we directly update |config_state|. This is safe because we
  //    only do so when at least one requested media capability is supported.)
  // 2. Let supported media capabilities be empty.
  DCHECK_EQ(supported_media_capabilities->size(), 0ul);
  // 3. For each value in requested media capabilities:
  for (size_t i = 0; i < requested_media_capabilities.size(); i++) {
    // 3.1. Let contentType be the value's contentType member.
    // 3.2. Let robustness be the value's robustness member.
    const blink::WebMediaKeySystemMediaCapability& capability =
        requested_media_capabilities[i];
    // 3.3. If contentType is the empty string, return null.
    if (capability.mimeType.isEmpty()) {
      DVLOG(2) << "Rejecting requested configuration because "
               << "a capability contentType was empty.";
      return false;
    }

    // 3.4-3.11. (Implemented by IsSupportedContentType().)
    ConfigState proposed_config_state = *config_state;
    if (!base::IsStringASCII(capability.mimeType) ||
        !base::IsStringASCII(capability.codecs) ||
        !IsSupportedContentType(key_system, media_type,
                                base::UTF16ToASCII(
                                    base::StringPiece16(capability.mimeType)),
                                base::UTF16ToASCII(
                                    base::StringPiece16(capability.codecs)),
                                &proposed_config_state)) {
      continue;
    }
    // 3.12. If robustness is not the empty string, run the following steps:
    if (!capability.robustness.isEmpty()) {
      // 3.12.1. If robustness is an unrecognized value or not supported by
      //         implementation, continue to the next iteration. String
      //         comparison is case-sensitive.
      if (!base::IsStringASCII(capability.robustness))
        continue;
      EmeConfigRule robustness_rule = key_systems_->GetRobustnessConfigRule(
          key_system, media_type, base::UTF16ToASCII(
              base::StringPiece16(capability.robustness)));
      if (!proposed_config_state.IsRuleSupported(robustness_rule))
        continue;
      proposed_config_state.AddRule(robustness_rule);
      // 3.12.2. Add robustness to configuration.
      //         (It's already added, we use capability as configuration.)
    }
    // 3.13. If the user agent and implementation do not support playback of
    //       encrypted media data as specified by configuration, including all
    //       media types, in combination with local accumulated capabilities,
    //       continue to the next iteration.
    //       (This is handled when adding rules to |proposed_config_state|.)
    // 3.14. Add configuration to supported media capabilities.
    supported_media_capabilities->push_back(capability);
    // 3.15. Add configuration to local accumulated capabilities.
    *config_state = proposed_config_state;
  }
  // 4. If supported media capabilities is empty, return null.
  if (supported_media_capabilities->empty()) {
    DVLOG(2) << "Rejecting requested configuration because "
             << "no capabilities were supported.";
    return false;
  }
  // 5. Return media type capabilities.
  return true;
}

KeySystemConfigSelector::ConfigurationSupport
KeySystemConfigSelector::GetSupportedConfiguration(
    const std::string& key_system,
    const blink::WebMediaKeySystemConfiguration& candidate,
    ConfigState* config_state,
    blink::WebMediaKeySystemConfiguration* accumulated_configuration) {
  // From https://w3c.github.io/encrypted-media/#get-supported-configuration
  // 1. Let accumulated configuration be empty. (Done by caller.)
  // 2. If the initDataTypes member is present in candidate configuration, run
  //    the following steps:
  if (candidate.hasInitDataTypes) {
    // 2.1. Let supported types be empty.
    std::vector<blink::WebEncryptedMediaInitDataType> supported_types;

    // 2.2. For each value in candidate configuration's initDataTypes member:
    for (size_t i = 0; i < candidate.initDataTypes.size(); i++) {
      // 2.2.1. Let initDataType be the value.
      blink::WebEncryptedMediaInitDataType init_data_type =
          candidate.initDataTypes[i];
      // 2.2.2. If the implementation supports generating requests based on
      //        initDataType, add initDataType to supported types. String
      //        comparison is case-sensitive. The empty string is never
      //        supported.
      if (key_systems_->IsSupportedInitDataType(
              key_system, ConvertToEmeInitDataType(init_data_type))) {
        supported_types.push_back(init_data_type);
      }
    }

    // 2.3. If supported types is empty, return null.
    if (supported_types.empty()) {
      DVLOG(2) << "Rejecting requested configuration because "
               << "no initDataType values were supported.";
      return CONFIGURATION_NOT_SUPPORTED;
    }

    // 2.4. Add supported types to accumulated configuration.
    accumulated_configuration->initDataTypes = supported_types;
  }

  // 3. Follow the steps for the value of candidate configuration's
  //    distinctiveIdentifier member from the following list:
  //      - "required": If the implementation does not support a persistent
  //        Distinctive Identifier in combination with accumulated
  //        configuration, return null.
  //      - "optional": Continue.
  //      - "not-allowed": If the implementation requires a Distinctive
  //        Identifier in combination with accumulated configuration, return
  //        null.
  // We also reject OPTIONAL when distinctive identifiers are ALWAYS_ENABLED and
  // permission has already been denied. This would happen anyway at step 11.
  EmeConfigRule di_rule = GetDistinctiveIdentifierConfigRule(
      key_systems_->GetDistinctiveIdentifierSupport(key_system),
      candidate.distinctiveIdentifier);
  if (!config_state->IsRuleSupported(di_rule)) {
    DVLOG(2) << "Rejecting requested configuration because "
             << "the distinctiveIdentifier requirement was not supported.";
    return CONFIGURATION_NOT_SUPPORTED;
  }
  config_state->AddRule(di_rule);

  // 4. Add the value of the candidate configuration's distinctiveIdentifier
  //    member to accumulated configuration.
  accumulated_configuration->distinctiveIdentifier =
      candidate.distinctiveIdentifier;

  // 5. Follow the steps for the value of candidate configuration's
  //    persistentState member from the following list:
  //      - "required": If the implementation does not support persisting state
  //        in combination with accumulated configuration, return null.
  //      - "optional": Continue.
  //      - "not-allowed": If the implementation requires persisting state in
  //        combination with accumulated configuration, return null.
  EmeConfigRule ps_rule = GetPersistentStateConfigRule(
      key_systems_->GetPersistentStateSupport(key_system),
      candidate.persistentState);
  if (!config_state->IsRuleSupported(ps_rule)) {
    DVLOG(2) << "Rejecting requested configuration because "
             << "the persistentState requirement was not supported.";
    return CONFIGURATION_NOT_SUPPORTED;
  }
  config_state->AddRule(ps_rule);

  // 6. Add the value of the candidate configuration's persistentState
  //    member to accumulated configuration.
  accumulated_configuration->persistentState = candidate.persistentState;

  // 7. Follow the steps for the first matching condition from the following
  //    list:
  //      - If the sessionTypes member is present in candidate configuration,
  //        let session types be candidate configuration's sessionTypes member.
  //      - Otherwise, let session types be [ "temporary" ].
  blink::WebVector<blink::WebEncryptedMediaSessionType> session_types;
  if (candidate.hasSessionTypes) {
    session_types = candidate.sessionTypes;
  } else {
    std::vector<blink::WebEncryptedMediaSessionType> temporary(1);
    temporary[0] = blink::WebEncryptedMediaSessionType::Temporary;
    session_types = temporary;
  }

  // 8. For each value in session types:
  for (size_t i = 0; i < session_types.size(); i++) {
    // 8.1. Let session type be the value.
    blink::WebEncryptedMediaSessionType session_type = session_types[i];
    // 8.2. If the implementation does not support session type in combination
    //      with accumulated configuration, return null.
    // 8.3. If session type is "persistent-license" or
    //      "persistent-release-message", follow the steps for accumulated
    //      configuration's persistentState value from the following list:
    //        - "required": Continue.
    //        - "optional": Change accumulated configuration's persistentState
    //          value to "required".
    //        - "not-allowed": Return null.
    EmeConfigRule session_type_rule = EmeConfigRule::NOT_SUPPORTED;
    switch (session_type) {
      case blink::WebEncryptedMediaSessionType::Unknown:
        DVLOG(2) << "Rejecting requested configuration because "
                 << "a required session type was not recognized.";
        return CONFIGURATION_NOT_SUPPORTED;
      case blink::WebEncryptedMediaSessionType::Temporary:
        session_type_rule = EmeConfigRule::SUPPORTED;
        break;
      case blink::WebEncryptedMediaSessionType::PersistentLicense:
        session_type_rule = GetSessionTypeConfigRule(
            key_systems_->GetPersistentLicenseSessionSupport(key_system));
        break;
      case blink::WebEncryptedMediaSessionType::PersistentReleaseMessage:
        session_type_rule = GetSessionTypeConfigRule(
            key_systems_->GetPersistentReleaseMessageSessionSupport(
                key_system));
        break;
    }
    if (!config_state->IsRuleSupported(session_type_rule)) {
      DVLOG(2) << "Rejecting requested configuration because "
               << "a required session type was not supported.";
      return CONFIGURATION_NOT_SUPPORTED;
    }
    config_state->AddRule(session_type_rule);
  }

  // 9. Add session types to accumulated configuration.
  accumulated_configuration->sessionTypes = session_types;

  // 10. If the videoCapabilities member is present in candidate configuration:
  if (candidate.hasVideoCapabilities) {
    // 10.1. Let video capabilities be the result of executing the Get Supported
    //       Capabilities for Media Type algorithm on Video, candidate
    //       configuration's videoCapabilities member, and accumulated
    //       configuration.
    // 10.2. If video capabilities is null, return null.
    std::vector<blink::WebMediaKeySystemMediaCapability> video_capabilities;
    if (!GetSupportedCapabilities(key_system, EmeMediaType::VIDEO,
                                  candidate.videoCapabilities, config_state,
                                  &video_capabilities)) {
      return CONFIGURATION_NOT_SUPPORTED;
    }

    // 10.3. Add video capabilities to accumulated configuration.
    accumulated_configuration->hasVideoCapabilities = true;
    accumulated_configuration->videoCapabilities = video_capabilities;
  }

  // 11. If the audioCapabilities member is present in candidate configuration:
  if (candidate.hasAudioCapabilities) {
    // 11.1. Let audio capabilities be the result of executing the Get Supported
    //       Capabilities for Media Type algorithm on Audio, candidate
    //       configuration's audioCapabilities member, and accumulated
    //       configuration.
    // 11.2. If audio capabilities is null, return null.
    std::vector<blink::WebMediaKeySystemMediaCapability> audio_capabilities;
    if (!GetSupportedCapabilities(key_system, EmeMediaType::AUDIO,
                                  candidate.audioCapabilities, config_state,
                                  &audio_capabilities)) {
      return CONFIGURATION_NOT_SUPPORTED;
    }

    // 11.3. Add audio capabilities to accumulated configuration.
    accumulated_configuration->hasAudioCapabilities = true;
    accumulated_configuration->audioCapabilities = audio_capabilities;
  }

  // 12. If accumulated configuration's distinctiveIdentifier value is
  //     "optional", follow the steps for the first matching condition from the
  //     following list:
  //       - If the implementation requires a Distinctive Identifier for any of
  //         the combinations in accumulated configuration, change accumulated
  //         configuration's distinctiveIdentifier value to "required".
  //       - Otherwise, change accumulated configuration's distinctiveIdentifier
  //         value to "not-allowed".
  if (accumulated_configuration->distinctiveIdentifier ==
      blink::WebMediaKeySystemConfiguration::Requirement::Optional) {
    EmeConfigRule not_allowed_rule = GetDistinctiveIdentifierConfigRule(
        key_systems_->GetDistinctiveIdentifierSupport(key_system),
        EmeFeatureRequirement::NotAllowed);
    EmeConfigRule required_rule = GetDistinctiveIdentifierConfigRule(
        key_systems_->GetDistinctiveIdentifierSupport(key_system),
        EmeFeatureRequirement::Required);
    bool not_allowed_supported =
        config_state->IsRuleSupported(not_allowed_rule);
    bool required_supported = config_state->IsRuleSupported(required_rule);
    // If a distinctive identifier is recommend and that is a possible outcome,
    // prefer that.
    if (required_supported && config_state->IsIdentifierRecommended() &&
        config_state->IsPermissionPossible()) {
      not_allowed_supported = false;
    }
    if (not_allowed_supported) {
      accumulated_configuration->distinctiveIdentifier =
          blink::WebMediaKeySystemConfiguration::Requirement::NotAllowed;
      config_state->AddRule(not_allowed_rule);
    } else if (required_supported) {
      accumulated_configuration->distinctiveIdentifier =
          blink::WebMediaKeySystemConfiguration::Requirement::Required;
      config_state->AddRule(required_rule);
    } else {
      // We should not have passed step 3.
      NOTREACHED();
      return CONFIGURATION_NOT_SUPPORTED;
    }
  }

  // 13. If accumulated configuration's persistentState value is "optional",
  //     follow the steps for the first matching condition from the following
  //     list:
  //       - If the implementation requires persisting state for any of the
  //         combinations in accumulated configuration, change accumulated
  //         configuration's persistentState value to "required".
  //       - Otherwise, change accumulated configuration's persistentState value
  //         to "not-allowed".
  if (accumulated_configuration->persistentState ==
      blink::WebMediaKeySystemConfiguration::Requirement::Optional) {
    EmeConfigRule not_allowed_rule = GetPersistentStateConfigRule(
        key_systems_->GetPersistentStateSupport(key_system),
        EmeFeatureRequirement::NotAllowed);
    EmeConfigRule required_rule = GetPersistentStateConfigRule(
        key_systems_->GetPersistentStateSupport(key_system),
        EmeFeatureRequirement::Required);
    // |distinctiveIdentifier| should not be affected after it is decided.
    DCHECK(not_allowed_rule == EmeConfigRule::NOT_SUPPORTED ||
           not_allowed_rule == EmeConfigRule::PERSISTENCE_NOT_ALLOWED);
    DCHECK(required_rule == EmeConfigRule::NOT_SUPPORTED ||
           required_rule == EmeConfigRule::PERSISTENCE_REQUIRED);
    bool not_allowed_supported =
        config_state->IsRuleSupported(not_allowed_rule);
    bool required_supported = config_state->IsRuleSupported(required_rule);
    if (not_allowed_supported) {
      accumulated_configuration->persistentState =
          blink::WebMediaKeySystemConfiguration::Requirement::NotAllowed;
      config_state->AddRule(not_allowed_rule);
    } else if (required_supported) {
      accumulated_configuration->persistentState =
          blink::WebMediaKeySystemConfiguration::Requirement::Required;
      config_state->AddRule(required_rule);
    } else {
      // We should not have passed step 5.
      NOTREACHED();
      return CONFIGURATION_NOT_SUPPORTED;
    }
  }

  // 14. If implementation in the configuration specified by the combination of
  //     the values in accumulated configuration is not supported or not allowed
  //     in the origin, return null.
  // 15. If accumulated configuration's distinctiveIdentifier value is
  //     "required", [prompt the user for consent].
  if (accumulated_configuration->distinctiveIdentifier ==
      blink::WebMediaKeySystemConfiguration::Requirement::Required) {
    // The caller is responsible for resolving what to do if permission is
    // required but has been denied (it should treat it as NOT_SUPPORTED).
    if (!config_state->IsPermissionGranted())
      return CONFIGURATION_REQUIRES_PERMISSION;
  }

  // 16. If the label member is present in candidate configuration, add the
  //     value of the candidate configuration's label member to accumulated
  //     configuration.
  accumulated_configuration->label = candidate.label;

  // 17. Return accumulated configuration.
  return CONFIGURATION_SUPPORTED;
}

void KeySystemConfigSelector::SelectConfig(
    const blink::WebString& key_system,
    const blink::WebVector<blink::WebMediaKeySystemConfiguration>&
        candidate_configurations,
    const blink::WebSecurityOrigin& security_origin,
    bool are_secure_codecs_supported,
    base::Callback<void(const blink::WebMediaKeySystemConfiguration&,
                        const CdmConfig&)> succeeded_cb,
    base::Callback<void(const blink::WebString&)> not_supported_cb) {
  // Continued from requestMediaKeySystemAccess(), step 7, from
  // https://w3c.github.io/encrypted-media/#requestmediakeysystemaccess
  //
  // 7.1. If keySystem is not one of the Key Systems supported by the user
  //      agent, reject promise with with a new DOMException whose name is
  //      NotSupportedError. String comparison is case-sensitive.
  if (!base::IsStringASCII(key_system)) {
    not_supported_cb.Run("Only ASCII keySystems are supported");
    return;
  }

  std::string key_system_ascii =
      base::UTF16ToASCII(base::StringPiece16(key_system));
  if (!key_systems_->IsSupportedKeySystem(key_system_ascii)) {
    not_supported_cb.Run("Unsupported keySystem");
    return;
  }

  // 7.2-7.4. Implemented by OnSelectConfig().
  // TODO(sandersd): This should be async, ideally not on the main thread.
  std::unique_ptr<SelectionRequest> request(new SelectionRequest());
  request->key_system = key_system_ascii;
  request->candidate_configurations = candidate_configurations;
  request->security_origin = security_origin;
  request->are_secure_codecs_supported = are_secure_codecs_supported;
  request->succeeded_cb = succeeded_cb;
  request->not_supported_cb = not_supported_cb;
  SelectConfigInternal(std::move(request));
}

void KeySystemConfigSelector::SelectConfigInternal(
    std::unique_ptr<SelectionRequest> request) {
  // Continued from requestMediaKeySystemAccess(), step 7.1, from
  // https://w3c.github.io/encrypted-media/#requestmediakeysystemaccess
  //
  // 7.2. Let implementation be the implementation of keySystem.
  //      (|key_systems_| fills this role.)
  // 7.3. For each value in supportedConfigurations:
  for (size_t i = 0; i < request->candidate_configurations.size(); i++) {
    // 7.3.1. Let candidate configuration be the value.
    // 7.3.2. Let supported configuration be the result of executing the Get
    //        Supported Configuration algorithm on implementation, candidate
    //        configuration, and origin.
    // 7.3.3. If supported configuration is not null, [initialize and return a
    //        new MediaKeySystemAccess object.]
    ConfigState config_state(request->was_permission_requested,
                             request->is_permission_granted);
    DCHECK(config_state.IsRuleSupported(
        EmeConfigRule::HW_SECURE_CODECS_NOT_ALLOWED));
    if (!request->are_secure_codecs_supported)
      config_state.AddRule(EmeConfigRule::HW_SECURE_CODECS_NOT_ALLOWED);
    blink::WebMediaKeySystemConfiguration accumulated_configuration;
    CdmConfig cdm_config;
    ConfigurationSupport support = GetSupportedConfiguration(
        request->key_system, request->candidate_configurations[i],
        &config_state, &accumulated_configuration);
    switch (support) {
      case CONFIGURATION_NOT_SUPPORTED:
        continue;
      case CONFIGURATION_REQUIRES_PERMISSION:
        if (request->was_permission_requested) {
          DVLOG(2) << "Rejecting requested configuration because "
                   << "permission was denied.";
          continue;
        }
        {
          // Note: the GURL must not be constructed inline because
          // base::Passed(&request) sets |request| to null.
          GURL security_origin(
              blink::WebStringToGURL(request->security_origin.toString()));
          media_permission_->RequestPermission(
              MediaPermission::PROTECTED_MEDIA_IDENTIFIER, security_origin,
              base::Bind(&KeySystemConfigSelector::OnPermissionResult,
                         weak_factory_.GetWeakPtr(), base::Passed(&request)));
        }
        return;
      case CONFIGURATION_SUPPORTED:
        cdm_config.allow_distinctive_identifier =
            (accumulated_configuration.distinctiveIdentifier ==
             blink::WebMediaKeySystemConfiguration::Requirement::Required);
        cdm_config.allow_persistent_state =
            (accumulated_configuration.persistentState ==
             blink::WebMediaKeySystemConfiguration::Requirement::Required);
        cdm_config.use_hw_secure_codecs =
            config_state.AreHwSecureCodecsRequired();
        request->succeeded_cb.Run(accumulated_configuration, cdm_config);
        return;
    }
  }

  // 7.4. Reject promise with a new DOMException whose name is
  //      NotSupportedError.
  request->not_supported_cb.Run(
      "None of the requested configurations were supported.");
}

void KeySystemConfigSelector::OnPermissionResult(
    std::unique_ptr<SelectionRequest> request,
    bool is_permission_granted) {
  request->was_permission_requested = true;
  request->is_permission_granted = is_permission_granted;
  SelectConfigInternal(std::move(request));
}

}  // namespace media
