// 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