Repository: electron/electron (language: cpp)

Issue:
[Bug]: How can I cancel a bluetooth request?
### Preflight Checklist

- [X] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) for this project.
- [X] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) that this project adheres to.
- [X] I have searched the [issue tracker](https://www.github.com/electron/electron/issues) for a bug report that matches the one I want to file, without success.

### Electron Version

23

### What operating system are you using?

Windows

### Operating System Version

win10

### What arch are you using?

x64

### Last Known Working Electron version

_No response_

### Expected Behavior

navigator.bluetooth.requestDevice  return null  after a while, if there is no device found. 

### Actual Behavior

navigator.bluetooth.requestDevice   always bolck if there is no device

### Testcase Gist URL
https://gist.github.com/a5d16943212179f7c033d5bd70531404
_No response_

### Additional Information

_No response_


Discussion / hints:
Hello @717009629. Thanks for reporting this and helping to make Electron better!

Would it be possible for you to make a standalone testcase with only the code necessary to reproduce the issue? For example, [Electron Fiddle](https://www.electronjs.org/fiddle) is a great tool for making small test cases and makes it easy to publish your test case to a [gist](https://gist.github.com) that Electron maintainers can use.

Stand-alone test cases make fixing issues go more smoothly: it ensure everyone's looking at the same issue, it removes all unnecessary variables from the equation, and it can also provide the basis for automated regression tests.

Now adding the `blocked/need-repro` label for this reason. After you make a test case, please link to it in a followup comment. This issue will be closed in 10 days if the above is not addressed.

Fix patch:
diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md
index c8b0ef8c011a1..47f6e8a41c0c7 100644
--- a/docs/api/web-contents.md
+++ b/docs/api/web-contents.md
@@ -777,20 +777,24 @@ Returns:
 * `callback` Function
   * `deviceId` string
 
-Emitted when bluetooth device needs to be selected on call to
-`navigator.bluetooth.requestDevice`. To use `navigator.bluetooth` api
-`webBluetooth` should be enabled. If `event.preventDefault` is not called,
-first available device will be selected. `callback` should be called with
-`deviceId` to be selected, passing empty string to `callback` will
-cancel the request.
+Emitted when a bluetooth device needs to be selected when a call to
+`navigator.bluetooth.requestDevice` is made. `callback` should be called with
+the `deviceId` of the device to be selected.  Passing an empty string to
+`callback` will cancel the request.
 
-If no event listener is added for this event, all bluetooth requests will be cancelled.
+If an event listener is not added for this event, or if `event.preventDefault`
+is not called when handling this event, the first available device will be
+automatically selected.
+
+Due to the nature of bluetooth, scanning for devices when
+`navigator.bluetooth.requestDevice` is called may take time and will cause
+`select-bluetooth-device` to fire multiple times until `callback` is called
+with either a device id or an empty string to cancel the request.
 
 ```javascript title='main.js'
 const { app, BrowserWindow } = require('electron')
 
 let win = null
-app.commandLine.appendSwitch('enable-experimental-web-platform-features')
 
 app.whenReady().then(() => {
   win = new BrowserWindow({ width: 800, height: 600 })
@@ -800,6 +804,9 @@ app.whenReady().then(() => {
       return device.deviceName === 'test'
     })
     if (!result) {
+      // The device wasn't found so we need to either wait longer (eg until the
+      // device is turned on) or cancel the request by calling the callback 
+      // with an empty string.
       callback('')
     } else {
       callback(result.deviceId)
diff --git a/docs/fiddles/features/web-bluetooth/index.html b/docs/fiddles/features/web-bluetooth/index.html
index b2be53d400a6a..5b0158fa36424 100644
--- a/docs/fiddles/features/web-bluetooth/index.html
+++ b/docs/fiddles/features/web-bluetooth/index.html
@@ -9,6 +9,7 @@
     <h1>Web Bluetooth API</h1>
 
     <button id="clickme">Test Bluetooth</button>
+    <button id="cancel">Cancel Bluetooth Request</button>
 
     <p>Currently selected bluetooth device: <strong id="device-name""></strong></p>
 
diff --git a/docs/fiddles/features/web-bluetooth/main.js b/docs/fiddles/features/web-bluetooth/main.js
index be44dc0507319..4d6e6d38e27ce 100644
--- a/docs/fiddles/features/web-bluetooth/main.js
+++ b/docs/fiddles/features/web-bluetooth/main.js
@@ -1,6 +1,9 @@
 const {app, BrowserWindow, ipcMain} = require('electron')
 const path = require('path')
 
+let bluetoothPinCallback 
+let selectBluetoothCallback
+
 function createWindow () {
   const mainWindow = new BrowserWindow({
     width: 800,
@@ -12,10 +15,22 @@ function createWindow () {
 
   mainWindow.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
     event.preventDefault()
-    if (deviceList && deviceList.length > 0) {
-      callback(deviceList[0].deviceId)
-    }
+    selectBluetoothCallback = callback
+    const result = deviceList.find((device) => {
+      return device.deviceName === 'test'
+    })
+    if (result) {
+      callback(result.deviceId)
+    } else {
+      // The device wasn't found so we need to either wait longer (eg until the
+      // device is turned on) or until the user cancels the request
+    }     
+  })
+
+  ipcMain.on('cancel-bluetooth-request', (event) => {
+    selectBluetoothCallback('')
   })
+  
 
   // Listen for a message from the renderer to get the response for the Bluetooth pairing.
   ipcMain.on('bluetooth-pairing-response', (event, response) => {
diff --git a/docs/fiddles/features/web-bluetooth/preload.js b/docs/fiddles/features/web-bluetooth/preload.js
index 0c21fcce93881..a8d30560368e4 100644
--- a/docs/fiddles/features/web-bluetooth/preload.js
+++ b/docs/fiddles/features/web-bluetooth/preload.js
@@ -1,6 +1,7 @@
 const { contextBridge, ipcRenderer } = require('electron')
 
 contextBridge.exposeInMainWorld('electronAPI', {
+  cancelBluetoothRequest: (callback) => ipcRenderer.send('cancel-bluetooth-request', callback),
   bluetoothPairingRequest: (callback) => ipcRenderer.on('bluetooth-pairing-request', callback),
   bluetoothPairingResponse: (response) => ipcRenderer.send('bluetooth-pairing-response', response)
 })
\ No newline at end of file
diff --git a/docs/fiddles/features/web-bluetooth/renderer.js b/docs/fiddles/features/web-bluetooth/renderer.js
index c51a06ca91777..241c253de0150 100644
--- a/docs/fiddles/features/web-bluetooth/renderer.js
+++ b/docs/fiddles/features/web-bluetooth/renderer.js
@@ -7,6 +7,12 @@ async function testIt() {
 
 document.getElementById('clickme').addEventListener('click',testIt)
 
+function cancelRequest() {
+  window.electronAPI.cancelBluetoothRequest()
+}
+
+document.getElementById('cancel').addEventListener('click', cancelRequest)
+
 window.electronAPI.bluetoothPairingRequest((event, details) => {
   const response = {}
 
diff --git a/shell/browser/lib/bluetooth_chooser.cc b/shell/browser/lib/bluetooth_chooser.cc
index 7c9942d7ba584..7d84cff45e2c0 100644
--- a/shell/browser/lib/bluetooth_chooser.cc
+++ b/shell/browser/lib/bluetooth_chooser.cc
@@ -27,8 +27,6 @@ namespace electron {
 
 namespace {
 
-const int kMaxScanRetries = 5;
-
 void OnDeviceChosen(const content::BluetoothChooser::EventHandler& handler,
                     const std::string& device_id) {
   if (device_id.empty()) {
@@ -66,29 +64,15 @@ void BluetoothChooser::SetAdapterPresence(AdapterPresence presence) {
 }
 
 void BluetoothChooser::ShowDiscoveryState(DiscoveryState state) {
+  bool idle_state = false;
   switch (state) {
     case DiscoveryState::FAILED_TO_START:
       refreshing_ = false;
       event_handler_.Run(content::BluetoothChooserEvent::CANCELLED, "");
-      break;
+      return;
     case DiscoveryState::IDLE:
       refreshing_ = false;
-      if (device_map_.empty()) {
-        auto event = ++num_retries_ > kMaxScanRetries
-                         ? content::BluetoothChooserEvent::CANCELLED
-                         : content::BluetoothChooserEvent::RESCAN;
-        event_handler_.Run(event, "");
-      } else {
-        bool prevent_default = api_web_contents_->Emit(
-            "select-bluetooth-device", GetDeviceList(),
-            base::BindOnce(&OnDeviceChosen, event_handler_));
-        if (!prevent_default) {
-          auto it = device_map_.begin();
-          auto device_id = it->first;
-          event_handler_.Run(content::BluetoothChooserEvent::SELECTED,
-                             device_id);
-        }
-      }
+      idle_state = true;
       break;
     case DiscoveryState::DISCOVERING:
       // The first time this state fires is due to a rescan triggering so set a
@@ -101,6 +85,18 @@ void BluetoothChooser::ShowDiscoveryState(DiscoveryState state) {
       }
       break;
   }
+  bool prevent_default =
+      api_web_contents_->Emit("select-bluetooth-device", GetDeviceList(),
+                              base::BindOnce(&OnDeviceChosen, event_handler_));
+  if (!prevent_default && idle_state) {
+    if (device_map_.empty()) {
+      event_handler_.Run(content::BluetoothChooserEvent::CANCELLED, "");
+    } else {
+      auto it = device_map_.begin();
+      auto device_id = it->first;
+      event_handler_.Run(content::BluetoothChooserEvent::SELECTED, device_id);
+    }
+  }
 }
 
 void BluetoothChooser::AddOrUpdateDevice(const std::string& device_id,
diff --git a/shell/browser/lib/bluetooth_chooser.h b/shell/browser/lib/bluetooth_chooser.h
index 8143e4ce5cfe9..aed252c556866 100644
--- a/shell/browser/lib/bluetooth_chooser.h
+++ b/shell/browser/lib/bluetooth_chooser.h
@@ -44,7 +44,6 @@ class BluetoothChooser : public content::BluetoothChooser {
   std::map<std::string, std::u16string> device_map_;
   api::WebContents* api_web_contents_;
   EventHandler event_handler_;
-  int num_retries_ = 0;
   bool refreshing_ = false;
   bool rescan_ = false;
 };


Test patch: