Repository: electron/electron
Pull request: fix: take bluetooth serial ports into account in `serial.getPorts()`

#### Description of Change

Closes https://github.com/electron/electron/issues/43083
Refs https://chromium-review.googlesource.com/c/chromium/src/+/4609200

Fixes an issue where `navigator.serial.getPorts()` incorrectly returned an empty array in some cases. This happened as a result of https://github.com/electron/electron/commit/abf7a486050f7705dcea661364b64e058c6c7aae - when `navigator.serial.getPorts()` is called, each port is validated via `SerialChooserContext::HasPortPermission`. To maintain parity with upstream, I incorporated all relevant [checks from upstream](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/serial/serial_chooser_context.cc;l=448-456?q=SerialChooserContext::HasPortPermission&sq=package:chromium&type=cs). However, around the same time,  upstream migrated some of its ports to use a `bluetooth_service_class_id`, and this wasn't incorporated properly into the  `SerialChooserContext::CanStorePersistentEntry` logic. As a result, all serial ports with bluetooth capability were getting filtered out unintentionally, leading to the user sometimes seeing an empty array where they expected several valid ports.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed an issue where `navigator.serial.getPorts()` incorrectly returned an empty array in some cases.

Changed files:
filename: shell/browser/serial/serial_chooser_context.cc
patch: @@ -25,14 +25,15 @@ namespace electron {
 
 constexpr char kPortNameKey[] = "name";
 constexpr char kTokenKey[] = "token";
+constexpr char kBluetoothDevicePathKey[] = "bluetooth_device_path";
 #if BUILDFLAG(IS_WIN)
-const char kDeviceInstanceIdKey[] = "device_instance_id";
+constexpr char kDeviceInstanceIdKey[] = "device_instance_id";
 #else
-const char kVendorIdKey[] = "vendor_id";
-const char kProductIdKey[] = "product_id";
-const char kSerialNumberKey[] = "serial_number";
+constexpr char kVendorIdKey[] = "vendor_id";
+constexpr char kProductIdKey[] = "product_id";
+constexpr char kSerialNumberKey[] = "serial_number";
 #if BUILDFLAG(IS_MAC)
-const char kUsbDriverKey[] = "usb_driver";
+constexpr char kUsbDriverKey[] = "usb_driver";
 #endif  // BUILDFLAG(IS_MAC)
 #endif  // BUILDFLAG(IS_WIN)
 
@@ -45,33 +46,38 @@ std::string EncodeToken(const base::UnguessableToken& token) {
 
 base::Value PortInfoToValue(const device::mojom::SerialPortInfo& port) {
   base::Value::Dict value;
-  if (port.display_name && !port.display_name->empty())
+  if (port.display_name && !port.display_name->empty()) {
     value.Set(kPortNameKey, *port.display_name);
-  else
+  } else {
     value.Set(kPortNameKey, port.path.LossyDisplayName());
+  }
 
   if (!SerialChooserContext::CanStorePersistentEntry(port)) {
     value.Set(kTokenKey, EncodeToken(port.token));
     return base::Value(std::move(value));
   }
 
+  if (port.bluetooth_service_class_id &&
+      port.bluetooth_service_class_id->IsValid()) {
+    value.Set(kBluetoothDevicePathKey, port.path.LossyDisplayName());
+  } else {
 #if BUILDFLAG(IS_WIN)
-  // Windows provides a handy device identifier which we can rely on to be
-  // sufficiently stable for identifying devices across restarts.
-  value.Set(kDeviceInstanceIdKey, port.device_instance_id);
+    // Windows provides a handy device identifier which we can rely on to be
+    // sufficiently stable for identifying devices across restarts.
+    value.Set(kDeviceInstanceIdKey, port.device_instance_id);
 #else
-  DCHECK(port.has_vendor_id);
-  value.Set(kVendorIdKey, port.vendor_id);
-  DCHECK(port.has_product_id);
-  value.Set(kProductIdKey, port.product_id);
-  DCHECK(port.serial_number);
-  value.Set(kSerialNumberKey, *port.serial_number);
-
+    CHECK(port.has_vendor_id);
+    value.Set(kVendorIdKey, port.vendor_id);
+    CHECK(port.has_product_id);
+    value.Set(kProductIdKey, port.product_id);
+    CHECK(port.serial_number);
+    value.Set(kSerialNumberKey, *port.serial_number);
 #if BUILDFLAG(IS_MAC)
-  DCHECK(port.usb_driver_name && !port.usb_driver_name->empty());
-  value.Set(kUsbDriverKey, *port.usb_driver_name);
+    CHECK(port.usb_driver_name && !port.usb_driver_name->empty());
+    value.Set(kUsbDriverKey, *port.usb_driver_name);
 #endif  // BUILDFLAG(IS_MAC)
 #endif  // BUILDFLAG(IS_WIN)
+  }
   return base::Value(std::move(value));
 }
 
@@ -174,11 +180,19 @@ bool SerialChooserContext::CanStorePersistentEntry(
   if (!port.display_name || port.display_name->empty())
     return false;
 
+  const bool has_bluetooth = port.bluetooth_service_class_id &&
+                             port.bluetooth_service_class_id->IsValid() &&
+                             !port.path.empty();
+  if (has_bluetooth) {
+    return true;
+  }
+
 #if BUILDFLAG(IS_WIN)
   return !port.device_instance_id.empty();
 #else
-  if (!port.has_vendor_id || !port.has_product_id || !port.serial_number ||
-      port.serial_number->empty()) {
+  const bool has_usb = port.has_vendor_id && port.has_product_id &&
+                       port.serial_number && !port.serial_number->empty();
+  if (!has_usb) {
     return false;
   }