blob: db880f7a69c7b44553a39857f7ea2492befa07aa [file] [log] [blame]
// 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 <stddef.h>
#include <stdint.h>
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "extensions/browser/api/device_permissions_prompt.h"
#include "extensions/shell/browser/shell_extensions_api_client.h"
#include "extensions/shell/test/shell_apitest.h"
#include "extensions/test/extension_test_message_listener.h"
#include "mojo/public/cpp/bindings/binding_set.h"
#include "mojo/public/cpp/bindings/interface_ptr_set.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
#include "services/device/public/cpp/hid/hid_report_descriptor.h"
#include "services/device/public/interfaces/constants.mojom.h"
#include "services/device/public/interfaces/hid.mojom.h"
#include "services/service_manager/public/cpp/service_context.h"
using base::ThreadTaskRunnerHandle;
using device::HidReportDescriptor;
const char* const kTestDeviceGuids[] = {"A", "B", "C", "D", "E"};
// These report descriptors define two devices with 8-byte input, output and
// feature reports. The first implements usage page 0xFF00 and has a single
// report without and ID. The second implements usage page 0xFF01 and has a
// single report with ID 1.
const uint8_t kReportDescriptor[] = {0x06, 0x00, 0xFF, 0x08, 0xA1, 0x01, 0x15,
0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95,
0x08, 0x08, 0x81, 0x02, 0x08, 0x91, 0x02,
0x08, 0xB1, 0x02, 0xC0};
const uint8_t kReportDescriptorWithIDs[] = {
0x06, 0x01, 0xFF, 0x08, 0xA1, 0x01, 0x15, 0x00, 0x26,
0xFF, 0x00, 0x85, 0x01, 0x75, 0x08, 0x95, 0x08, 0x08,
0x81, 0x02, 0x08, 0x91, 0x02, 0x08, 0xB1, 0x02, 0xC0};
namespace extensions {
class FakeHidConnectionImpl : public device::mojom::HidConnection {
public:
explicit FakeHidConnectionImpl(device::mojom::HidDeviceInfoPtr device)
: device_(std::move(device)) {}
~FakeHidConnectionImpl() override = default;
// device::mojom::HidConnection implemenation:
void Read(ReadCallback callback) override {
const char kResult[] = "This is a HID input report.";
uint8_t report_id = device_->has_report_id ? 1 : 0;
std::vector<uint8_t> buffer(kResult, kResult + sizeof(kResult) - 1);
std::move(callback).Run(true, report_id, buffer);
}
void Write(uint8_t report_id,
const std::vector<uint8_t>& buffer,
WriteCallback callback) override {
const char kExpected[] = "o-report"; // 8 bytes
if (buffer.size() != sizeof(kExpected) - 1) {
std::move(callback).Run(false);
return;
}
int expected_report_id = device_->has_report_id ? 1 : 0;
if (report_id != expected_report_id) {
std::move(callback).Run(false);
return;
}
if (memcmp(buffer.data(), kExpected, sizeof(kExpected) - 1) != 0) {
std::move(callback).Run(false);
return;
}
std::move(callback).Run(true);
}
void GetFeatureReport(uint8_t report_id,
GetFeatureReportCallback callback) override {
uint8_t expected_report_id = device_->has_report_id ? 1 : 0;
if (report_id != expected_report_id) {
std::move(callback).Run(false, base::nullopt);
return;
}
const char kResult[] = "This is a HID feature report.";
std::vector<uint8_t> buffer;
if (device_->has_report_id)
buffer.push_back(report_id);
buffer.insert(buffer.end(), kResult, kResult + sizeof(kResult) - 1);
std::move(callback).Run(true, buffer);
}
void SendFeatureReport(uint8_t report_id,
const std::vector<uint8_t>& buffer,
SendFeatureReportCallback callback) override {
const char kExpected[] = "The app is setting this HID feature report.";
if (buffer.size() != sizeof(kExpected) - 1) {
std::move(callback).Run(false);
return;
}
int expected_report_id = device_->has_report_id ? 1 : 0;
if (report_id != expected_report_id) {
std::move(callback).Run(false);
return;
}
if (memcmp(buffer.data(), kExpected, sizeof(kExpected) - 1) != 0) {
std::move(callback).Run(false);
return;
}
std::move(callback).Run(true);
}
private:
device::mojom::HidDeviceInfoPtr device_;
};
class FakeHidManager : public device::mojom::HidManager {
public:
FakeHidManager() {}
~FakeHidManager() override = default;
void Bind(const std::string& interface_name,
mojo::ScopedMessagePipeHandle handle,
const service_manager::BindSourceInfo& source_info) {
bindings_.AddBinding(this,
device::mojom::HidManagerRequest(std::move(handle)));
}
// device::mojom::HidManager implementation:
void GetDevicesAndSetClient(
device::mojom::HidManagerClientAssociatedPtrInfo client,
GetDevicesCallback callback) override {
std::vector<device::mojom::HidDeviceInfoPtr> device_list;
for (auto& map_entry : devices_)
device_list.push_back(map_entry.second->Clone());
std::move(callback).Run(std::move(device_list));
device::mojom::HidManagerClientAssociatedPtr client_ptr;
client_ptr.Bind(std::move(client));
clients_.AddPtr(std::move(client_ptr));
}
void GetDevices(GetDevicesCallback callback) override {
// Clients of HidManager in extensions only use GetDevicesAndSetClient().
NOTREACHED();
}
void Connect(const std::string& device_guid,
ConnectCallback callback) override {
if (!base::ContainsKey(devices_, device_guid)) {
std::move(callback).Run(nullptr);
return;
}
// Strong binds a instance of FakeHidConnctionImpl.
device::mojom::HidConnectionPtr client;
mojo::MakeStrongBinding(
base::MakeUnique<FakeHidConnectionImpl>(devices_[device_guid]->Clone()),
mojo::MakeRequest(&client));
std::move(callback).Run(std::move(client));
}
void AddDevice(device::mojom::HidDeviceInfoPtr device) {
std::string guid = device->guid;
devices_[guid] = std::move(device);
device::mojom::HidDeviceInfo* device_info = devices_[guid].get();
clients_.ForAllPtrs([device_info](device::mojom::HidManagerClient* client) {
client->DeviceAdded(device_info->Clone());
});
}
void RemoveDevice(const std::string& guid) {
if (base::ContainsKey(devices_, guid)) {
device::mojom::HidDeviceInfo* device_info = devices_[guid].get();
clients_.ForAllPtrs(
[device_info](device::mojom::HidManagerClient* client) {
client->DeviceRemoved(device_info->Clone());
});
devices_.erase(guid);
}
}
private:
std::map<std::string, device::mojom::HidDeviceInfoPtr> devices_;
mojo::AssociatedInterfacePtrSet<device::mojom::HidManagerClient> clients_;
mojo::BindingSet<device::mojom::HidManager> bindings_;
};
class TestDevicePermissionsPrompt
: public DevicePermissionsPrompt,
public DevicePermissionsPrompt::Prompt::Observer {
public:
explicit TestDevicePermissionsPrompt(content::WebContents* web_contents)
: DevicePermissionsPrompt(web_contents) {}
~TestDevicePermissionsPrompt() override { prompt()->SetObserver(nullptr); }
void ShowDialog() override { prompt()->SetObserver(this); }
void OnDeviceAdded(size_t index, const base::string16& device_name) override {
OnDevicesChanged();
}
void OnDeviceRemoved(size_t index,
const base::string16& device_name) override {
OnDevicesChanged();
}
private:
void OnDevicesChanged() {
if (prompt()->multiple()) {
for (size_t i = 0; i < prompt()->GetDeviceCount(); ++i) {
prompt()->GrantDevicePermission(i);
}
prompt()->Dismissed();
} else {
for (size_t i = 0; i < prompt()->GetDeviceCount(); ++i) {
// Always choose the device whose serial number is "A".
if (prompt()->GetDeviceSerialNumber(i) == base::UTF8ToUTF16("A")) {
prompt()->GrantDevicePermission(i);
prompt()->Dismissed();
return;
}
}
}
}
};
class TestExtensionsAPIClient : public ShellExtensionsAPIClient {
public:
TestExtensionsAPIClient() : ShellExtensionsAPIClient() {}
std::unique_ptr<DevicePermissionsPrompt> CreateDevicePermissionsPrompt(
content::WebContents* web_contents) const override {
return std::make_unique<TestDevicePermissionsPrompt>(web_contents);
}
};
class HidApiTest : public ShellApiTest {
public:
void SetUpOnMainThread() override {
ShellApiTest::SetUpOnMainThread();
fake_hid_manager_ = base::MakeUnique<FakeHidManager>();
// Because Device Service also runs in this process(browser process), here
// we can directly set our binder to intercept interface requests against
// it.
service_manager::ServiceContext::SetGlobalBinderForTesting(
device::mojom::kServiceName, device::mojom::HidManager::Name_,
base::Bind(&FakeHidManager::Bind,
base::Unretained(fake_hid_manager_.get())));
AddDevice(kTestDeviceGuids[0], 0x18D1, 0x58F0, false, "A");
AddDevice(kTestDeviceGuids[1], 0x18D1, 0x58F0, true, "B");
AddDevice(kTestDeviceGuids[2], 0x18D1, 0x58F1, false, "C");
}
void AddDevice(const std::string& device_guid,
int vendor_id,
int product_id,
bool report_id,
std::string serial_number) {
std::vector<uint8_t> report_descriptor;
if (report_id) {
report_descriptor.insert(
report_descriptor.begin(), kReportDescriptorWithIDs,
kReportDescriptorWithIDs + sizeof(kReportDescriptorWithIDs));
} else {
report_descriptor.insert(report_descriptor.begin(), kReportDescriptor,
kReportDescriptor + sizeof(kReportDescriptor));
}
std::vector<device::mojom::HidCollectionInfoPtr> collections;
bool has_report_id;
size_t max_input_report_size;
size_t max_output_report_size;
size_t max_feature_report_size;
HidReportDescriptor descriptor_parser(report_descriptor);
descriptor_parser.GetDetails(
&collections, &has_report_id, &max_input_report_size,
&max_output_report_size, &max_feature_report_size);
auto device = device::mojom::HidDeviceInfo::New(
device_guid, vendor_id, product_id, "Test Device", serial_number,
device::mojom::HidBusType::kHIDBusTypeUSB, report_descriptor,
std::move(collections), has_report_id, max_input_report_size,
max_output_report_size, max_feature_report_size, "");
fake_hid_manager_->AddDevice(std::move(device));
}
FakeHidManager* GetFakeHidManager() { return fake_hid_manager_.get(); }
protected:
std::unique_ptr<FakeHidManager> fake_hid_manager_;
};
IN_PROC_BROWSER_TEST_F(HidApiTest, HidApp) {
ASSERT_TRUE(RunAppTest("api_test/hid/api")) << message_;
}
IN_PROC_BROWSER_TEST_F(HidApiTest, OnDeviceAdded) {
ExtensionTestMessageListener load_listener("loaded", false);
ExtensionTestMessageListener result_listener("success", false);
result_listener.set_failure_message("failure");
ASSERT_TRUE(LoadApp("api_test/hid/add_event"));
ASSERT_TRUE(load_listener.WaitUntilSatisfied());
// Add a blocked device first so that the test will fail if a notification is
// received.
AddDevice(kTestDeviceGuids[3], 0x18D1, 0x58F1, false, "A");
AddDevice(kTestDeviceGuids[4], 0x18D1, 0x58F0, false, "A");
ASSERT_TRUE(result_listener.WaitUntilSatisfied());
EXPECT_EQ("success", result_listener.message());
}
IN_PROC_BROWSER_TEST_F(HidApiTest, OnDeviceRemoved) {
ExtensionTestMessageListener load_listener("loaded", false);
ExtensionTestMessageListener result_listener("success", false);
result_listener.set_failure_message("failure");
ASSERT_TRUE(LoadApp("api_test/hid/remove_event"));
ASSERT_TRUE(load_listener.WaitUntilSatisfied());
// Device C was not returned by chrome.hid.getDevices, the app will not get
// a notification.
GetFakeHidManager()->RemoveDevice(kTestDeviceGuids[2]);
// Device A was returned, the app will get a notification.
GetFakeHidManager()->RemoveDevice(kTestDeviceGuids[0]);
ASSERT_TRUE(result_listener.WaitUntilSatisfied());
EXPECT_EQ("success", result_listener.message());
}
IN_PROC_BROWSER_TEST_F(HidApiTest, GetUserSelectedDevices) {
ExtensionTestMessageListener open_listener("opened_device", false);
TestExtensionsAPIClient test_api_client;
ASSERT_TRUE(LoadApp("api_test/hid/get_user_selected_devices"));
ASSERT_TRUE(open_listener.WaitUntilSatisfied());
ExtensionTestMessageListener remove_listener("removed", false);
GetFakeHidManager()->RemoveDevice(kTestDeviceGuids[0]);
ASSERT_TRUE(remove_listener.WaitUntilSatisfied());
ExtensionTestMessageListener add_listener("added", false);
AddDevice(kTestDeviceGuids[0], 0x18D1, 0x58F0, true, "A");
ASSERT_TRUE(add_listener.WaitUntilSatisfied());
}
} // namespace extensions