Skip to content

Commit

Permalink
feat: add support for WebHID
Browse files Browse the repository at this point in the history
  • Loading branch information
jkleinsc committed Jul 26, 2021
1 parent 461db8f commit 9a6fcd6
Show file tree
Hide file tree
Showing 22 changed files with 1,403 additions and 3 deletions.
1 change: 1 addition & 0 deletions BUILD.gn
Expand Up @@ -361,6 +361,7 @@ source_set("electron_lib") {
"//ppapi/shared_impl",
"//printing/buildflags",
"//services/device/public/cpp/geolocation",
"//services/device/public/cpp/hid",
"//services/device/public/mojom",
"//services/proxy_resolver:lib",
"//services/video_capture/public/mojom:constants",
Expand Down
74 changes: 73 additions & 1 deletion docs/api/session.md
Expand Up @@ -180,6 +180,78 @@ Emitted when a hunspell dictionary file download fails. For details
on the failure you should collect a netlog and inspect the download
request.

#### Event: 'select-hid-device'

Returns:

* `event` Event
* `details` Object
* `deviceList` [HIDDevice[]](structures/hid-device.md)
* `webContents` [WebContents](web-contents.md)
* `callback` Function
* `deviceId` String

Emitted when a HID device needs to be selected when a call to
`navigator.hid.requestDevice` is made. `callback` should be called with
`deviceId` to be selected, passing an empty string to `callback` will
cancel the request. Additionally, permissioning on `navigator.hid` can
be managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler)
with the `hid` permission.

```javascript
const { app, BrowserWindow } = require('electron')

let win = null

app.whenReady().then(() => {
win = new BrowserWindow({
width: 800,
height: 600
})

win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
if (permission === 'hid') {
// Add logic here to determine if permission should be given to allow HID selection
return true
}
})

win.webContents.session.on('select-hid-device', (event, details, callback) => {
event.preventDefault()
const selectedPort = details.deviceList.find((device) => {
return device.vendorId === '9025' && device.productId === '67'
})
if (!selectedPort) {
callback('')
} else {
callback(selectedPort.deviceId)
}
})
})
```

#### Event: 'hid-device-added'

Returns:

* `event` Event
* `details` Object
* `device` [HIDDevice[]](structures/hid-device.md)
* `webContents` [WebContents](web-contents.md)

Emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired if a new HID device becomes available. For example, this event will fire when a new USB device is plugged in.

#### Event: 'hid-device-removed'

Returns:

* `event` Event
* `details` Object
* `device` [HIDDevice[]](structures/hid-device.md)
* `webContents` [WebContents](web-contents.md)

Emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired if a HID device has been removed. For example, this event will fire when a USB device is unplugged.

#### Event: 'select-serial-port' _Experimental_

Returns:
Expand Down Expand Up @@ -528,7 +600,7 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents

* `handler` Function\<Boolean> | null
* `webContents` ([WebContents](web-contents.md) | null) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. Cross origin sub frames making permission checks will pass a `null` webContents to this handler. You should use `embeddingOrigin` and `requestingOrigin` to determine what origin the owning frame and the requesting frame are on respectively.
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, or `serial`.
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, or `serial`.
* `requestingOrigin` String - The origin URL of the permission check
* `details` Object - Some properties are only available on certain permission types.
* `embeddingOrigin` String (optional) - The origin of the frame embedding the frame that made the permission check. Only set for cross-origin sub frames making permission checks.
Expand Down
8 changes: 8 additions & 0 deletions docs/api/structures/hid-device.md
@@ -0,0 +1,8 @@
# HIDDevice Object

