chromium/chromeos/components/cdm_factory_daemon/output_protection_impl.cc

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

#include "chromeos/components/cdm_factory_daemon/output_protection_impl.h"

#include <utility>

#include "ash/shell.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "ui/display/manager/display_configurator.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/types/display_constants.h"

namespace chromeos {
namespace {
// Make sure the mapping between the Mojo enums and the Chrome enums do not
// fall out of sync.
#define VALIDATE_ENUM(mojo_type, chrome_type, name)                           \
  static_assert(                                                              \
      static_cast<uint32_t>(cdm::mojom::OutputProtection::mojo_type::name) == \
          display::chrome_type##_##name,                                      \
      #chrome_type "_" #name "value doesn't match")

VALIDATE_ENUM(ProtectionType, CONTENT_PROTECTION_METHOD, NONE);
VALIDATE_ENUM(ProtectionType, CONTENT_PROTECTION_METHOD, HDCP_TYPE_0);
VALIDATE_ENUM(ProtectionType, CONTENT_PROTECTION_METHOD, HDCP_TYPE_1);
VALIDATE_ENUM(LinkType, DISPLAY_CONNECTION_TYPE, NONE);
VALIDATE_ENUM(LinkType, DISPLAY_CONNECTION_TYPE, UNKNOWN);
VALIDATE_ENUM(LinkType, DISPLAY_CONNECTION_TYPE, INTERNAL);
VALIDATE_ENUM(LinkType, DISPLAY_CONNECTION_TYPE, VGA);
VALIDATE_ENUM(LinkType, DISPLAY_CONNECTION_TYPE, HDMI);
VALIDATE_ENUM(LinkType, DISPLAY_CONNECTION_TYPE, DVI);
VALIDATE_ENUM(LinkType, DISPLAY_CONNECTION_TYPE, DISPLAYPORT);
VALIDATE_ENUM(LinkType, DISPLAY_CONNECTION_TYPE, NETWORK);

static_assert(display::DISPLAY_CONNECTION_TYPE_LAST ==
                  display::DISPLAY_CONNECTION_TYPE_NETWORK,
              "DISPLAY_CONNECTION_TYPE_LAST value doesn't match");

constexpr uint32_t kUnprotectableConnectionTypes =
    display::DISPLAY_CONNECTION_TYPE_UNKNOWN |
    display::DISPLAY_CONNECTION_TYPE_VGA |
    display::DISPLAY_CONNECTION_TYPE_NETWORK;

constexpr uint32_t kProtectableConnectionTypes =
    display::DISPLAY_CONNECTION_TYPE_HDMI |
    display::DISPLAY_CONNECTION_TYPE_DVI |
    display::DISPLAY_CONNECTION_TYPE_DISPLAYPORT;

std::vector<int64_t> GetDisplayIdsFromSnapshots(
    const std::vector<raw_ptr<display::DisplaySnapshot, VectorExperimental>>&
        snapshots) {
  std::vector<int64_t> display_ids;
  for (display::DisplaySnapshot* ds : snapshots) {
    display_ids.push_back(ds->display_id());
  }
  return display_ids;
}

cdm::mojom::OutputProtection::ProtectionType ConvertProtection(
    uint32_t protection_mask) {
  // Only return Type 1 if that is the only type active since we want to reflect
  // the overall output security.
  if ((protection_mask & display::kContentProtectionMethodHdcpAll) ==
      display::CONTENT_PROTECTION_METHOD_HDCP_TYPE_1) {
    return cdm::mojom::OutputProtection::ProtectionType::HDCP_TYPE_1;
  } else if (protection_mask & display::CONTENT_PROTECTION_METHOD_HDCP_TYPE_0) {
    return cdm::mojom::OutputProtection::ProtectionType::HDCP_TYPE_0;
  } else {
    return cdm::mojom::OutputProtection::ProtectionType::NONE;
  }
}

class DisplaySystemDelegateImpl
    : public OutputProtectionImpl::DisplaySystemDelegate {
 public:
  DisplaySystemDelegateImpl() {
    display_configurator_ =
        ash::Shell::Get()->display_manager()->configurator();
    DCHECK(display_configurator_);
    content_protection_manager_ =
        display_configurator_->content_protection_manager();
    DCHECK(content_protection_manager_);
  }
  ~DisplaySystemDelegateImpl() override = default;

  void ApplyContentProtection(
      display::ContentProtectionManager::ClientId client_id,
      int64_t display_id,
      uint32_t protection_mask,
      display::ContentProtectionManager::ApplyContentProtectionCallback
          callback) override {
    content_protection_manager_->ApplyContentProtection(
        client_id, display_id, protection_mask, std::move(callback));
  }
  void QueryContentProtection(
      display::ContentProtectionManager::ClientId client_id,
      int64_t display_id,
      display::ContentProtectionManager::QueryContentProtectionCallback
          callback) override {
    content_protection_manager_->QueryContentProtection(client_id, display_id,
                                                        std::move(callback));
  }
  display::ContentProtectionManager::ClientId RegisterClient() override {
    return content_protection_manager_->RegisterClient();
  }
  void UnregisterClient(
      display::ContentProtectionManager::ClientId client_id) override {
    content_protection_manager_->UnregisterClient(client_id);
  }
  const std::vector<raw_ptr<display::DisplaySnapshot, VectorExperimental>>&
  cached_displays() const override {
    return display_configurator_->cached_displays();
  }

 private:
  raw_ptr<display::ContentProtectionManager>
      content_protection_manager_;  // Not owned.
  raw_ptr<display::DisplayConfigurator> display_configurator_;  // Not owned.
};

// These are reported to UMA server. Do not renumber or reuse values.
enum class OutputProtectionStatus {
  kQueried = 0,
  kNoExternalLink = 1,
  kAllExternalLinksProtected = 2,
  // Note: Only add new values immediately before this line.
  kMaxValue = kAllExternalLinksProtected,
};

void ReportOutputProtectionUMA(OutputProtectionStatus status) {
  UMA_HISTOGRAM_ENUMERATION("Media.EME.OutputProtection.PlatformCdm", status);
}

}  // namespace

// static
void OutputProtectionImpl::Create(
    mojo::PendingReceiver<cdm::mojom::OutputProtection> receiver,
    std::unique_ptr<DisplaySystemDelegate> delegate) {
  // This needs to run on the UI thread for its interactions with the display
  // system.
  if (!content::GetUIThreadTaskRunner({})->RunsTasksInCurrentSequence()) {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&OutputProtectionImpl::Create,
                                  std::move(receiver), std::move(delegate)));
    return;
  }
  // This object should destruct when the mojo connection is lost.
  mojo::MakeSelfOwnedReceiver(
      std::make_unique<OutputProtectionImpl>(std::move(delegate)),
      std::move(receiver));
}

