Browse Source

feat: add support for WebHID (#31192)

* feat: add support for WebHID (#30213)

* feat: add support for WebHID

* Apply suggestions from code review

Co-authored-by: Jeremy Rose <[email protected]>

* Address review feedback

* Address review feedback

* chore: clear granted_devices on navigation

Also added test to verify devices get cleared

* fixup testing for device clear

* make sure navigator.hid.getDevices is run on correct frame

* clear granted devices on RenderFrameHost deletion/change

* manage device permissions per RenderFrameHost

This change makes sure we don't clear device permission prematurely due to child frame navigation

* Update shell/browser/api/electron_api_web_contents.cc

Co-authored-by: Jeremy Rose <[email protected]>

* apply review feedback from @zcbenz

* Match upstream ObjectMap

This change matches what ObjectPermissionContextBase uses to cache object permissions: https://source.chromium.org/chromium/chromium/src/+/main:components/permissions/object_permission_context_base.h;l=52;drc=8f95b5eab2797a3e26bba299f3b0df85bfc98bf5;bpv=1;bpt=0

The main reason for this was to resolve this crash on Win x64:
ok 2 WebContentsView doesn't crash when GCed during allocation
Received fatal exception EXCEPTION_ACCESS_VIOLATION
Backtrace:
        gin::WrappableBase::SecondWeakCallback [0x00007FF6F2AFA005+133] (o:\gin\wrappable.cc:53)
        v8::internal::GlobalHandles::InvokeSecondPassPhantomCallbacks [0x00007FF6F028F9AB+171] (o:\v8\src\handles\global-handles.cc:1400)
        v8::internal::GlobalHandles::InvokeSecondPassPhantomCallbacksFromTask [0x00007FF6F028F867+391] (o:\v8\src\handles\global-handles.cc:1387)
        node::PerIsolatePlatformData::RunForegroundTask [0x00007FF6F3B4D065+317] (o:\third_party\electron_node\src\node_platform.cc:415)
        node::PerIsolatePlatformData::FlushForegroundTasksInternal [0x00007FF6F3B4C424+776] (o:\third_party\electron_node\src\node_platform.cc:479)
        uv_run [0x00007FF6F2DDD07C+492] (o:\third_party\electron_node\deps\uv\src\win\core.c:609)
        electron::NodeBindings::UvRunOnce [0x00007FF6EEE1E036+294] (o:\electron\shell\common\node_bindings.cc:631)
        base::TaskAnnotator::RunTask [0x00007FF6F2318A19+457] (o:\base\task\common\task_annotator.cc:178)
        base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl [0x00007FF6F2E6F553+963] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:361)
        base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork [0x00007FF6F2E6EC69+137] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:266)
        base::MessagePumpForUI::DoRunLoop [0x00007FF6F235AA58+216] (o:\base\message_loop\message_pump_win.cc:221)
        base::MessagePumpWin::Run [0x00007FF6F235A01A+106] (o:\base\message_loop\message_pump_win.cc:79)
        base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run [0x00007FF6F2E702DA+682] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:470)
        base::RunLoop::Run [0x00007FF6F22F95BA+842] (o:\base\run_loop.cc:136)
        content::BrowserMainLoop::RunMainMessageLoop [0x00007FF6F14423CC+208] (o:\content\browser\browser_main_loop.cc:990)
        content::BrowserMainRunnerImpl::Run [0x00007FF6F144402F+143] (o:\content\browser\browser_main_runner_impl.cc:153)
        content::BrowserMain [0x00007FF6F143F911+257] (o:\content\browser\browser_main.cc:49)
        content::RunBrowserProcessMain [0x00007FF6EFFA7D18+112] (o:\content\app\content_main_runner_impl.cc:608)
        content::ContentMainRunnerImpl::RunBrowser [0x00007FF6EFFA8CF4+1220] (o:\content\app\content_main_runner_impl.cc:1104)
        content::ContentMainRunnerImpl::Run [0x00007FF6EFFA87C9+393] (o:\content\app\content_main_runner_impl.cc:971)
        content::RunContentProcess [0x00007FF6EFFA73BD+733] (o:\content\app\content_main.cc:394)
        content::ContentMain [0x00007FF6EFFA79E1+54] (o:\content\app\content_main.cc:422)
        wWinMain [0x00007FF6EECA1535+889] (o:\electron\shell\app\electron_main.cc:291)
        __scrt_common_main_seh [0x00007FF6F6F88482+262] (d:\A01\_work\6\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288)
        BaseThreadInitThunk [0x00007FFEC0087034+20]
        RtlUserThreadStart [0x00007FFEC1F02651+33]
✗ Electron tests failed with code 0xc0000005.

Co-authored-by: Jeremy Rose <[email protected]>
(cherry picked from commit 6aece4a83d12b6ec610994895e43560ae1aa77dc)

* fixup for 14-x-y

Co-authored-by: John Kleinschmidt <[email protected]>
trop[bot] 3 years ago
parent
commit
54e103afa5
36 changed files with 2137 additions and 4 deletions
  1. 1 0
      BUILD.gn
  2. 156 1
      docs/api/session.md
  3. 8 0
      docs/api/structures/hid-device.md
  4. 17 0
      docs/fiddles/features/web-bluetooth/index.html
  5. 30 0
      docs/fiddles/features/web-bluetooth/main.js
  6. 8 0
      docs/fiddles/features/web-bluetooth/renderer.js
  7. 21 0
      docs/fiddles/features/web-hid/index.html
  8. 50 0
      docs/fiddles/features/web-hid/main.js
  9. 19 0
      docs/fiddles/features/web-hid/renderer.js
  10. 16 0
      docs/fiddles/features/web-serial/index.html
  11. 54 0
      docs/fiddles/features/web-serial/main.js
  12. 19 0
      docs/fiddles/features/web-serial/renderer.js
  13. 99 0
      docs/tutorial/devices.md
  14. 3 1
      electron_strings.grdp
  15. 1 0
      filenames.auto.gni
  16. 8 0
      filenames.gni
  17. 14 0
      shell/browser/api/electron_api_session.cc
  18. 2 0
      shell/browser/api/electron_api_session.h
  19. 48 0
      shell/browser/api/electron_api_web_contents.cc
  20. 24 0
      shell/browser/api/electron_api_web_contents.h
  21. 6 0
      shell/browser/electron_browser_client.cc
  22. 4 0
      shell/browser/electron_browser_client.h
  23. 78 0
      shell/browser/electron_permission_manager.cc
  24. 16 0
      shell/browser/electron_permission_manager.h
  25. 159 0
      shell/browser/hid/electron_hid_delegate.cc
  26. 83 0
      shell/browser/hid/electron_hid_delegate.h
  27. 272 0
      shell/browser/hid/hid_chooser_context.cc
  28. 136 0
      shell/browser/hid/hid_chooser_context.h
  29. 55 0
      shell/browser/hid/hid_chooser_context_factory.cc
  30. 42 0
      shell/browser/hid/hid_chooser_context_factory.h
  31. 366 0
      shell/browser/hid/hid_chooser_controller.cc
  32. 126 0
      shell/browser/hid/hid_chooser_controller.h
  33. 48 0
      shell/browser/web_contents_permission_helper.cc
  34. 21 1
      shell/browser/web_contents_permission_helper.h
  35. 2 0
      shell/common/gin_converters/content_converter.cc
  36. 125 1
      spec-main/chromium-spec.ts

+ 1 - 0
BUILD.gn

@@ -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",

+ 156 - 1
docs/api/session.md

@@ -179,6 +179,96 @@ 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)
+  * `frame` [WebFrameMain](web-frame-main.md)
+* `callback` Function
+  * `deviceId` String | null (optional)
+
+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 no arguments to `callback` will
+cancel the request.  Additionally, permissioning on `navigator.hid` can
+be further managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler)
+and [ses.setDevicePermissionHandler(handler)`](#sessetdevicepermissionhandlerhandler).
+
+```javascript
+const { app, BrowserWindow } = require('electron')
+
+let win = null
+
+app.whenReady().then(() => {
+  win = new BrowserWindow()
+
+  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
+    }
+    return false
+  })
+
+  // Optionally, retrieve previously persisted devices from a persistent store
+  const grantedDevices = fetchGrantedDevices()
+
+  win.webContents.session.setDevicePermissionHandler((details) => {
+    if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') {
+      if (details.device.vendorId === 123 && details.device.productId === 345) {
+        // Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first)
+        return true
+      }
+
+      // Search through the list of devices that have previously been granted permission
+      return grantedDevices.some((grantedDevice) => {
+        return grantedDevice.vendorId === details.device.vendorId &&
+              grantedDevice.productId === details.device.productId &&
+              grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
+      })
+    }
+    return false
+  })
+
+  win.webContents.session.on('select-hid-device', (event, details, callback) => {
+    event.preventDefault()
+    const selectedDevice = details.deviceList.find((device) => {
+      return device.vendorId === '9025' && device.productId === '67'
+    })
+    callback(selectedPort?.deviceId)
+  })
+})
+```
+
+#### Event: 'hid-device-added'
+
+Returns:
+
+* `event` Event
+* `details` Object
+  * `device` [HIDDevice[]](structures/hid-device.md)
+  * `frame` [WebFrameMain](web-frame-main.md)
+
+Emitted when a new HID device becomes available. For example, when a new USB device is plugged in.
+
+This event will only be emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired.
+
+#### Event: 'hid-device-removed'
+
+Returns:
+
+* `event` Event
+* `details` Object
+  * `device` [HIDDevice[]](structures/hid-device.md)
+  * `frame` [WebFrameMain](web-frame-main.md)
+
+Emitted when a HID device has been removed.  For example, this event will fire when a USB device is unplugged.
+
+This event will only be emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired.
+
 #### Event: 'select-serial-port'
 
 Returns:
@@ -523,7 +613,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.  All cross origin sub frames making permission checks will pass a `null` webContents to this handler, while certain other permission checks such as `notifications` checks will always pass `null`.  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.
@@ -551,6 +641,71 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
 })
 ```
 