* `deviceId` String - Unique identifier for the device.
* `name` String - Name of the device.
* `vendorId` Integer - The USB vendor ID.
* `productId` Integer - The USB product ID.
* `serialNumber` String (optional) - The USB device serial number.
* `guid` String (optional) - Unique identifier for the HID interface. A device may have multiple HID interfaces.
4 changes: 3 additions & 1 deletion electron_strings.grdp
Expand Up @@ -111,5 +111,7 @@
</message>
<message name="IDS_BADGE_UNREAD_NOTIFICATIONS" desc="The accessibility text which will be read by a screen reader when there are notifcatications">
{UNREAD_NOTIFICATIONS, plural, =1 {1 Unread Notification} other {# Unread Notifications}}
</message>
</message>
<message name="IDS_HID_CHOOSER_ITEM_WITHOUT_NAME" desc="User option displaying the device IDs for a Human Interface Device (HID) without a device name.">
Unknown Device (<ph name="DEVICE_ID">$1<ex>1234:abcd</ex></ph>) </message>
</grit-part>
1 change: 1 addition & 0 deletions filenames.auto.gni
Expand Up @@ -83,6 +83,7 @@ auto_filenames = {
"docs/api/structures/file-filter.md",
"docs/api/structures/file-path-with-headers.md",
"docs/api/structures/gpu-feature-status.md",
"docs/api/structures/hid-device.md",
"docs/api/structures/input-event.md",
"docs/api/structures/io-counters.md",
"docs/api/structures/ipc-main-event.md",
Expand Down
8 changes: 8 additions & 0 deletions filenames.gni
Expand Up @@ -383,6 +383,14 @@ filenames = {
"shell/browser/file_select_helper_mac.mm",
"shell/browser/font_defaults.cc",
"shell/browser/font_defaults.h",
"shell/browser/hid/electron_hid_delegate.cc",
"shell/browser/hid/electron_hid_delegate.h",
"shell/browser/hid/hid_chooser_context.cc",
"shell/browser/hid/hid_chooser_context.h",
"shell/browser/hid/hid_chooser_context_factory.cc",
"shell/browser/hid/hid_chooser_context_factory.h",
"shell/browser/hid/hid_chooser_controller.cc",
"shell/browser/hid/hid_chooser_controller.h",
"shell/browser/javascript_environment.cc",
"shell/browser/javascript_environment.h",
"shell/browser/lib/bluetooth_chooser.cc",
Expand Down
6 changes: 6 additions & 0 deletions shell/browser/electron_browser_client.cc
Expand Up @@ -1653,4 +1653,10 @@ device::GeolocationManager* ElectronBrowserClient::GetGeolocationManager() {
#endif
}

content::HidDelegate* ElectronBrowserClient::GetHidDelegate() {
if (!hid_delegate_)
hid_delegate_ = std::make_unique<ElectronHidDelegate>();
return hid_delegate_.get();
}

} // namespace electron
4 changes: 4 additions & 0 deletions shell/browser/electron_browser_client.h
Expand Up @@ -20,6 +20,7 @@
#include "net/ssl/client_cert_identity.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "shell/browser/bluetooth/electron_bluetooth_delegate.h"
#include "shell/browser/hid/electron_hid_delegate.h"
#include "shell/browser/serial/electron_serial_delegate.h"
#include "third_party/blink/public/mojom/badging/badging.mojom-forward.h"

Expand Down Expand Up @@ -89,6 +90,8 @@ class ElectronBrowserClient : public content::ContentBrowserClient,

content::BluetoothDelegate* GetBluetoothDelegate() override;

content::HidDelegate* GetHidDelegate() override;

device::GeolocationManager* GetGeolocationManager() override;

protected:
Expand Down Expand Up @@ -301,6 +304,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,

std::unique_ptr<ElectronSerialDelegate> serial_delegate_;
std::unique_ptr<ElectronBluetoothDelegate> bluetooth_delegate_;
std::unique_ptr<ElectronHidDelegate> hid_delegate_;

#if defined(OS_MAC)
ElectronBrowserMainParts* browser_main_parts_ = nullptr;
Expand Down
3 changes: 3 additions & 0 deletions shell/browser/electron_browser_context.cc
Expand Up @@ -95,6 +95,8 @@ std::string MakePartitionName(const std::string& input) {

} // namespace

const char kHidGrantedDevicesPref[] =
"profile.content_settings.exceptions.hid_chooser_data";
const char kSerialGrantedDevicesPref[] =
"profile.content_settings.exceptions.serial-chooser-data";

Expand Down Expand Up @@ -194,6 +196,7 @@ void ElectronBrowserContext::InitPrefs() {
registry->RegisterFilePathPref(prefs::kDownloadDefaultDirectory,
download_dir);
registry->RegisterDictionaryPref(prefs::kDevToolsFileSystemPaths);
registry->RegisterDictionaryPref(kHidGrantedDevicesPref);
registry->RegisterDictionaryPref(kSerialGrantedDevicesPref);
InspectableWebContents::RegisterPrefs(registry.get());
MediaDeviceIDSalt::RegisterPrefs(registry.get());
Expand Down
1 change: 1 addition & 0 deletions shell/browser/electron_browser_context.h
Expand Up @@ -47,6 +47,7 @@ class WebViewManager;
class ProtocolRegistry;

// Preference keys for device apis
extern const char kHidGrantedDevicesPref[];
extern const char kSerialGrantedDevicesPref[];

class ElectronBrowserContext : public content::BrowserContext {
Expand Down
157 changes: 157 additions & 0 deletions shell/browser/hid/electron_hid_delegate.cc
@@ -0,0 +1,157 @@
// Copyright (c) 2021 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#include "shell/browser/hid/electron_hid_delegate.h"

#include <string>
#include <utility>

#include "content/public/browser/web_contents.h"
#include "shell/browser/hid/hid_chooser_context.h"
#include "shell/browser/hid/hid_chooser_context_factory.h"
#include "shell/browser/hid/hid_chooser_controller.h"
#include "shell/browser/web_contents_permission_helper.h"

namespace {

electron::HidChooserContext* GetChooserContext(
content::RenderFrameHost* frame) {
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
auto* browser_context = web_contents->GetBrowserContext();
return electron::HidChooserContextFactory::GetForBrowserContext(
browser_context);
}

} // namespace

namespace electron {

ElectronHidDelegate::ElectronHidDelegate() = default;

ElectronHidDelegate::~ElectronHidDelegate() = default;

std::unique_ptr<content::HidChooser> ElectronHidDelegate::RunChooser(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
content::HidChooser::Callback callback) {
auto* chooser_context = GetChooserContext(render_frame_host);
if (!device_observation_.IsObserving())
device_observation_.Observe(chooser_context);

HidChooserController* controller = ControllerForFrame(render_frame_host);
if (controller) {
DeleteControllerForFrame(render_frame_host);
}
AddControllerForFrame(render_frame_host, std::move(filters),
std::move(callback));

// Return a nullptr because the return value isn't used for anything, eg
// there is no mechanism to cancel navigator.hid.requestDevice(). The return
// value is simply used in Chromium to cleanup the chooser UI once the serial
// service is destroyed.
return nullptr;
}

bool ElectronHidDelegate::CanRequestDevicePermission(
content::RenderFrameHost* render_frame_host) {
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
auto* permission_helper =
WebContentsPermissionHelper::FromWebContents(web_contents);
return permission_helper->CheckHIDAccessPermission(
web_contents->GetMainFrame()->GetLastCommittedOrigin());
}

bool ElectronHidDelegate::HasDevicePermission(
content::RenderFrameHost* render_frame_host,
const device::mojom::HidDeviceInfo& device) {
auto* chooser_context = GetChooserContext(render_frame_host);
const auto& origin =
render_frame_host->GetMainFrame()->GetLastCommittedOrigin();
return chooser_context->HasDevicePermission(origin, device);
}

device::mojom::HidManager* ElectronHidDelegate::GetHidManager(
content::RenderFrameHost* render_frame_host) {
auto* chooser_context = GetChooserContext(render_frame_host);
return chooser_context->GetHidManager();
}

void ElectronHidDelegate::AddObserver(
content::RenderFrameHost* render_frame_host,
Observer* observer) {
observer_list_.AddObserver(observer);
auto* chooser_context = GetChooserContext(render_frame_host);
if (!device_observation_.IsObserving())
device_observation_.Observe(chooser_context);
}

void ElectronHidDelegate::RemoveObserver(
content::RenderFrameHost* render_frame_host,
content::HidDelegate::Observer* observer) {
observer_list_.RemoveObserver(observer);
}

const device::mojom::HidDeviceInfo* ElectronHidDelegate::GetDeviceInfo(
content::RenderFrameHost* render_frame_host,
const std::string& guid) {
auto* chooser_context = GetChooserContext(render_frame_host);
return chooser_context->GetDeviceInfo(guid);
}

void ElectronHidDelegate::OnDeviceAdded(
const device::mojom::HidDeviceInfo& device_info) {
for (auto& observer : observer_list_)
observer.OnDeviceAdded(device_info);
}

void ElectronHidDelegate::OnDeviceRemoved(
const device::mojom::HidDeviceInfo& device_info) {
for (auto& observer : observer_list_)
observer.OnDeviceRemoved(device_info);
}

void ElectronHidDelegate::OnDeviceChanged(
const device::mojom::HidDeviceInfo& device_info) {
for (auto& observer : observer_list_)
observer.OnDeviceChanged(device_info);
}

void ElectronHidDelegate::OnHidManagerConnectionError() {
device_observation_.Reset();

for (auto& observer : observer_list_)
observer.OnHidManagerConnectionError();
}

void ElectronHidDelegate::OnHidChooserContextShutdown() {
device_observation_.Reset();
}

HidChooserController* ElectronHidDelegate::ControllerForFrame(
content::RenderFrameHost* render_frame_host) {
auto mapping = controller_map_.find(render_frame_host);
return mapping == controller_map_.end() ? nullptr : mapping->second.get();
}

HidChooserController* ElectronHidDelegate::AddControllerForFrame(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
content::HidChooser::Callback callback) {
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
auto controller = std::make_unique<HidChooserController>(
render_frame_host, std::move(filters), std::move(callback), web_contents,
weak_factory_.GetWeakPtr());
controller_map_.insert(
std::make_pair(render_frame_host, std::move(controller)));
return ControllerForFrame(render_frame_host);
}

void ElectronHidDelegate::DeleteControllerForFrame(
content::RenderFrameHost* render_frame_host) {
controller_map_.erase(render_frame_host);
}

} // namespace electron

0 comments on commit 9a6fcd6

Please sign in to comment.