OutputProtectionImpl::OutputProtectionImpl(
    std::unique_ptr<DisplaySystemDelegate> delegate)
    : delegate_(std::move(delegate)) {
  if (!delegate_)
    delegate_ = std::make_unique<DisplaySystemDelegateImpl>();
}

OutputProtectionImpl::~OutputProtectionImpl() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (client_id_)
    delegate_->UnregisterClient(client_id_);
}

void OutputProtectionImpl::QueryStatus(QueryStatusCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!client_id_)
    Initialize();
  if (display_id_list_.empty()) {
    std::move(callback).Run(true, display::DISPLAY_CONNECTION_TYPE_NONE,
                            ProtectionType::NONE);
    return;
  }

  ReportOutputProtectionQuery();

  // We want to copy this since we will manipulate it.
  std::vector<int64_t> remaining_displays = display_id_list_;
  int64_t curr_display_id = remaining_displays.back();
  remaining_displays.pop_back();
  delegate_->QueryContentProtection(
      client_id_, curr_display_id,
      base::BindOnce(&OutputProtectionImpl::QueryStatusCallbackAggregator,
                     weak_factory_.GetWeakPtr(), std::move(remaining_displays),
                     std::move(callback), true,
                     display::DISPLAY_CONNECTION_TYPE_NONE,
                     display::CONTENT_PROTECTION_METHOD_NONE,
                     display::CONTENT_PROTECTION_METHOD_NONE));
}

void OutputProtectionImpl::EnableProtection(ProtectionType desired_protection,
                                            EnableProtectionCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!client_id_)
    Initialize();

  if (display_id_list_.empty()) {
    std::move(callback).Run(true);
    return;
  }

  // We just pass through what the client requests.
  switch (desired_protection) {
    case ProtectionType::HDCP_TYPE_0:
      desired_protection_mask_ = display::CONTENT_PROTECTION_METHOD_HDCP_TYPE_0;
      break;
    case ProtectionType::HDCP_TYPE_1:
      desired_protection_mask_ = display::CONTENT_PROTECTION_METHOD_HDCP_TYPE_1;
      break;
    case ProtectionType::NONE:
      desired_protection_mask_ = display::CONTENT_PROTECTION_METHOD_NONE;
      break;
  }

  // We want to copy this since we will manipulate it.
  std::vector<int64_t> remaining_displays = display_id_list_;
  int64_t curr_display_id = remaining_displays.back();
  remaining_displays.pop_back();
  delegate_->ApplyContentProtection(
      client_id_, curr_display_id, desired_protection_mask_,
      base::BindOnce(&OutputProtectionImpl::EnableProtectionCallbackAggregator,
                     weak_factory_.GetWeakPtr(), std::move(remaining_displays),
                     std::move(callback), true));
}

void OutputProtectionImpl::Initialize() {
  DCHECK(!client_id_);
  // This needs to be setup on the browser thread, so wait to do it until we
  // are on that thread (i.e. don't do it in the constructor).
  client_id_ = delegate_->RegisterClient();
  DCHECK(client_id_);
  display_observer_.emplace(this);
  display_id_list_ = GetDisplayIdsFromSnapshots(delegate_->cached_displays());
}

