blob: 15e6cfcede21b3b8a6a82f1198db004d0ce5a55e [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/hid/hid_chooser_controller.h"
#include <algorithm>
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/not_fatal_until.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/chooser_controller/title_util.h"
#include "chrome/browser/hid/hid_chooser_context.h"
#include "chrome/browser/hid/hid_chooser_context_factory.h"
#include "chrome/browser/hid/web_hid_histograms.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/web_contents.h"
#include "services/device/public/cpp/hid/hid_switches.h"
#include "ui/base/l10n/l10n_util.h"
namespace {
std::string PhysicalDeviceIdFromDeviceInfo(
const device::mojom::HidDeviceInfo& device) {
// A single physical device may expose multiple HID interfaces, each
// represented by a HidDeviceInfo object. When a device exposes multiple
// HID interfaces, the HidDeviceInfo objects will share a common
// |physical_device_id|. Group these devices so that a single chooser item
// is shown for each physical device. If a device's physical device ID is
// empty, use its GUID instead.
return device.physical_device_id.empty() ? device.guid
: device.physical_device_id;
}
bool FilterMatch(const blink::mojom::HidDeviceFilterPtr& filter,
const device::mojom::HidDeviceInfo& device) {
if (filter->device_ids) {
if (filter->device_ids->is_vendor()) {
if (filter->device_ids->get_vendor() != device.vendor_id) {
return false;
}
} else if (filter->device_ids->is_vendor_and_product()) {
const auto& vendor_and_product =
filter->device_ids->get_vendor_and_product();
if (vendor_and_product->vendor != device.vendor_id) {
return false;
}
if (vendor_and_product->product != device.product_id) {
return false;
}
}
}
if (filter->usage) {
if (filter->usage->is_page()) {
if (!base::Contains(device.collections, filter->usage->get_page(),
[](const device::mojom::HidCollectionInfoPtr& c) {
return c->usage->usage_page;
})) {
return false;
}
} else if (filter->usage->is_usage_and_page()) {
const auto& usage_and_page = filter->usage->get_usage_and_page();
if (std::ranges::none_of(
device.collections,
[&usage_and_page](const device::mojom::HidCollectionInfoPtr& c) {
return usage_and_page->usage_page == c->usage->usage_page &&
usage_and_page->usage == c->usage->usage;
})) {
return false;
}
}
}
return true;
}
} // namespace
HidChooserController::HidChooserController(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters,
content::HidChooser::Callback callback)
: ChooserController(
CreateChooserTitle(render_frame_host, IDS_HID_CHOOSER_PROMPT)),
filters_(std::move(filters)),
exclusion_filters_(std::move(exclusion_filters)),
callback_(std::move(callback)),
initiator_document_(render_frame_host->GetWeakDocumentPtr()),
origin_(render_frame_host->GetMainFrame()->GetLastCommittedOrigin()) {
// The use above of GetMainFrame is safe as content::HidService instances are
// not created for fenced frames.
DCHECK(!render_frame_host->IsNestedWithinFencedFrame());
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
auto* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
chooser_context_ =
HidChooserContextFactory::GetForProfile(profile)->AsWeakPtr();
DCHECK(chooser_context_);
chooser_context_->GetHidManager()->GetDevices(base::BindOnce(
&HidChooserController::OnGotDevices, weak_factory_.GetWeakPtr()));
}
HidChooserController::~HidChooserController() {
if (callback_) {
std::move(callback_).Run(std::vector<device::mojom::HidDeviceInfoPtr>());
}
}
bool HidChooserController::ShouldShowHelpButton() const {
return true;
}
std::u16string HidChooserController::GetNoOptionsText() const {
return l10n_util::GetStringUTF16(IDS_DEVICE_CHOOSER_NO_DEVICES_FOUND_PROMPT);
}
std::u16string HidChooserController::GetOkButtonLabel() const {
return l10n_util::GetStringUTF16(IDS_USB_DEVICE_CHOOSER_CONNECT_BUTTON_TEXT);
}
std::pair<std::u16string, std::u16string>
HidChooserController::GetThrobberLabelAndTooltip() const {
return {l10n_util::GetStringUTF16(IDS_HID_CHOOSER_LOADING_LABEL),
l10n_util::GetStringUTF16(IDS_HID_CHOOSER_LOADING_LABEL_TOOLTIP)};
}
size_t HidChooserController::NumOptions() const {
return items_.size();
}
std::u16string HidChooserController::GetOption(size_t index) const {
DCHECK_LT(index, items_.size());
DCHECK(base::Contains(device_map_, items_[index]));
const auto& device = *device_map_.find(items_[index])->second.front();
return HidChooserContext::DisplayNameFromDeviceInfo(device);
}
bool HidChooserController::IsPaired(size_t index) const {
DCHECK_LT(index, items_.size());
if (!chooser_context_) {
return false;
}
DCHECK(base::Contains(device_map_, items_[index]));
const auto& device_infos = device_map_.find(items_[index])->second;
DCHECK_GT(device_infos.size(), 0u);
for (const auto& device : device_infos) {
if (!chooser_context_->HasDevicePermission(origin_, *device)) {
return false;
}
}
return true;
}
void HidChooserController::Select(const std::vector<size_t>& indices) {
DCHECK_EQ(1u, indices.size());
size_t index = indices[0];
DCHECK_LT(index, items_.size());
if (!chooser_context_) {
std::move(callback_).Run({});
return;
}
DCHECK(base::Contains(device_map_, items_[index]));
auto& device_infos = device_map_.find(items_[index])->second;
DCHECK_GT(device_infos.size(), 0u);
std::vector<device::mojom::HidDeviceInfoPtr> devices;
devices.reserve(device_infos.size());
bool any_persistent_permission_granted = false;
for (auto& device : device_infos) {
chooser_context_->GrantDevicePermission(origin_, *device);
if (HidChooserContext::CanStorePersistentEntry(*device)) {
any_persistent_permission_granted = true;
}
devices.push_back(device->Clone());
}
RecordWebHidChooserClosure(
any_persistent_permission_granted
? WebHidChooserClosed::kPermissionGranted
: WebHidChooserClosed::kEphemeralPermissionGranted);
std::move(callback_).Run(std::move(devices));
}
void HidChooserController::Cancel() {
// Called when the user presses the Cancel button in the chooser dialog.
RecordWebHidChooserClosure(device_map_.empty()
? WebHidChooserClosed::kCancelledNoDevices
: WebHidChooserClosed::kCancelled);
}
void HidChooserController::Close() {
// Called when the user dismisses the chooser by clicking outside the chooser
// dialog, or when the dialog closes without the user taking action.
RecordWebHidChooserClosure(WebHidChooserClosed::kLostFocus);
}
void HidChooserController::OpenHelpCenterUrl() const {
auto* rfh = initiator_document_.AsRenderFrameHostIfValid();
auto* web_contents = rfh && rfh->IsActive()
? content::WebContents::FromRenderFrameHost(rfh)
: nullptr;
if (!web_contents) {
return;
}
web_contents->OpenURL(
content::OpenURLParams(
GURL(chrome::kChooserHidOverviewUrl), content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_AUTO_TOPLEVEL, /*is_renderer_initiated=*/false),
/*navigation_handle_callback=*/{});
}
void HidChooserController::OnDeviceAdded(
const device::mojom::HidDeviceInfo& device) {
if (!DisplayDevice(device)) {
return;
}
if (AddDeviceInfo(device) && view()) {
view()->OnOptionAdded(items_.size() - 1);
}
return;
}
void HidChooserController::OnDeviceRemoved(
const device::mojom::HidDeviceInfo& device) {
auto id = PhysicalDeviceIdFromDeviceInfo(device);
auto items_it = std::ranges::find(items_, id);
if (items_it == items_.end()) {
return;
}
size_t index = std::distance(items_.begin(), items_it);
if (RemoveDeviceInfo(device) && view()) {
view()->OnOptionRemoved(index);
}
}
void HidChooserController::OnDeviceChanged(
const device::mojom::HidDeviceInfo& device) {
bool has_chooser_item =
base::Contains(items_, PhysicalDeviceIdFromDeviceInfo(device));
if (!DisplayDevice(device)) {
if (has_chooser_item) {
OnDeviceRemoved(device);
}
return;
}
if (!has_chooser_item) {
OnDeviceAdded(device);
return;
}
// Update the item to replace the old device info with |device|.
UpdateDeviceInfo(device);
}
void HidChooserController::OnHidManagerConnectionError() {
observation_.Reset();
}
void HidChooserController::OnHidChooserContextShutdown() {
observation_.Reset();
}
void HidChooserController::OnGotDevices(
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
for (auto& device : devices) {
if (DisplayDevice(*device)) {
AddDeviceInfo(*device);
}
}
// Listen to HidChooserContext for OnDeviceAdded/Removed events after the
// enumeration.
if (chooser_context_) {
observation_.Observe(chooser_context_.get());
}
if (view()) {
view()->OnOptionsInitialized();
}
}
bool HidChooserController::DisplayDevice(
const device::mojom::HidDeviceInfo& device) const {
// Check if `device` has a top-level collection with a FIDO usage. FIDO
// devices may be displayed if the origin is privileged or the blocklist is
// disabled.
const bool has_fido_collection =
base::Contains(device.collections, device::mojom::kPageFido,
[](const auto& c) { return c->usage->usage_page; });
if (has_fido_collection) {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kDisableHidBlocklist) ||
(chooser_context_ &&
chooser_context_->IsFidoAllowedForOrigin(origin_))) {
return FilterMatchesAny(device) && !IsExcluded(device);
}
AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kInfo,
base::StringPrintf(
"Chooser dialog is not displaying a FIDO HID device: vendorId=%d, "
"productId=%d, name='%s', serial='%s'",
device.vendor_id, device.product_id, device.product_name.c_str(),
device.serial_number.c_str()));
return false;
}
if (device.is_excluded_by_blocklist) {
AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kInfo,
base::StringPrintf(
"Chooser dialog is not displaying a device excluded by "
"the HID blocklist: vendorId=%d, "
"productId=%d, name='%s', serial='%s'",
device.vendor_id, device.product_id, device.product_name.c_str(),
device.serial_number.c_str()));
return false;
}
return FilterMatchesAny(device) && !IsExcluded(device);
}
bool HidChooserController::FilterMatchesAny(
const device::mojom::HidDeviceInfo& device) const {
if (filters_.empty()) {
return true;
}
for (const auto& filter : filters_) {
if (FilterMatch(filter, device)) {
return true;
}
}
return false;
}
bool HidChooserController::IsExcluded(
const device::mojom::HidDeviceInfo& device) const {
for (const auto& exclusion_filter : exclusion_filters_) {
if (FilterMatch(exclusion_filter, device)) {
return true;
}
}
return false;
}
void HidChooserController::AddMessageToConsole(
blink::mojom::ConsoleMessageLevel level,
const std::string& message) const {
if (content::RenderFrameHost* rfh =
initiator_document_.AsRenderFrameHostIfValid()) {
rfh->AddMessageToConsole(level, message);
}
}
bool HidChooserController::AddDeviceInfo(
const device::mojom::HidDeviceInfo& device) {
auto id = PhysicalDeviceIdFromDeviceInfo(device);
auto find_it = device_map_.find(id);
if (find_it != device_map_.end()) {
find_it->second.push_back(device.Clone());
return false;
}
// A new device was connected. Append it to the end of the chooser list.
device_map_[id].push_back(device.Clone());
items_.push_back(id);
return true;
}
bool HidChooserController::RemoveDeviceInfo(
const device::mojom::HidDeviceInfo& device) {
auto id = PhysicalDeviceIdFromDeviceInfo(device);
auto find_it = device_map_.find(id);
CHECK(find_it != device_map_.end(), base::NotFatalUntil::M130);
auto& device_infos = find_it->second;
std::erase_if(device_infos,
[&device](const device::mojom::HidDeviceInfoPtr& d) {
return d->guid == device.guid;
});
if (!device_infos.empty()) {
return false;
}
// A device was disconnected. Remove it from the chooser list.
device_map_.erase(find_it);
std::erase(items_, id);
return true;
}
void HidChooserController::UpdateDeviceInfo(
const device::mojom::HidDeviceInfo& device) {
auto id = PhysicalDeviceIdFromDeviceInfo(device);
auto physical_device_it = device_map_.find(id);
CHECK(physical_device_it != device_map_.end(), base::NotFatalUntil::M130);
auto& device_infos = physical_device_it->second;
auto device_it = std::ranges::find(device_infos, device.guid,
&device::mojom::HidDeviceInfo::guid);
CHECK(device_it != device_infos.end(), base::NotFatalUntil::M130);
*device_it = device.Clone();
}