+#### `ses.setDevicePermissionHandler(handler)`
+
+* `handler` Function\<Boolean> | null
+  * `details` Object
+    * `deviceType` String - The type of device that permission is being requested on, can be `hid`.
+    * `origin` String - The origin URL of the device permission check.
+    * `device` [HIDDevice](structures/hid-device.md) - the device that permission is being requested for.
+    * `frame` [WebFrameMain](web-frame-main.md) - WebFrameMain checking the device permission.
+
+Sets the handler which can be used to respond to device permission checks for the `session`.
+Returning `true` will allow the device to be permitted and `false` will reject it.
+To clear the handler, call `setDevicePermissionHandler(null)`.
+This handler can be used to provide default permissioning to devices without first calling for permission
+to devices (eg via `navigator.hid.requestDevice`).  If this handler is not defined, the default device
+permissions as granted through device selection (eg via `navigator.hid.requestDevice`) will be used.
+Additionally, the default behavior of Electron is to store granted device permision through the lifetime
+of the corresponding WebContents.  If longer term storage is needed, a developer can store granted device
+permissions (eg when handling the `select-hid-device` event) and then read from that storage with `setDevicePermissionHandler`.
+
+```javascript
+const { app, BrowserWindow } = require('electron')
+
+let win = null
+
+app.whenReady().then(() => {
+  win = new BrowserWindow()
+
+  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
+    }
+    return false
+  })
+
+  // Optionally, retrieve previously persisted devices from a persistent store
+  const grantedDevices = fetchGrantedDevices()
+
+  win.webContents.session.setDevicePermissionHandler((details) => {
+    if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') {
+      if (details.device.vendorId === 123 && details.device.productId === 345) {
+        // Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first)
+        return true
+      }
+
+      // Search through the list of devices that have previously been granted permission
+      return grantedDevices.some((grantedDevice) => {
+        return grantedDevice.vendorId === details.device.vendorId &&
+              grantedDevice.productId === details.device.productId &&
+              grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
+      })
+    }
+    return false
+  })
+
+  win.webContents.session.on('select-hid-device', (event, details, callback) => {
+    event.preventDefault()
+    const selectedDevice = details.deviceList.find((device) => {
+      return device.vendorId === '9025' && device.productId === '67'
+    })
+    callback(selectedPort?.deviceId)
+  })
+})
+```
+
 #### `ses.clearHostResolverCache()`
 
 Returns `Promise<void>` - Resolves when the operation is complete.

+ 8 - 0
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.

+ 17 - 0
docs/fiddles/features/web-bluetooth/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+    <title>Web Bluetooth API</title>
+  </head>
+  <body>
+    <h1>Web Bluetooth API</h1>
+
+    <button id="clickme">Test Bluetooth</button>
+
+    <p>Currently selected bluetooth device: <strong id="device-name""></strong></p>
+
+    <script src="./renderer.js"></script>
+  </body>
+</html>

+ 30 - 0
docs/fiddles/features/web-bluetooth/main.js

@@ -0,0 +1,30 @@
+const {app, BrowserWindow} = require('electron')
+const path = require('path')
+
+function createWindow () {
+  const mainWindow = new BrowserWindow({
+    width: 800,
+    height: 600
+  })
+
+  mainWindow.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
+    event.preventDefault()
+    if (deviceList && deviceList.length > 0) {
+      callback(deviceList[0].deviceId)
+    } 
+  })
+
+  mainWindow.loadFile('index.html')
+}
+
+app.whenReady().then(() => {
+  createWindow()
+  
+  app.on('activate', function () {
+    if (BrowserWindow.getAllWindows().length === 0) createWindow()
+  })
+})
+
+app.on('window-all-closed', function () {
+  if (process.platform !== 'darwin') app.quit()
+})

+ 8 - 0
docs/fiddles/features/web-bluetooth/renderer.js

@@ -0,0 +1,8 @@
+async function testIt() {
+  const device = await navigator.bluetooth.requestDevice({
+    acceptAllDevices: true
+  })
+  document.getElementById('device-name').innerHTML = device.name || `ID: ${device.id}`
+}
+
+document.getElementById('clickme').addEventListener('click',testIt)

+ 21 - 0
docs/fiddles/features/web-hid/index.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+    <title>WebHID API</title>
+  </head>
+  <body>
+    <h1>WebHID API</h1>
+
+    <button id="clickme">Test WebHID</button>
+
+    <h3>HID devices automatically granted access via <i>setDevicePermissionHandler</i></h3>
+    <div id="granted-devices"></div>
+    
+    <h3>HID devices automatically granted access via <i>select-hid-device</i></h3>
+    <div id="granted-devices2"></div>
+
+    <script src="./renderer.js"></script>
+  </body>
+</html>

+ 50 - 0
docs/fiddles/features/web-hid/main.js

@@ -0,0 +1,50 @@
+const {app, BrowserWindow} = require('electron')
+const path = require('path')
+
+function createWindow () {
+  const mainWindow = new BrowserWindow({
+    width: 800,
+    height: 600
+  })
+  
+  mainWindow.webContents.session.on('select-hid-device', (event, details, callback) => {
+    event.preventDefault()
+    if (details.deviceList && details.deviceList.length > 0) {
+      callback(details.deviceList[0].deviceId)
+    }
+  })
+
+  mainWindow.webContents.session.on('hid-device-added', (event, device) => {    
+    console.log('hid-device-added FIRED WITH', device)
+  })
+
+  mainWindow.webContents.session.on('hid-device-removed', (event, device) => {    
+    console.log('hid-device-removed FIRED WITH', device)
+  })
+
+  mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
+    if (permission === 'hid' && details.securityOrigin === 'file:///') {
+      return true
+    }
+  })
+
+  mainWindow.webContents.session.setDevicePermissionHandler((details) => {
+    if (details.deviceType === 'hid' && details.origin === 'file://') {
+      return true
+    }
+  })
+  
+  mainWindow.loadFile('index.html')
+}
+
+app.whenReady().then(() => {
+  createWindow()
+  
+  app.on('activate', function () {
+    if (BrowserWindow.getAllWindows().length === 0) createWindow()
+  })
+})
+
+app.on('window-all-closed', function () {
+  if (process.platform !== 'darwin') app.quit()
+})

+ 19 - 0
docs/fiddles/features/web-hid/renderer.js

@@ -0,0 +1,19 @@
+async function testIt() {
+  const grantedDevices = await navigator.hid.getDevices()
+  let grantedDeviceList = ''
+  grantedDevices.forEach(device => {
+    grantedDeviceList += `<hr>${device.productName}</hr>`
+  })
+  document.getElementById('granted-devices').innerHTML = grantedDeviceList
+  const grantedDevices2 = await navigator.hid.requestDevice({
+    filters: []
+  })
+
+  grantedDeviceList = ''
+   grantedDevices2.forEach(device => {
+    grantedDeviceList += `<hr>${device.productName}</hr>`
+  })
+  document.getElementById('granted-devices2').innerHTML = grantedDeviceList
+}
+
+document.getElementById('clickme').addEventListener('click',testIt)

+ 16 - 0
docs/fiddles/features/web-serial/index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+    <title>Web Serial API</title>
+  <body>
+    <h1>Web Serial API</h1>
+
+    <button id="clickme">Test Web Serial API</button>
+
+    <p>Matching Arduino Uno device: <strong id="device-name""></strong></p>
+
+    <script src="./renderer.js"></script>
+  </body>
+</html>

+ 54 - 0
docs/fiddles/features/web-serial/main.js

@@ -0,0 +1,54 @@
+const {app, BrowserWindow} = require('electron')
+const path = require('path')
+
+function createWindow () {
+  const mainWindow = new BrowserWindow({
+    width: 800,
+    height: 600
+  })
+  
+  mainWindow.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
+    event.preventDefault()
+    if (portList && portList.length > 0) {
+      callback(portList[0].portId)
+    } else {
+      callback('') //Could not find any matching devices
+    }
+  })
+
+  mainWindow.webContents.session.on('serial-port-added', (event, port) => {
+    console.log('serial-port-added FIRED WITH', port)
+  })
+
+  mainWindow.webContents.session.on('serial-port-removed', (event, port) => {
+    console.log('serial-port-removed FIRED WITH', port)
+  })
+
+  mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
+    if (permission === 'serial' && details.securityOrigin === 'file:///') {
+      return true
+    }
+  })
+
+  mainWindow.webContents.session.setDevicePermissionHandler((details) => {
+    if (details.deviceType === 'serial' && details.origin === 'file://') {
+      return true
+    }
+  })
+  
+  mainWindow.loadFile('index.html')
+
+  mainWindow.webContents.openDevTools()
+}
+
+app.whenReady().then(() => {
+  createWindow()
+  
+  app.on('activate', function () {
+    if (BrowserWindow.getAllWindows().length === 0) createWindow()
+  })
+})
+
+app.on('window-all-closed', function () {
+  if (process.platform !== 'darwin') app.quit()
+})

+ 19 - 0
docs/fiddles/features/web-serial/renderer.js

@@ -0,0 +1,19 @@
+async function testIt() {
+  const filters = [
+    { usbVendorId: 0x2341, usbProductId: 0x0043 },
+    { usbVendorId: 0x2341, usbProductId: 0x0001 }
+  ];
+  try {
+    const port = await navigator.serial.requestPort({filters});
+    const portInfo = port.getInfo();
+    document.getElementById('device-name').innerHTML = `vendorId: ${portInfo.usbVendorId} | productId: ${portInfo.usbProductId} `
+  } catch (ex) {
+    if (ex.name === 'NotFoundError') {
+      document.getElementById('device-name').innerHTML = 'Device NOT found'
+    } else {
+      document.getElementById('device-name').innerHTML = ex
+    }
+  }
+}
+
+document.getElementById('clickme').addEventListener('click',testIt)

+ 99 - 0
docs/tutorial/devices.md