void OutputProtectionImpl::EnableProtectionCallbackAggregator(
    std::vector<int64_t> remaining_displays,
    EnableProtectionCallback callback,
    bool aggregate_success,
    bool success) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  aggregate_success &= success;
  if (remaining_displays.empty()) {
    std::move(callback).Run(aggregate_success);
    return;
  }
  int64_t curr_display_id = remaining_displays.back();
  remaining_displays.pop_back();
  delegate_->ApplyContentProtection(
      client_id_, curr_display_id, desired_protection_mask_,
      base::BindOnce(&OutputProtectionImpl::EnableProtectionCallbackAggregator,
                     weak_factory_.GetWeakPtr(), std::move(remaining_displays),
                     std::move(callback), aggregate_success));
}

void OutputProtectionImpl::QueryStatusCallbackAggregator(
    std::vector<int64_t> remaining_displays,
    QueryStatusCallback callback,
    bool aggregate_success,
    uint32_t aggregate_link_mask,
    uint32_t aggregate_protection_mask,
    uint32_t aggregate_no_protection_mask,
    bool success,
    uint32_t link_mask,
    uint32_t protection_mask) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  aggregate_success &= success;
  aggregate_link_mask |= link_mask;
  if (link_mask & kUnprotectableConnectionTypes) {
    aggregate_no_protection_mask |= display::kContentProtectionMethodHdcpAll;
  }
  if (link_mask & kProtectableConnectionTypes) {
    aggregate_protection_mask |= protection_mask;
  }
  if (!remaining_displays.empty()) {
    int64_t curr_display_id = remaining_displays.back();
    remaining_displays.pop_back();
    delegate_->QueryContentProtection(
        client_id_, curr_display_id,
        base::BindOnce(
            &OutputProtectionImpl::QueryStatusCallbackAggregator,
            weak_factory_.GetWeakPtr(), std::move(remaining_displays),
            std::move(callback), aggregate_success, aggregate_link_mask,
            aggregate_protection_mask, aggregate_no_protection_mask));
    return;
  }

  if (aggregate_success) {
    ReportOutputProtectionQueryResult(aggregate_link_mask,
                                      aggregate_protection_mask);
  }

  aggregate_protection_mask &= ~aggregate_no_protection_mask;
  std::move(callback).Run(aggregate_success, aggregate_link_mask,
                          ConvertProtection(aggregate_protection_mask));
}

void OutputProtectionImpl::HandleDisplayChange() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  display_id_list_ = GetDisplayIdsFromSnapshots(delegate_->cached_displays());
  if (desired_protection_mask_) {
    // We always reapply content protection on display changes since we affect
    // all displays.
    EnableProtection(ConvertProtection(desired_protection_mask_),
                     base::DoNothing());
  }
}

void OutputProtectionImpl::OnDisplayAdded(const display::Display& display) {
  HandleDisplayChange();
}

void OutputProtectionImpl::OnDisplayMetricsChanged(
    const display::Display& display,
    uint32_t changed_metrics) {
  HandleDisplayChange();
}

void OutputProtectionImpl::OnDisplaysRemoved(
    const display::Displays& removed_displays) {
  HandleDisplayChange();
}

void OutputProtectionImpl::ReportOutputProtectionQuery() {
  if (uma_for_output_protection_query_reported_)
    return;

  ReportOutputProtectionUMA(OutputProtectionStatus::kQueried);
  uma_for_output_protection_query_reported_ = true;
}

void OutputProtectionImpl::ReportOutputProtectionQueryResult(
    uint32_t link_mask,
    uint32_t protection_mask) {
  DCHECK(uma_for_output_protection_query_reported_);

  if (uma_for_output_protection_positive_result_reported_)
    return;

  // Report UMAs for output protection query result.
  uint32_t external_links =
      (link_mask & ~display::DISPLAY_CONNECTION_TYPE_INTERNAL);

  if (!external_links) {
    ReportOutputProtectionUMA(OutputProtectionStatus::kNoExternalLink);
    uma_for_output_protection_positive_result_reported_ = true;
    return;
  }

  bool is_unprotectable_link_connected =
      (external_links & ~kProtectableConnectionTypes) != 0;
  bool is_hdcp_enabled_on_all_protectable_links =
      (protection_mask & desired_protection_mask_) != 0;

  if (!is_unprotectable_link_connected &&
      is_hdcp_enabled_on_all_protectable_links) {
    ReportOutputProtectionUMA(
        OutputProtectionStatus::kAllExternalLinksProtected);
    uma_for_output_protection_positive_result_reported_ = true;
    return;
  }

  // Do not report a negative result because it could be a false negative.
  // Instead, we will calculate number of negatives using the total number of
  // queries and positive results.
}

}  // namespace chromeos