blob: a8e836f5f8ef25f9672539a9ad94bbbaf1be1cf2 [file] [log] [blame]
// Copyright 2019 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 "chrome/browser/ui/hid/hid_chooser_controller.h"
#include <utility>
#include "base/bind.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/hid/hid_chooser_context.h"
#include "chrome/browser/hid/hid_chooser_context_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/usb/usb_blocklist.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/web_contents.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;
}
} // namespace
HidChooserController::HidChooserController(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
content::HidChooser::Callback callback)
: ChooserController(render_frame_host,
IDS_HID_CHOOSER_PROMPT_ORIGIN,
IDS_HID_CHOOSER_PROMPT_EXTENSION_NAME),
filters_(std::move(filters)),
callback_(std::move(callback)),
requesting_origin_(render_frame_host->GetLastCommittedOrigin()),
embedding_origin_(
content::WebContents::FromRenderFrameHost(render_frame_host)
->GetMainFrame()
->GetLastCommittedOrigin()) {
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 false;
}
base::string16 HidChooserController::GetNoOptionsText() const {
return l10n_util::GetStringUTF16(IDS_DEVICE_CHOOSER_NO_DEVICES_FOUND_PROMPT);
}
base::string16 HidChooserController::GetOkButtonLabel() const {
return l10n_util::GetStringUTF16(IDS_USB_DEVICE_CHOOSER_CONNECT_BUTTON_TEXT);
}
size_t HidChooserController::NumOptions() const {
return items_.size();
}
base::string16 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(requesting_origin_,
embedding_origin_, *device)) {
return false;
}
}
return true;
}
void HidChooserController::Select(const std::vector<size_t>& indices) {
// TODO(crbug.com/964041): Record metrics when an item is selected.
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());
for (auto& device : device_infos) {
chooser_context_->GrantDevicePermission(requesting_origin_,
embedding_origin_, *device);
devices.push_back(device->Clone());
}
std::move(callback_).Run(std::move(devices));
}
void HidChooserController::Cancel() {
// Called when the user presses the Cancel button in the chooser dialog.
// TODO(crbug.com/964041): Record metrics when the chooser dialog is canceled.
}
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.
// TODO(crbug.com/964041): Record metrics when the chooser dialog is closed.
}
void HidChooserController::OpenHelpCenterUrl() const {
NOTIMPLEMENTED();
}
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::find(items_.begin(), items_.end(), 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::OnHidManagerConnectionError() {
observer_.RemoveAll();
}
void HidChooserController::OnHidChooserContextShutdown() {
observer_.RemoveAll();
}
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_)
observer_.Add(chooser_context_.get());
if (view())
view()->OnOptionsInitialized();
}
bool HidChooserController::DisplayDevice(
const device::mojom::HidDeviceInfo& device) const {
// Do not pass the device to the chooser if it is on the USB blocklist.
if (UsbBlocklist::Get().IsExcluded({device.vendor_id, device.product_id, 0}))
return false;
// Do not pass the device to the chooser if it has a top-level collection with
// the FIDO usage page.
auto find_it =
std::find_if(device.collections.begin(), device.collections.end(),
[](const device::mojom::HidCollectionInfoPtr& c) {
return c->usage->usage_page == device::mojom::kPageFido;
});
if (find_it != device.collections.end())
return false;
return FilterMatchesAny(device);
}
bool HidChooserController::FilterMatchesAny(
const device::mojom::HidDeviceInfo& device) const {
if (filters_.empty())
return true;
for (const auto& filter : filters_) {
if (filter->device_ids) {
if (filter->device_ids->is_vendor()) {
if (filter->device_ids->get_vendor() != device.vendor_id)
continue;
} 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)
continue;
if (vendor_and_product->product != device.product_id)
continue;
}
}
if (filter->usage) {
if (filter->usage->is_page()) {
const uint16_t usage_page = filter->usage->get_page();
auto find_it =
std::find_if(device.collections.begin(), device.collections.end(),
[=](const device::mojom::HidCollectionInfoPtr& c) {
return usage_page == c->usage->usage_page;
});
if (find_it == device.collections.end())
continue;
} else if (filter->usage->is_usage_and_page()) {
const auto& usage_and_page = filter->usage->get_usage_and_page();
auto find_it = std::find_if(
device.collections.begin(), device.collections.end(),
[&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;
});
if (find_it == device.collections.end())
continue;
}
}
return true;
}
return false;
}
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);
DCHECK(find_it != device_map_.end());
auto& device_infos = find_it->second;
base::EraseIf(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);
base::Erase(items_, id);
return true;
}