@@ -0,0 +1,99 @@
+# Device Access
+
+Like Chromium based browsers, Electron provides access to device hardware
+through web APIs.  For the most part these APIs work like they do in a browser,
+but there are some differences that need to be taken into account.  The primary
+difference between Electron and browsers is what happens when device access is
+requested.  In a browser, users are presented with a popup where they can grant
+access to an individual device.  In Electron APIs are provided which can be
+used by a developer to either automatically pick a device or prompt users to
+pick a device via a developer created interface.
+
+## Web Bluetooth API
+
+The [Web Bluetooth API](https://web.dev/bluetooth/) can be used to communicate
+with bluetooth devices. In order to use this API in Electron, developers will
+need to handle the [`select-bluetooth-device` event on the webContents](../api/web-contents.md#event-select-bluetooth-device)
+associated with the device request.
+
+### Example
+
+This example demonstrates an Electron application that automatically selects
+the first available bluetooth device when the `Test Bluetooth` button is
+clicked.
+
+```javascript fiddle='docs/fiddles/features/web-bluetooth'
+
+```
+
+## WebHID API
+
+The [WebHID API](https://web.dev/hid/) can be used to access HID devices such
+as keyboards and gamepads.  Electron provides several APIs for working with
+the WebHID API:
+
+* The [`select-hid-device` event on the Session](../api/session.md#event-select-hid-device)
+  can be used to select a HID device when a call to
+  `navigator.hid.requestDevice` is made.  Additionally the [`hid-device-added`](../api/session.md#event-hid-device-added)
+  and [`hid-device-removed`](../api/session.md#event-hid-device-removed) events
+  on the Session can be used to handle devices being plugged in or unplugged during the
+  `navigator.hid.requestDevice` process.
+* [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
+  can be used to provide default permissioning to devices without first calling
+  for permission to devices via `navigator.hid.requestDevice`.  Additionally,
+  the default behavior of Electron is to store granted device permision through
+  the lifetime of the corresponding WebContents.  If longer term storage is
+  needed, a developer can store granted device permissions (eg when handling
+  the `select-hid-device` event) and then read from that storage with
+  `setDevicePermissionHandler`.
+* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
+  can be used to disable HID access for specific origins.
+
+### Blocklist
+
+By default Electron employs the same [blocklist](https://github.com/WICG/webhid/blob/main/blocklist.txt)
+used by Chromium.  If you wish to override this behavior, you can do so by
+setting the `disable-hid-blocklist` flag:
+
+```javascript
+app.commandLine.appendSwitch('disable-hid-blocklist')
+```
+
+### Example
+
+This example demonstrates an Electron application that automatically selects
+HID devices through [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
+and through [`select-hid-device` event on the Session](../api/session.md#event-select-hid-device)
+when the `Test WebHID` button is clicked.
+
+```javascript fiddle='docs/fiddles/features/web-hid'
+
+```
+
+## Web Serial API
+
+The [Web Serial API](https://web.dev/serial/) can be used to access serial
+devices that are connected via serial port, USB, or Bluetooth.  In order to use
+this API in Electron, developers will need to handle the
+[`select-serial-port` event on the Session](../api/session.md#event-select-serial-port)
+associated with the serial port request.
+
+There are several additional APIs for working with the Web Serial API:
+
+* The [`serial-port-added`](../api/session.md#event-serial-port-added)
+  and [`serial-port-removed`](../api/session.md#event-serial-port-removed) events
+  on the Session can be used to handle devices being plugged in or unplugged during the
+  `navigator.serial.requestPort` process.
+* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
+  can be used to disable serial access for specific origins.
+
+### Example
+
+This example demonstrates an Electron application that automatically selects
+the first available Arduino Uno serial device (if connected) through
+[`select-serial-port` event on the Session](../api/session.md#event-select-serial-port)
+when the `Test Web Serial` button is clicked.
+
+```javascript fiddle='docs/fiddles/features/web-serial'
+
+```

+ 3 - 1
electron_strings.grdp

@@ -125,5 +125,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 - 0
filenames.auto.gni

@@ -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",

+ 8 - 0
filenames.gni

@@ -389,6 +389,14 @@ filenames = {
     "shell/browser/font/electron_font_access_delegate.h",
     "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",

+ 14 - 0
shell/browser/api/electron_api_session.cc

@@ -644,6 +644,18 @@ void Session::SetPermissionCheckHandler(v8::Local<v8::Value> val,
   permission_manager->SetPermissionCheckHandler(handler);
 }
 
+void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
+                                         gin::Arguments* args) {
+  ElectronPermissionManager::DeviceCheckHandler handler;
+  if (!(val->IsNull() || gin::ConvertFromV8(args->isolate(), val, &handler))) {
+    args->ThrowTypeError("Must pass null or function");
+    return;
+  }
+  auto* permission_manager = static_cast<ElectronPermissionManager*>(
+      browser_context()->GetPermissionControllerDelegate());
+  permission_manager->SetDevicePermissionHandler(handler);
+}
+
 v8::Local<v8::Promise> Session::ClearHostResolverCache(gin::Arguments* args) {
   v8::Isolate* isolate = args->isolate();
   gin_helper::Promise<void> promise(isolate);
@@ -1147,6 +1159,8 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
                  &Session::SetPermissionRequestHandler)
       .SetMethod("setPermissionCheckHandler",
                  &Session::SetPermissionCheckHandler)
+      .SetMethod("setDevicePermissionHandler",
+                 &Session::SetDevicePermissionHandler)
       .SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)
       .SetMethod("clearAuthCache", &Session::ClearAuthCache)
       .SetMethod("allowNTLMCredentialsForDomains",

+ 2 - 0
shell/browser/api/electron_api_session.h

@@ -104,6 +104,8 @@ class Session : public gin::Wrappable<Session>,
                                    gin::Arguments* args);
   void SetPermissionCheckHandler(v8::Local<v8::Value> val,
                                  gin::Arguments* args);
+  void SetDevicePermissionHandler(v8::Local<v8::Value> val,
+                                  gin::Arguments* args);
   v8::Local<v8::Promise> ClearHostResolverCache(gin::Arguments* args);
   v8::Local<v8::Promise> ClearAuthCache();
   void AllowNTLMCredentialsForDomains(const std::string& domains);

+ 48 - 0
shell/browser/api/electron_api_web_contents.cc

@@ -914,6 +914,12 @@ void WebContents::InitWithWebContents(
 }
 
 WebContents::~WebContents() {
+  // clear out objects that have been granted permissions so that when
+  // WebContents::RenderFrameDeleted is called as a result of WebContents
+  // destruction it doesn't try to clear out a granted_devices_
+  // on a destructed object.
+  granted_devices_.clear();
+
   if (!inspectable_web_contents_) {
     WebContentsDestroyed();
     return;
@@ -1426,6 +1432,12 @@ void WebContents::RenderFrameDeleted(
   // - Cross-origin navigation creates a new RFH in a separate process which
   //   is swapped by content::RenderFrameHostManager.
   //
+
+  // clear out objects that have been granted permissions
+  if (!granted_devices_.empty()) {
+    granted_devices_.erase(render_frame_host->GetFrameTreeNodeId());
+  }
+
   // WebFrameMain::FromRenderFrameHost(rfh) will use the RFH's FrameTreeNode ID
   // to find an existing instance of WebFrameMain. During a cross-origin
   // navigation, the deleted RFH will be the old host which was swapped out. In
@@ -3249,6 +3261,42 @@ v8::Local<v8::Promise> WebContents::TakeHeapSnapshot(
   return handle;
 }
 
+void WebContents::GrantDevicePermission(
+    const url::Origin& origin,
+    const base::Value* device,
+    content::PermissionType permissionType,
+    content::RenderFrameHost* render_frame_host) {
+  granted_devices_[render_frame_host->GetFrameTreeNodeId()][permissionType]
+                  [origin]
+                      .push_back(
+                          std::make_unique<base::Value>(device->Clone()));
+}
+
+std::vector<base::Value> WebContents::GetGrantedDevices(
+    const url::Origin& origin,
+    content::PermissionType permissionType,
+    content::RenderFrameHost* render_frame_host) {
+  const auto& devices_for_frame_host_it =
+      granted_devices_.find(render_frame_host->GetFrameTreeNodeId());
+  if (devices_for_frame_host_it == granted_devices_.end())
+    return {};
+
+  const auto& current_devices_it =
+      devices_for_frame_host_it->second.find(permissionType);
+  if (current_devices_it == devices_for_frame_host_it->second.end())
+    return {};
+
+  const auto& origin_devices_it = current_devices_it->second.find(origin);
+  if (origin_devices_it == current_devices_it->second.end())
+    return {};
+
+  std::vector<base::Value> results;
+  for (const auto& object : origin_devices_it->second)
+    results.push_back(object->Clone());
+
+  return results;
+}
+
 void WebContents::UpdatePreferredSize(content::WebContents* web_contents,
                                       const gfx::Size& pref_size) {
   Emit("preferred-size-changed", pref_size);

+ 24 - 0
shell/browser/api/electron_api_web_contents.h

@@ -20,6 +20,7 @@
 #include "content/common/frame.mojom.h"
 #include "content/public/browser/devtools_agent_host.h"
 #include "content/public/browser/keyboard_event_processing_result.h"
+#include "content/public/browser/permission_type.h"
 #include "content/public/browser/render_widget_host.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_contents_delegate.h"
@@ -91,6 +92,11 @@ class OffScreenWebContentsView;
 
 namespace api {
 
+using DevicePermissionMap = std::map<
+    int,
+    std::map<content::PermissionType,
+             std::map<url::Origin, std::vector<std::unique_ptr<base::Value>>>>>;
+
 // Wrapper around the content::WebContents.
 class WebContents : public gin::Wrappable<WebContents>,
                     public gin_helper::EventEmitterMixin<WebContents>,
@@ -421,6 +427,21 @@ class WebContents : public gin::Wrappable<WebContents>,
   void DoGetZoomLevel(
       electron::mojom::ElectronBrowser::DoGetZoomLevelCallback callback);
 
+  // Grants |origin| access to |device|.
+  // To be used in place of ObjectPermissionContextBase::GrantObjectPermission.
+  void GrantDevicePermission(const url::Origin& origin,
+                             const base::Value* device,
+                             content::PermissionType permissionType,
+                             content::RenderFrameHost* render_frame_host);
+
+  // Returns the list of devices that |origin| has been granted permission to
+  // access. To be used in place of
+  // ObjectPermissionContextBase::GetGrantedObjects.
+  std::vector<base::Value> GetGrantedDevices(
+      const url::Origin& origin,
+      content::PermissionType permissionType,
+      content::RenderFrameHost* render_frame_host);
+
  private:
   // Does not manage lifetime of |web_contents|.
   WebContents(v8::Isolate* isolate, content::WebContents* web_contents);
@@ -781,6 +802,9 @@ class WebContents : public gin::Wrappable<WebContents>,
 
   service_manager::BinderRegistryWithArgs<content::RenderFrameHost*> registry_;
 
+  // In-memory cache that holds objects that have been granted permissions.
+  DevicePermissionMap granted_devices_;
+
   base::WeakPtrFactory<WebContents> weak_factory_{this};
 
   DISALLOW_COPY_AND_ASSIGN(WebContents);

+ 6 - 0
shell/browser/electron_browser_client.cc

@@ -1636,4 +1636,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 - 0
shell/browser/electron_browser_client.h

@@ -21,6 +21,7 @@
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "shell/browser/bluetooth/electron_bluetooth_delegate.h"
 #include "shell/browser/font/electron_font_access_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"
 
@@ -92,6 +93,8 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
 
   content::BluetoothDelegate* GetBluetoothDelegate() override;
 
+  content::HidDelegate* GetHidDelegate() override;
+
   device::GeolocationManager* GetGeolocationManager() override;
 
  protected:
@@ -305,6 +308,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
   std::unique_ptr<ElectronSerialDelegate> serial_delegate_;
   std::unique_ptr<ElectronBluetoothDelegate> bluetooth_delegate_;
   std::unique_ptr<ElectronFontAccessDelegate> font_access_delegate_;
+  std::unique_ptr<ElectronHidDelegate> hid_delegate_;
 
 #if defined(OS_MAC)
   ElectronBrowserMainParts* browser_main_parts_ = nullptr;

+ 78 - 0
shell/browser/electron_permission_manager.cc

@@ -16,9 +16,17 @@
 #include "content/public/browser/render_process_host.h"
 #include "content/public/browser/render_view_host.h"
 #include "content/public/browser/web_contents.h"
+#include "gin/data_object_builder.h"
+#include "shell/browser/api/electron_api_web_contents.h"
 #include "shell/browser/electron_browser_client.h"
 #include "shell/browser/electron_browser_main_parts.h"
+#include "shell/browser/hid/hid_chooser_context.h"
+#include "shell/browser/web_contents_permission_helper.h"
 #include "shell/browser/web_contents_preferences.h"
+#include "shell/common/gin_converters/content_converter.h"
+#include "shell/common/gin_converters/frame_converter.h"
+#include "shell/common/gin_converters/value_converter.h"
+#include "shell/common/gin_helper/event_emitter_caller.h"
 
 namespace electron {
 
@@ -116,6 +124,11 @@ void ElectronPermissionManager::SetPermissionCheckHandler(
   check_handler_ = handler;
 }
 
+void ElectronPermissionManager::SetDevicePermissionHandler(
+    const DeviceCheckHandler& handler) {
+  device_permission_handler_ = handler;
+}
+
 void ElectronPermissionManager::RequestPermission(
     content::PermissionType permission,
     content::RenderFrameHost* render_frame_host,
@@ -281,6 +294,71 @@ bool ElectronPermissionManager::CheckPermissionWithDetails(
                             mutable_details);
 }
 
+bool ElectronPermissionManager::CheckDevicePermission(
+    content::PermissionType permission,
+    const url::Origin& origin,
+    const base::Value* device,
+    content::RenderFrameHost* render_frame_host) const {
+  auto* web_contents =
+      content::WebContents::FromRenderFrameHost(render_frame_host);
+  api::WebContents* api_web_contents = api::WebContents::From(web_contents);
+  if (device_permission_handler_.is_null()) {
+    if (api_web_contents) {
+      std::vector<base::Value> granted_devices =
+          api_web_contents->GetGrantedDevices(origin, permission,
+                                              render_frame_host);
+
+      for (const auto& granted_device : granted_devices) {
+        if (permission ==
+            static_cast<content::PermissionType>(
+                WebContentsPermissionHelper::PermissionType::HID)) {
+          if (device->FindIntKey(kHidVendorIdKey) !=
+                  granted_device.FindIntKey(kHidVendorIdKey) ||
+              device->FindIntKey(kHidProductIdKey) !=
+                  granted_device.FindIntKey(kHidProductIdKey)) {
+            continue;
+          }
+
+          const auto* serial_number =
+              granted_device.FindStringKey(kHidSerialNumberKey);
+          const auto* device_serial_number =
+              device->FindStringKey(kHidSerialNumberKey);
+
+          if (serial_number && device_serial_number &&
+              *device_serial_number == *serial_number)
+            return true;
+        }
+      }
+    }
+    return false;
+  } else {
+    v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+    v8::HandleScope scope(isolate);
+    v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
+                                        .Set("deviceType", permission)
+                                        .Set("origin", origin.Serialize())
+                                        .Set("device", device->Clone())
+                                        .Set("frame", render_frame_host)
+                                        .Build();
+    return device_permission_handler_.Run(details);
+  }
+}
+
+void ElectronPermissionManager::GrantDevicePermission(
+    content::PermissionType permission,
+    const url::Origin& origin,
+    const base::Value* device,
+    content::RenderFrameHost* render_frame_host) const {
+  if (device_permission_handler_.is_null()) {
+    auto* web_contents =
+        content::WebContents::FromRenderFrameHost(render_frame_host);
+    api::WebContents* api_web_contents = api::WebContents::From(web_contents);
+    if (api_web_contents)
+      api_web_contents->GrantDevicePermission(origin, device, permission,
+                                              render_frame_host);
+  }
+}
+
 blink::mojom::PermissionStatus
 ElectronPermissionManager::GetPermissionStatusForFrame(
     content::PermissionType permission,

+ 16 - 0
shell/browser/electron_permission_manager.h

@@ -13,6 +13,7 @@
 #include "base/containers/id_map.h"
 #include "base/values.h"
 #include "content/public/browser/permission_controller_delegate.h"
+#include "gin/dictionary.h"
 
 namespace content {
 class WebContents;
@@ -39,9 +40,13 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
                                    const GURL& requesting_origin,
                                    const base::Value&)>;
 
+  using DeviceCheckHandler =
+      base::RepeatingCallback<bool(const v8::Local<v8::Object>&)>;
+
   // Handler to dispatch permission requests in JS.
   void SetPermissionRequestHandler(const RequestHandler& handler);
   void SetPermissionCheckHandler(const CheckHandler& handler);
+  void SetDevicePermissionHandler(const DeviceCheckHandler& handler);
 
   // content::PermissionControllerDelegate:
   void RequestPermission(content::PermissionType permission,
@@ -78,6 +83,16 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
                                   const GURL& requesting_origin,
                                   const base::DictionaryValue* details) const;
 
+  bool CheckDevicePermission(content::PermissionType permission,
+                             const url::Origin& origin,
+                             const base::Value* object,
+                             content::RenderFrameHost* render_frame_host) const;
+
+  void GrantDevicePermission(content::PermissionType permission,
+                             const url::Origin& origin,
+                             const base::Value* object,
+                             content::RenderFrameHost* render_frame_host) const;
+
  protected:
   void OnPermissionResponse(int request_id,
                             int permission_id,
@@ -105,6 +120,7 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
 
   RequestHandler request_handler_;
   CheckHandler check_handler_;
+  DeviceCheckHandler device_permission_handler_;
 
   PendingRequestsMap pending_requests_;
 

+ 159 - 0
shell/browser/hid/electron_hid_delegate.cc

@@ -0,0 +1,159 @@
+// 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) {
+  electron::HidChooserContext* 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,
+                                              render_frame_host);
+}
+
+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

+ 83 - 0
shell/browser/hid/electron_hid_delegate.h

@@ -0,0 +1,83 @@
+// 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.
+
+#ifndef SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_
+#define SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "base/observer_list.h"
+#include "base/scoped_observation.h"
+#include "content/public/browser/hid_delegate.h"
+#include "shell/browser/hid/hid_chooser_context.h"
+
+namespace electron {
+
+class HidChooserController;
+
+class ElectronHidDelegate : public content::HidDelegate,
+                            public HidChooserContext::DeviceObserver {
+ public:
+  ElectronHidDelegate();
+  ElectronHidDelegate(ElectronHidDelegate&) = delete;
+  ElectronHidDelegate& operator=(ElectronHidDelegate&) = delete;
+  ~ElectronHidDelegate() override;
+
+  // content::HidDelegate:
+  std::unique_ptr<content::HidChooser> RunChooser(
+      content::RenderFrameHost* render_frame_host,
+      std::vector<blink::mojom::HidDeviceFilterPtr> filters,
+      content::HidChooser::Callback callback) override;
+  bool CanRequestDevicePermission(
+      content::RenderFrameHost* render_frame_host) override;
+  bool HasDevicePermission(content::RenderFrameHost* render_frame_host,
+                           const device::mojom::HidDeviceInfo& device) override;
+  device::mojom::HidManager* GetHidManager(
+      content::RenderFrameHost* render_frame_host) override;
+  void AddObserver(content::RenderFrameHost* render_frame_host,
+                   content::HidDelegate::Observer* observer) override;
+  void RemoveObserver(content::RenderFrameHost* render_frame_host,
+                      content::HidDelegate::Observer* observer) override;
+  const device::mojom::HidDeviceInfo* GetDeviceInfo(
+      content::RenderFrameHost* render_frame_host,
+      const std::string& guid) override;
+
+  // HidChooserContext::DeviceObserver:
+  void OnDeviceAdded(const device::mojom::HidDeviceInfo&) override;
+  void OnDeviceRemoved(const device::mojom::HidDeviceInfo&) override;
+  void OnDeviceChanged(const device::mojom::HidDeviceInfo&) override;
+  void OnHidManagerConnectionError() override;
+  void OnHidChooserContextShutdown() override;
+
+  void DeleteControllerForFrame(content::RenderFrameHost* render_frame_host);
+
+ private:
+  HidChooserController* ControllerForFrame(
+      content::RenderFrameHost* render_frame_host);
+
+  HidChooserController* AddControllerForFrame(
+      content::RenderFrameHost* render_frame_host,
+      std::vector<blink::mojom::HidDeviceFilterPtr> filters,
+      content::HidChooser::Callback callback);
+
+  base::ScopedObservation<HidChooserContext,
+                          HidChooserContext::DeviceObserver,
+                          &HidChooserContext::AddDeviceObserver,
+                          &HidChooserContext::RemoveDeviceObserver>
+      device_observation_{this};
+  base::ObserverList<content::HidDelegate::Observer> observer_list_;
+
+  std::unordered_map<content::RenderFrameHost*,
+                     std::unique_ptr<HidChooserController>>
+      controller_map_;
+
+  base::WeakPtrFactory<ElectronHidDelegate> weak_factory_{this};
+};
+
+}  // namespace electron
+
+#endif  // SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_

+ 272 - 0
shell/browser/hid/hid_chooser_context.cc

@@ -0,0 +1,272 @@
+// 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 "shell/browser/hid/hid_chooser_context.h"
+
+#include <utility>
+
+#include "base/command_line.h"
+#include "base/containers/contains.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/values.h"
+#include "components/content_settings/core/common/content_settings_types.h"
+#include "components/prefs/pref_service.h"
+#include "content/public/browser/device_service.h"
+#include "electron/grit/electron_resources.h"
+#include "services/device/public/cpp/hid/hid_blocklist.h"
+#include "services/device/public/cpp/hid/hid_switches.h"
+#include "shell/browser/web_contents_permission_helper.h"
+#include "ui/base/l10n/l10n_util.h"
+
+namespace electron {
+
+const char kHidDeviceNameKey[] = "name";
+const char kHidGuidKey[] = "guid";
+const char kHidVendorIdKey[] = "vendorId";
+const char kHidProductIdKey[] = "productId";
+const char kHidSerialNumberKey[] = "serialNumber";
+
+HidChooserContext::HidChooserContext(ElectronBrowserContext* context)
+    : browser_context_(context) {}
+
+HidChooserContext::~HidChooserContext() {
+  // Notify observers that the chooser context is about to be destroyed.
+  // Observers must remove themselves from the observer lists.
+  for (auto& observer : device_observer_list_) {
+    observer.OnHidChooserContextShutdown();
+    DCHECK(!device_observer_list_.HasObserver(&observer));
+  }
+}
+
+// static
+std::u16string HidChooserContext::DisplayNameFromDeviceInfo(
+    const device::mojom::HidDeviceInfo& device) {
+  if (device.product_name.empty()) {
+    auto device_id_string = base::ASCIIToUTF16(
+        base::StringPrintf("%04X:%04X", device.vendor_id, device.product_id));
+    return l10n_util::GetStringFUTF16(IDS_HID_CHOOSER_ITEM_WITHOUT_NAME,
+                                      device_id_string);
+  }
+  return base::UTF8ToUTF16(device.product_name);
+}
+
+// static
+bool HidChooserContext::CanStorePersistentEntry(
+    const device::mojom::HidDeviceInfo& device) {
+  return !device.serial_number.empty() && !device.product_name.empty();
+}
+
+// static
+base::Value HidChooserContext::DeviceInfoToValue(
+    const device::mojom::HidDeviceInfo& device) {
+  base::Value value(base::Value::Type::DICTIONARY);
+  value.SetStringKey(
+      kHidDeviceNameKey,
+      base::UTF16ToUTF8(HidChooserContext::DisplayNameFromDeviceInfo(device)));
+  value.SetIntKey(kHidVendorIdKey, device.vendor_id);
+  value.SetIntKey(kHidProductIdKey, device.product_id);
+  if (HidChooserContext::CanStorePersistentEntry(device)) {
+    // Use the USB serial number as a persistent identifier. If it is
+    // unavailable, only ephemeral permissions may be granted.
+    value.SetStringKey(kHidSerialNumberKey, device.serial_number);
+  } else {
+    // The GUID is a temporary ID created on connection that remains valid until
+    // the device is disconnected. Ephemeral permissions are keyed by this ID
+    // and must be granted again each time the device is connected.
+    value.SetStringKey(kHidGuidKey, device.guid);
+  }
+  return value;
+}
+
+void HidChooserContext::GrantDevicePermission(
+    const url::Origin& origin,
+    const device::mojom::HidDeviceInfo& device,
+    content::RenderFrameHost* render_frame_host) {
+  DCHECK(base::Contains(devices_, device.guid));
+  if (CanStorePersistentEntry(device)) {
+    auto* web_contents =
+        content::WebContents::FromRenderFrameHost(render_frame_host);
+    auto* permission_helper =
+        WebContentsPermissionHelper::FromWebContents(web_contents);
+    permission_helper->GrantHIDDevicePermission(
+        origin, DeviceInfoToValue(device), render_frame_host);
+  } else {
+    ephemeral_devices_[origin].insert(device.guid);
+  }
+}
+
+bool HidChooserContext::HasDevicePermission(
+    const url::Origin& origin,
+    const device::mojom::HidDeviceInfo& device,
+    content::RenderFrameHost* render_frame_host) {
+  if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
+          switches::kDisableHidBlocklist) &&
+      device::HidBlocklist::IsDeviceExcluded(device))
+    return false;
+
+  auto it = ephemeral_devices_.find(origin);
+  if (it != ephemeral_devices_.end() &&
+      base::Contains(it->second, device.guid)) {
+    return true;
+  }
+
+  auto* web_contents =
+      content::WebContents::FromRenderFrameHost(render_frame_host);
+  auto* permission_helper =
+      WebContentsPermissionHelper::FromWebContents(web_contents);
+  return permission_helper->CheckHIDDevicePermission(
+      origin, DeviceInfoToValue(device), render_frame_host);
+}
+
+void HidChooserContext::AddDeviceObserver(DeviceObserver* observer) {
+  EnsureHidManagerConnection();
+  device_observer_list_.AddObserver(observer);
+}
+
+void HidChooserContext::RemoveDeviceObserver(DeviceObserver* observer) {
+  device_observer_list_.RemoveObserver(observer);
+}
+
+void HidChooserContext::GetDevices(
+    device::mojom::HidManager::GetDevicesCallback callback) {
+  if (!is_initialized_) {
+    EnsureHidManagerConnection();
+    pending_get_devices_requests_.push(std::move(callback));
+    return;
+  }
+
+  std::vector<device::mojom::HidDeviceInfoPtr> device_list;
+  device_list.reserve(devices_.size());
+  for (const auto& pair : devices_)
+    device_list.push_back(pair.second->Clone());
+  base::SequencedTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE, base::BindOnce(std::move(callback), std::move(device_list)));
+}
+
+const device::mojom::HidDeviceInfo* HidChooserContext::GetDeviceInfo(
+    const std::string& guid) {
+  DCHECK(is_initialized_);
+  auto it = devices_.find(guid);
+  return it == devices_.end() ? nullptr : it->second.get();
+}
+
+device::mojom::HidManager* HidChooserContext::GetHidManager() {
+  EnsureHidManagerConnection();
+  return hid_manager_.get();
+}
+
+base::WeakPtr<HidChooserContext> HidChooserContext::AsWeakPtr() {
+  return weak_factory_.GetWeakPtr();
+}
+
+void HidChooserContext::DeviceAdded(device::mojom::HidDeviceInfoPtr device) {
+  DCHECK(device);
+
+  // Update the device list.
+  if (!base::Contains(devices_, device->guid))
+    devices_.insert({device->guid, device->Clone()});
+
+  // Notify all observers.
+  for (auto& observer : device_observer_list_)
+    observer.OnDeviceAdded(*device);
+}
+
+void HidChooserContext::DeviceRemoved(device::mojom::HidDeviceInfoPtr device) {
+  DCHECK(device);
+  DCHECK(base::Contains(devices_, device->guid));
+
+  // Update the device list.
+  devices_.erase(device->guid);
+
+  // Notify all device observers.
+  for (auto& observer : device_observer_list_)
+    observer.OnDeviceRemoved(*device);
+
+  // Next we'll notify observers for revoked permissions. If the device does not
+  // support persistent permissions then device permissions are revoked on
+  // disconnect.
+  if (CanStorePersistentEntry(*device))
+    return;
+
+  std::vector<url::Origin> revoked_origins;
+  for (auto& map_entry : ephemeral_devices_) {
+    if (map_entry.second.erase(device->guid) > 0)
+      revoked_origins.push_back(map_entry.first);
+  }
+  if (revoked_origins.empty())
+    return;
+}
+
+void HidChooserContext::DeviceChanged(device::mojom::HidDeviceInfoPtr device) {
+  DCHECK(device);
+  DCHECK(base::Contains(devices_, device->guid));
+
+  // Update the device list.
+  devices_[device->guid] = device->Clone();
+
+  // Notify all observers.
+  for (auto& observer : device_observer_list_)
+    observer.OnDeviceChanged(*device);
+}
+
+void HidChooserContext::EnsureHidManagerConnection() {
+  if (hid_manager_)
+    return;
+
+  mojo::PendingRemote<device::mojom::HidManager> manager;
+  content::GetDeviceService().BindHidManager(
+      manager.InitWithNewPipeAndPassReceiver());
+  SetUpHidManagerConnection(std::move(manager));
+}
+
+void HidChooserContext::SetUpHidManagerConnection(
+    mojo::PendingRemote<device::mojom::HidManager> manager) {
+  hid_manager_.Bind(std::move(manager));
+  hid_manager_.set_disconnect_handler(base::BindOnce(
+      &HidChooserContext::OnHidManagerConnectionError, base::Unretained(this)));
+
+  hid_manager_->GetDevicesAndSetClient(
+      client_receiver_.BindNewEndpointAndPassRemote(),
+      base::BindOnce(&HidChooserContext::InitDeviceList,
+                     weak_factory_.GetWeakPtr()));
+}
+
+void HidChooserContext::InitDeviceList(
+    std::vector<device::mojom::HidDeviceInfoPtr> devices) {
+  for (auto& device : devices)
+    devices_.insert({device->guid, std::move(device)});
+
+  is_initialized_ = true;
+
+  while (!pending_get_devices_requests_.empty()) {
+    std::vector<device::mojom::HidDeviceInfoPtr> device_list;
+    device_list.reserve(devices.size());
+    for (const auto& entry : devices_)
+      device_list.push_back(entry.second->Clone());
+    std::move(pending_get_devices_requests_.front())
+        .Run(std::move(device_list));
+    pending_get_devices_requests_.pop();
+  }
+}
+
+void HidChooserContext::OnHidManagerConnectionError() {
+  hid_manager_.reset();
+  client_receiver_.reset();
+  devices_.clear();
+
+  std::vector<url::Origin> revoked_origins;
+  revoked_origins.reserve(ephemeral_devices_.size());
+  for (const auto& map_entry : ephemeral_devices_)
+    revoked_origins.push_back(map_entry.first);
+  ephemeral_devices_.clear();
+
+  // Notify all device observers.
+  for (auto& observer : device_observer_list_)
+    observer.OnHidManagerConnectionError();
+}
+
+}  // namespace electron

+ 136 - 0
shell/browser/hid/hid_chooser_context.h

@@ -0,0 +1,136 @@
+// Copyright (c) 2021 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_
+#define SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/containers/queue.h"
+#include "base/memory/weak_ptr.h"
+#include "base/observer_list.h"
+#include "base/unguessable_token.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/web_contents.h"
+#include "mojo/public/cpp/bindings/associated_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/device/public/mojom/hid.mojom.h"
+#include "shell/browser/electron_browser_context.h"
+#include "url/origin.h"
+
+namespace base {
+class Value;
+}
+
+namespace electron {
+
+extern const char kHidDeviceNameKey[];
+extern const char kHidGuidKey[];
+extern const char kHidVendorIdKey[];
+extern const char kHidProductIdKey[];
+extern const char kHidSerialNumberKey[];
+
+// Manages the internal state and connection to the device service for the
+// Human Interface Device (HID) chooser UI.
+class HidChooserContext : public KeyedService,
+                          public device::mojom::HidManagerClient {
+ public:
+  // This observer can be used to be notified when HID devices are connected or
+  // disconnected.
+  class DeviceObserver : public base::CheckedObserver {
+   public:
+    virtual void OnDeviceAdded(const device::mojom::HidDeviceInfo&) = 0;
+    virtual void OnDeviceRemoved(const device::mojom::HidDeviceInfo&) = 0;
+    virtual void OnDeviceChanged(const device::mojom::HidDeviceInfo&) = 0;
+    virtual void OnHidManagerConnectionError() = 0;
+
+    // Called when the HidChooserContext is shutting down. Observers must remove
+    // themselves before returning.
+    virtual void OnHidChooserContextShutdown() = 0;
+  };
+
+  explicit HidChooserContext(ElectronBrowserContext* context);
+  HidChooserContext(const HidChooserContext&) = delete;
+  HidChooserContext& operator=(const HidChooserContext&) = delete;
+  ~HidChooserContext() override;
+
+  // Returns a human-readable string identifier for |device|.
+  static std::u16string DisplayNameFromDeviceInfo(
+      const device::mojom::HidDeviceInfo& device);
+
+  // Returns true if a persistent permission can be granted for |device|.
+  static bool CanStorePersistentEntry(
+      const device::mojom::HidDeviceInfo& device);
+
+  static base::Value DeviceInfoToValue(
+      const device::mojom::HidDeviceInfo& device);
+
+  // HID-specific interface for granting and checking permissions.
+  void GrantDevicePermission(const url::Origin& origin,
+                             const device::mojom::HidDeviceInfo& device,
+                             content::RenderFrameHost* render_frame_host);
+  bool HasDevicePermission(const url::Origin& origin,
+                           const device::mojom::HidDeviceInfo& device,
+                           content::RenderFrameHost* render_frame_host);
+
+  // For ScopedObserver.
+  void AddDeviceObserver(DeviceObserver* observer);
+  void RemoveDeviceObserver(DeviceObserver* observer);
+
+  // Forward HidManager::GetDevices.
+  void GetDevices(device::mojom::HidManager::GetDevicesCallback callback);
+
+  // Only call this if you're sure |devices_| has been initialized before-hand.
+  // The returned raw pointer is owned by |devices_| and will be destroyed when
+  // the device is removed.
+  const device::mojom::HidDeviceInfo* GetDeviceInfo(const std::string& guid);
+
+  device::mojom::HidManager* GetHidManager();
+
+  base::WeakPtr<HidChooserContext> AsWeakPtr();
+
+ private:
+  // device::mojom::HidManagerClient implementation:
+  void DeviceAdded(device::mojom::HidDeviceInfoPtr device_info) override;
+  void DeviceRemoved(device::mojom::HidDeviceInfoPtr device_info) override;
+  void DeviceChanged(device::mojom::HidDeviceInfoPtr device_info) override;
+
+  void EnsureHidManagerConnection();
+  void SetUpHidManagerConnection(
+      mojo::PendingRemote<device::mojom::HidManager> manager);
+  void InitDeviceList(std::vector<device::mojom::HidDeviceInfoPtr> devices);
+  void OnHidManagerInitializedForTesting(
+      device::mojom::HidManager::GetDevicesCallback callback,
+      std::vector<device::mojom::HidDeviceInfoPtr> devices);
+  void OnHidManagerConnectionError();
+
+  ElectronBrowserContext* browser_context_;
+
+  bool is_initialized_ = false;
+  base::queue<device::mojom::HidManager::GetDevicesCallback>
+      pending_get_devices_requests_;
+
+  // Tracks the set of devices to which an origin has access to.
+  std::map<url::Origin, std::set<std::string>> ephemeral_devices_;
+
+  // Map from device GUID to device info.
+  std::map<std::string, device::mojom::HidDeviceInfoPtr> devices_;
+
+  mojo::Remote<device::mojom::HidManager> hid_manager_;
+  mojo::AssociatedReceiver<device::mojom::HidManagerClient> client_receiver_{
+      this};
+  base::ObserverList<DeviceObserver> device_observer_list_;
+
+  base::WeakPtrFactory<HidChooserContext> weak_factory_{this};
+};
+
+}  // namespace electron
+
+#endif  // SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_

+ 55 - 0
shell/browser/hid/hid_chooser_context_factory.cc

@@ -0,0 +1,55 @@
+// 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 "shell/browser/hid/hid_chooser_context_factory.h"
+
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "shell/browser/electron_browser_context.h"
+#include "shell/browser/hid/hid_chooser_context.h"
+
+namespace electron {
+
+// static
+HidChooserContextFactory* HidChooserContextFactory::GetInstance() {
+  static base::NoDestructor<HidChooserContextFactory> factory;
+  return factory.get();
+}
+
+// static
+HidChooserContext* HidChooserContextFactory::GetForBrowserContext(
+    content::BrowserContext* context) {
+  return static_cast<HidChooserContext*>(
+      GetInstance()->GetServiceForBrowserContext(context, true));
+}
+
+// static
+HidChooserContext* HidChooserContextFactory::GetForBrowserContextIfExists(
+    content::BrowserContext* context) {
+  return static_cast<HidChooserContext*>(
+      GetInstance()->GetServiceForBrowserContext(context, /*create=*/false));
+}
+
+HidChooserContextFactory::HidChooserContextFactory()
+    : BrowserContextKeyedServiceFactory(
+          "HidChooserContext",
+          BrowserContextDependencyManager::GetInstance()) {}
+
+HidChooserContextFactory::~HidChooserContextFactory() = default;
+
+KeyedService* HidChooserContextFactory::BuildServiceInstanceFor(
+    content::BrowserContext* context) const {
+  auto* browser_context =
+      static_cast<electron::ElectronBrowserContext*>(context);
+  return new HidChooserContext(browser_context);
+}
+
+content::BrowserContext* HidChooserContextFactory::GetBrowserContextToUse(
+    content::BrowserContext* context) const {
+  return context;
+}
+
+void HidChooserContextFactory::BrowserContextShutdown(
+    content::BrowserContext* context) {}
+
+}  // namespace electron

+ 42 - 0
shell/browser/hid/hid_chooser_context_factory.h

@@ -0,0 +1,42 @@
+// 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.
+
+#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_
+#define SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_
+
+#include "base/macros.h"
+#include "base/no_destructor.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace electron {
+
+class HidChooserContext;
+
+class HidChooserContextFactory : public BrowserContextKeyedServiceFactory {
+ public:
+  static HidChooserContext* GetForBrowserContext(
+      content::BrowserContext* context);
+  static HidChooserContext* GetForBrowserContextIfExists(
+      content::BrowserContext* context);
+  static HidChooserContextFactory* GetInstance();
+
+ private:
+  friend base::NoDestructor<HidChooserContextFactory>;
+
+  HidChooserContextFactory();
+  ~HidChooserContextFactory() override;
+
+  // BrowserContextKeyedBaseFactory:
+  KeyedService* BuildServiceInstanceFor(
+      content::BrowserContext* profile) const override;
+  content::BrowserContext* GetBrowserContextToUse(
+      content::BrowserContext* context) const override;
+  void BrowserContextShutdown(content::BrowserContext* context) override;
+
+  DISALLOW_COPY_AND_ASSIGN(HidChooserContextFactory);
+};
+
+}  // namespace electron
+
+#endif  // SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_

+ 366 - 0
shell/browser/hid/hid_chooser_controller.cc

@@ -0,0 +1,366 @@
+// 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/hid_chooser_controller.h"
+
+#include <utility>
+
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/containers/contains.h"
+#include "base/ranges/algorithm.h"
+#include "base/stl_util.h"
+#include "gin/data_object_builder.h"
+#include "services/device/public/cpp/hid/hid_blocklist.h"
+#include "services/device/public/cpp/hid/hid_switches.h"
+#include "shell/browser/api/electron_api_session.h"
+#include "shell/browser/hid/hid_chooser_context.h"
+#include "shell/browser/hid/hid_chooser_context_factory.h"
+#include "shell/browser/javascript_environment.h"
+#include "shell/common/gin_converters/callback_converter.h"
+#include "shell/common/gin_converters/content_converter.h"
+#include "shell/common/gin_converters/value_converter.h"
+#include "shell/common/gin_helper/dictionary.h"
+#include "shell/common/node_includes.h"
+#include "shell/common/process_util.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
+
+namespace gin {
+
+template <>
+struct Converter<device::mojom::HidDeviceInfoPtr> {
+  static v8::Local<v8::Value> ToV8(
+      v8::Isolate* isolate,
+      const device::mojom::HidDeviceInfoPtr& device) {
+    base::Value value = electron::HidChooserContext::DeviceInfoToValue(*device);
+    value.SetStringKey("deviceId", PhysicalDeviceIdFromDeviceInfo(*device));
+    return gin::ConvertToV8(isolate, value);
+  }
+};
+
+}  // namespace gin
+
+namespace electron {
+
+HidChooserController::HidChooserController(
+    content::RenderFrameHost* render_frame_host,
+    std::vector<blink::mojom::HidDeviceFilterPtr> filters,
+    content::HidChooser::Callback callback,
+    content::WebContents* web_contents,
+    base::WeakPtr<ElectronHidDelegate> hid_delegate)
+    : WebContentsObserver(web_contents),
+      filters_(std::move(filters)),
+      callback_(std::move(callback)),
+      origin_(content::WebContents::FromRenderFrameHost(render_frame_host)
+                  ->GetMainFrame()
+                  ->GetLastCommittedOrigin()),
+      frame_tree_node_id_(render_frame_host->GetFrameTreeNodeId()),
+      hid_delegate_(hid_delegate),
+      render_frame_host_id_(render_frame_host->GetGlobalId()) {
+  chooser_context_ = HidChooserContextFactory::GetForBrowserContext(
+                         web_contents->GetBrowserContext())
+                         ->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>());
+}
+
+api::Session* HidChooserController::GetSession() {
+  if (!web_contents()) {
+    return nullptr;
+  }
+  return api::Session::FromBrowserContext(web_contents()->GetBrowserContext());
+}
+
+void HidChooserController::OnDeviceAdded(
+    const device::mojom::HidDeviceInfo& device) {
+  if (!DisplayDevice(device))
+    return;
+  if (AddDeviceInfo(device)) {
+    api::Session* session = GetSession();
+    if (session) {
+      auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
+      v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+      v8::HandleScope scope(isolate);
+      v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
+                                          .Set("device", device.Clone())
+                                          .Set("frame", rfh)
+                                          .Build();
+      session->Emit("hid-device-added", details);
+    }
+  }
+
+  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;
+  api::Session* session = GetSession();
+  if (session) {
+    auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
+    v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+    v8::HandleScope scope(isolate);
+    v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
+                                        .Set("device", device.Clone())
+                                        .Set("frame", rfh)
+                                        .Build();
+    session->Emit("hid-device-removed", details);
+  }
+  RemoveDeviceInfo(device);
+}
+
+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::OnDeviceChosen(gin::Arguments* args) {
+  std::string device_id;
+  if (!args->GetNext(&device_id) || device_id.empty()) {
+    RunCallback({});
+  } else {
+    auto find_it = device_map_.find(device_id);
+    if (find_it != device_map_.end()) {
+      auto& device_infos = find_it->second;
+      std::vector<device::mojom::HidDeviceInfoPtr> devices;
+      devices.reserve(device_infos.size());
+      for (auto& device : device_infos) {
+        chooser_context_->GrantDevicePermission(origin_, *device,
+                                                web_contents()->GetMainFrame());
+        devices.push_back(device->Clone());
+      }
+      RunCallback(std::move(devices));
+    } else {
+      v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+      node::Environment* env = node::Environment::GetCurrent(isolate);
+      EmitWarning(env, "The device id " + device_id + " was not found.",
+                  "UnknownHIDDeviceId");
+      RunCallback({});
+    }
+  }
+}
+
+void HidChooserController::OnHidManagerConnectionError() {
+  observation_.Reset();
+}
+
+void HidChooserController::OnHidChooserContextShutdown() {
+  observation_.Reset();
+}
+
+void HidChooserController::OnGotDevices(
+    std::vector<device::mojom::HidDeviceInfoPtr> devices) {
+  std::vector<device::mojom::HidDeviceInfoPtr> devicesToDisplay;
+  devicesToDisplay.reserve(devices.size());
+
+  for (auto& device : devices) {
+    if (DisplayDevice(*device)) {
+      if (AddDeviceInfo(*device)) {
+        devicesToDisplay.push_back(device->Clone());
+      }
+    }
+  }
+
+  // Listen to HidChooserContext for OnDeviceAdded/Removed events after the
+  // enumeration.
+  if (chooser_context_)
+    observation_.Observe(chooser_context_.get());
+  bool prevent_default = false;
+  api::Session* session = GetSession();
+  if (session) {
+    auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
+    v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+    v8::HandleScope scope(isolate);
+    v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
+                                        .Set("deviceList", devicesToDisplay)
+                                        .Set("frame", rfh)
+                                        .Build();
+    prevent_default =
+        session->Emit("select-hid-device", details,
+                      base::AdaptCallbackForRepeating(
+                          base::BindOnce(&HidChooserController::OnDeviceChosen,
+                                         weak_factory_.GetWeakPtr())));
+  }
+  if (!prevent_default) {
+    RunCallback({});
+  }
+}
+
+bool HidChooserController::DisplayDevice(
+    const device::mojom::HidDeviceInfo& device) const {
+  if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
+          switches::kDisableHidBlocklist)) {
+    // Do not pass the device to the chooser if it is excluded by the blocklist.
+    if (device::HidBlocklist::IsDeviceExcluded(device))
+      return false;
+
+    // Do not pass the device to the chooser if it has a top-level collection
+    // with the FIDO usage page.
+    //
+    // Note: The HID blocklist also blocks top-level collections with the FIDO
+    // usage page, but will not block the device if it has other (non-FIDO)
+    // collections. The check below will exclude the device from the chooser
+    // if it has any top-level FIDO collection.
+    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;
+}
+
+void HidChooserController::UpdateDeviceInfo(
+    const device::mojom::HidDeviceInfo& device) {
+  auto id = PhysicalDeviceIdFromDeviceInfo(device);
+  auto physical_device_it = device_map_.find(id);
+  DCHECK(physical_device_it != device_map_.end());
+  auto& device_infos = physical_device_it->second;
+  auto device_it = base::ranges::find_if(
+      device_infos, [&device](const device::mojom::HidDeviceInfoPtr& d) {
+        return d->guid == device.guid;
+      });
+  DCHECK(device_it != device_infos.end());
+  *device_it = device.Clone();
+}
+
+void HidChooserController::RunCallback(
+    std::vector<device::mojom::HidDeviceInfoPtr> devices) {
+  if (callback_) {
+    std::move(callback_).Run(std::move(devices));
+  }
+}
+
+void HidChooserController::RenderFrameDeleted(
+    content::RenderFrameHost* render_frame_host) {
+  if (hid_delegate_) {
+    hid_delegate_->DeleteControllerForFrame(render_frame_host);
+  }
+}
+
+}  // namespace electron

+ 126 - 0
shell/browser/hid/hid_chooser_controller.h

@@ -0,0 +1,126 @@
+// Copyright (c) 2021 Microsoft, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_
+#define SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
+#include "content/public/browser/global_routing_id.h"
+#include "content/public/browser/hid_chooser.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "services/device/public/mojom/hid.mojom-forward.h"
+#include "shell/browser/api/electron_api_session.h"
+#include "shell/browser/hid/electron_hid_delegate.h"
+#include "shell/browser/hid/hid_chooser_context.h"
+#include "shell/common/gin_converters/frame_converter.h"
+#include "third_party/blink/public/mojom/hid/hid.mojom.h"
+
+namespace content {
+class RenderFrameHost;
+}  // namespace content
+
+namespace electron {
+
+class ElectronHidDelegate;
+
+class HidChooserContext;
+
+// HidChooserController provides data for the WebHID API permission prompt.
+class HidChooserController
+    : public content::WebContentsObserver,
+      public electron::HidChooserContext::DeviceObserver {
+ public:
+  // Construct a chooser controller for Human Interface Devices (HID).
+  // |render_frame_host| is used to initialize the chooser strings and to access
+  // the requesting and embedding origins. |callback| is called when the chooser
+  // is closed, either by selecting an item or by dismissing the chooser dialog.
+  // The callback is called with the selected device, or nullptr if no device is
+  // selected.
+  HidChooserController(content::RenderFrameHost* render_frame_host,
+                       std::vector<blink::mojom::HidDeviceFilterPtr> filters,
+                       content::HidChooser::Callback callback,
+                       content::WebContents* web_contents,
+                       base::WeakPtr<ElectronHidDelegate> hid_delegate);
+  HidChooserController(HidChooserController&) = delete;
+  HidChooserController& operator=(HidChooserController&) = delete;
+  ~HidChooserController() override;
+
+  // HidChooserContext::DeviceObserver:
+  void OnDeviceAdded(const device::mojom::HidDeviceInfo& device_info) override;
+  void OnDeviceRemoved(
+      const device::mojom::HidDeviceInfo& device_info) override;
+  void OnDeviceChanged(
+      const device::mojom::HidDeviceInfo& device_info) override;
+  void OnHidManagerConnectionError() override;
+  void OnHidChooserContextShutdown() override;
+
+  // content::WebContentsObserver:
+  void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override;
+
+ private:
+  api::Session* GetSession();
+  void OnGotDevices(std::vector<device::mojom::HidDeviceInfoPtr> devices);
+  bool DisplayDevice(const device::mojom::HidDeviceInfo& device) const;
+  bool FilterMatchesAny(const device::mojom::HidDeviceInfo& device) const;
+
+  // Add |device_info| to |device_map_|. The device is added to the chooser item
+  // representing the physical device. If the chooser item does not yet exist, a
+  // new item is appended. Returns true if an item was appended.
+  bool AddDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
+
+  // Remove |device_info| from |device_map_|. The device info is removed from
+  // the chooser item representing the physical device. If this would cause the
+  // item to be empty, the chooser item is removed. Does nothing if the device
+  // is not in the chooser item. Returns true if an item was removed.
+  bool RemoveDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
+
+  // Update the information for the device described by |device_info| in the
+  // |device_map_|.
+  void UpdateDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
+
+  void RunCallback(std::vector<device::mojom::HidDeviceInfoPtr> devices);
+  void OnDeviceChosen(gin::Arguments* args);
+
+  std::vector<blink::mojom::HidDeviceFilterPtr> filters_;
+  content::HidChooser::Callback callback_;
+  const url::Origin origin_;
+  const int frame_tree_node_id_;
+
+  // The lifetime of the chooser context is tied to the browser context used to
+  // create it, and may be destroyed while the chooser is still active.
+  base::WeakPtr<HidChooserContext> chooser_context_;
+
+  // Information about connected devices and their HID interfaces. A single
+  // physical device may expose multiple HID interfaces. Keys are physical
+  // device IDs, values are collections of HidDeviceInfo objects representing
+  // the HID interfaces hosted by the physical device.
+  std::map<std::string, std::vector<device::mojom::HidDeviceInfoPtr>>
+      device_map_;
+
+  // An ordered list of physical device IDs that determines the order of items
+  // in the chooser.
+  std::vector<std::string> items_;
+
+  base::ScopedObservation<HidChooserContext,
+                          HidChooserContext::DeviceObserver,
+                          &HidChooserContext::AddDeviceObserver,
+                          &HidChooserContext::RemoveDeviceObserver>
+      observation_{this};
+
+  base::WeakPtr<ElectronHidDelegate> hid_delegate_;
+
+  content::GlobalRenderFrameHostId render_frame_host_id_;
+
+  base::WeakPtrFactory<HidChooserController> weak_factory_{this};
+};
+
+}  // namespace electron
+
+#endif  // SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_

+ 48 - 0
shell/browser/web_contents_permission_helper.cc

@@ -94,6 +94,28 @@ bool WebContentsPermissionHelper::CheckPermission(
                                                         details);
 }
 
+bool WebContentsPermissionHelper::CheckDevicePermission(
+    content::PermissionType permission,
+    const url::Origin& origin,
+    const base::Value* device,
+    content::RenderFrameHost* render_frame_host) const {
+  auto* permission_manager = static_cast<ElectronPermissionManager*>(
+      web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
+  return permission_manager->CheckDevicePermission(permission, origin, device,
+                                                   render_frame_host);
+}
+
+void WebContentsPermissionHelper::GrantDevicePermission(
+    content::PermissionType permission,
+    const url::Origin& origin,
+    const base::Value* device,
+    content::RenderFrameHost* render_frame_host) const {
+  auto* permission_manager = static_cast<ElectronPermissionManager*>(
+      web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
+  permission_manager->GrantDevicePermission(permission, origin, device,
+                                            render_frame_host);
+}
+
 void WebContentsPermissionHelper::RequestFullscreenPermission(
     base::OnceCallback<void(bool)> callback) {
   RequestPermission(
@@ -168,6 +190,32 @@ bool WebContentsPermissionHelper::CheckSerialAccessPermission(
       static_cast<content::PermissionType>(PermissionType::SERIAL), &details);
 }
 
+bool WebContentsPermissionHelper::CheckHIDAccessPermission(
+    const url::Origin& embedding_origin) const {
+  base::DictionaryValue details;
+  details.SetString("securityOrigin", embedding_origin.GetURL().spec());
+  return CheckPermission(
+      static_cast<content::PermissionType>(PermissionType::HID), &details);
+}
+
+bool WebContentsPermissionHelper::CheckHIDDevicePermission(
+    const url::Origin& origin,
+    base::Value device,
+    content::RenderFrameHost* render_frame_host) const {
+  return CheckDevicePermission(
+      static_cast<content::PermissionType>(PermissionType::HID), origin,
+      &device, render_frame_host);
+}
+
+void WebContentsPermissionHelper::GrantHIDDevicePermission(
+    const url::Origin& origin,
+    base::Value device,
+    content::RenderFrameHost* render_frame_host) const {
+  return GrantDevicePermission(
+      static_cast<content::PermissionType>(PermissionType::HID), origin,
+      &device, render_frame_host);
+}
+
 WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPermissionHelper)
 
 }  // namespace electron

+ 21 - 1
shell/browser/web_contents_permission_helper.h

@@ -23,7 +23,8 @@ class WebContentsPermissionHelper
     POINTER_LOCK = static_cast<int>(content::PermissionType::NUM) + 1,
     FULLSCREEN,
     OPEN_EXTERNAL,
-    SERIAL
+    SERIAL,
+    HID
   };
 
   // Asynchronous Requests
@@ -41,6 +42,15 @@ class WebContentsPermissionHelper
   bool CheckMediaAccessPermission(const GURL& security_origin,
                                   blink::mojom::MediaStreamType type) const;
   bool CheckSerialAccessPermission(const url::Origin& embedding_origin) const;
+  bool CheckHIDAccessPermission(const url::Origin& embedding_origin) const;
+  bool CheckHIDDevicePermission(
+      const url::Origin& origin,
+      base::Value device,
+      content::RenderFrameHost* render_frame_host) const;
+  void GrantHIDDevicePermission(
+      const url::Origin& origin,
+      base::Value device,
+      content::RenderFrameHost* render_frame_host) const;
 
  private:
   explicit WebContentsPermissionHelper(content::WebContents* web_contents);
@@ -54,6 +64,16 @@ class WebContentsPermissionHelper
   bool CheckPermission(content::PermissionType permission,
                        const base::DictionaryValue* details) const;
 
+  bool CheckDevicePermission(content::PermissionType permission,
+                             const url::Origin& origin,
+                             const base::Value* device,
+                             content::RenderFrameHost* render_frame_host) const;
+
+  void GrantDevicePermission(content::PermissionType permission,
+                             const url::Origin& origin,
+                             const base::Value* device,
+                             content::RenderFrameHost* render_frame_host) const;
+
   content::WebContents* web_contents_;
 
   WEB_CONTENTS_USER_DATA_KEY_DECL();

+ 2 - 0
shell/common/gin_converters/content_converter.cc

@@ -207,6 +207,8 @@ v8::Local<v8::Value> Converter<content::PermissionType>::ToV8(
       return StringToV8(isolate, "openExternal");
     case PermissionType::SERIAL:
       return StringToV8(isolate, "serial");
+    case PermissionType::HID:
+      return StringToV8(isolate, "hid");
     default:
       return StringToV8(isolate, "unknown");
   }

+ 125 - 1
spec-main/chromium-spec.ts

@@ -1,5 +1,5 @@
 import { expect } from 'chai';
-import { BrowserWindow, WebContents, session, ipcMain, app, protocol, webContents } from 'electron/main';
+import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents } from 'electron/main';
 import { emittedOnce } from './events-helpers';
 import { closeAllWindows } from './window-helpers';
 import * as https from 'https';
@@ -1876,3 +1876,127 @@ describe('navigator.bluetooth', () => {
     expect(bluetooth).to.be.oneOf(['Found a device!', 'Bluetooth adapter not available.', 'User cancelled the requestDevice() chooser.']);
   });
 });
+
+describe('navigator.hid', () => {
+  let w: BrowserWindow;
+  let server: http.Server;
+  let serverUrl: string;
+  before(async () => {
+    w = new BrowserWindow({
+      show: false
+    });
+    await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
+    server = http.createServer((req, res) => {
+      res.setHeader('Content-Type', 'text/html');
+      res.end('<body>');
+    });
+    await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
+    serverUrl = `http://localhost:${(server.address() as any).port}`;
+  });
+
+  const getDevices: any = () => {
+    return w.webContents.executeJavaScript(`
+      navigator.hid.requestDevice({filters: []}).then(device => device.toString()).catch(err => err.toString());
+    `, true);
+  };
+
+  after(() => {
+    server.close();
+    closeAllWindows();
+  });
+  afterEach(() => {
+    session.defaultSession.setPermissionCheckHandler(null);
+    session.defaultSession.setDevicePermissionHandler(null);
+    session.defaultSession.removeAllListeners('select-hid-device');
+  });
+
+  it('does not return a device if select-hid-device event is not defined', async () => {
+    w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
+    const device = await getDevices();
+    expect(device).to.equal('');
+  });
+
+  it('does not return a device when permission denied', async () => {
+    let selectFired = false;
+    w.webContents.session.on('select-hid-device', (event, details, callback) => {
+      selectFired = true;
+      callback();
+    });
+    session.defaultSession.setPermissionCheckHandler(() => false);
+    const device = await getDevices();
+    expect(selectFired).to.be.false();
+    expect(device).to.equal('');
+  });
+
+  it('returns a device when select-hid-device event is defined', async () => {
+    let haveDevices = false;
+    let selectFired = false;
+    w.webContents.session.on('select-hid-device', (event, details, callback) => {
+      expect(details.frame).to.have.ownProperty('frameTreeNodeId').that.is.a('number');
+      selectFired = true;
+      if (details.deviceList.length > 0) {
+        haveDevices = true;
+        callback(details.deviceList[0].deviceId);
+      } else {
+        callback();
+      }
+    });
+    const device = await getDevices();
+    expect(selectFired).to.be.true();
+    if (haveDevices) {
+      expect(device).to.contain('[object HIDDevice]');
+    } else {
+      expect(device).to.equal('');
+    }
+    if (process.arch === 'arm64' || process.arch === 'arm') {
+      // arm CI returns HID devices - this block may need to change if CI hardware changes.
+      expect(haveDevices).to.be.true();
+      // Verify that navigation will clear device permissions
+      const grantedDevices = await w.webContents.executeJavaScript('navigator.hid.getDevices()');
+      expect(grantedDevices).to.not.be.empty();
+      w.loadURL(serverUrl);
+      const [,,,,, frameProcessId, frameRoutingId] = await emittedOnce(w.webContents, 'did-frame-navigate');
+      const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
+      expect(frame).to.not.be.empty();
+      if (frame) {
+        const grantedDevicesOnNewPage = await frame.executeJavaScript('navigator.hid.getDevices()');
+        expect(grantedDevicesOnNewPage).to.be.empty();
+      }
+    }
+  });
+
+  it('returns a device when DevicePermissionHandler is defined', async () => {
+    let haveDevices = false;
+    let selectFired = false;
+    let gotDevicePerms = false;
+    w.webContents.session.on('select-hid-device', (event, details, callback) => {
+      selectFired = true;
+      if (details.deviceList.length > 0) {
+        const foundDevice = details.deviceList.find((device) => {
+          if (device.name && device.name !== '' && device.serialNumber && device.serialNumber !== '') {
+            haveDevices = true;
+            return true;
+          }
+        });
+        if (foundDevice) {
+          callback(foundDevice.deviceId);
+          return;
+        }
+      }
+      callback();
+    });
+    session.defaultSession.setDevicePermissionHandler(() => {
+      gotDevicePerms = true;
+      return true;
+    });
+    await w.webContents.executeJavaScript('navigator.hid.getDevices();', true);
+    const device = await getDevices();
+    expect(selectFired).to.be.true();
+    if (haveDevices) {
+      expect(device).to.contain('[object HIDDevice]');
+      expect(gotDevicePerms).to.be.true();
+    } else {
+      expect(device).to.equal('');
+    }
+  });
+});