Commit 278801c2 authored by Ovidio Henriquez's avatar Ovidio Henriquez Committed by Commit Bot

bluetooth: getDevices() implementation

This change implements getDevices() which returns a list of
WebBluetoothDevice objects that the current site has permission to
access. If the kWebBluetoothNewPermissionsBackend flag is enabled, the
list of devices will contain all of the permitted devices. If the flag
is not enabled, then the list of devices will contain the permitted
devices that are currently connected to the system.

Design doc:
https://docs.google.com/document/d/1h3uAVXJARHrNWaNACUPiQhLt7XI-fFFQoARSs1WgMDM/edit#heading=h.5ugemo7p04z9

Bug: 577953
Change-Id: I9785f24ee46ac634b6a96d6146f54da37d132a4e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2044660
Commit-Queue: Ovidio de Jesús Ruiz-Henríquez <odejesush@chromium.org>
Reviewed-by: default avatarReilly Grant <reillyg@chromium.org>
Reviewed-by: default avatarVincent Scheib <scheib@chromium.org>
Reviewed-by: default avatarJohn Abd-El-Malek <jam@chromium.org>
Reviewed-by: default avatarMustafa Emre Acer <meacer@chromium.org>
Reviewed-by: default avatarPhilip Jägenstedt <foolip@chromium.org>
Cr-Commit-Position: refs/heads/master@{#748917}
parent c780858b
......@@ -4,6 +4,8 @@
#include "chrome/browser/bluetooth/chrome_bluetooth_delegate.h"
#include <memory>
#include "chrome/browser/bluetooth/bluetooth_chooser_context.h"
#include "chrome/browser/bluetooth/bluetooth_chooser_context_factory.h"
#include "chrome/browser/profiles/profile.h"
......@@ -106,3 +108,25 @@ bool ChromeBluetoothDelegate::IsAllowedToAccessAtLeastOneService(
frame->GetLastCommittedOrigin(),
web_contents->GetMainFrame()->GetLastCommittedOrigin(), device_id);
}
std::vector<blink::mojom::WebBluetoothDevicePtr>
ChromeBluetoothDelegate::GetPermittedDevices(content::RenderFrameHost* frame) {
auto* web_contents = WebContents::FromRenderFrameHost(frame);
std::vector<std::unique_ptr<permissions::ChooserContextBase::Object>>
objects = GetBluetoothChooserContext(web_contents)
->GetGrantedObjects(
frame->GetLastCommittedOrigin(),
web_contents->GetMainFrame()->GetLastCommittedOrigin());
std::vector<blink::mojom::WebBluetoothDevicePtr> permitted_devices;
for (const auto& object : objects) {
auto permitted_device = blink::mojom::WebBluetoothDevice::New();
permitted_device->id =
BluetoothChooserContext::GetObjectDeviceId(object->value);
permitted_device->name =
BluetoothChooserContext::GetObjectName(object->value);
permitted_devices.push_back(std::move(permitted_device));
}
return permitted_devices;
}
......@@ -6,6 +6,7 @@
#define CHROME_BROWSER_BLUETOOTH_CHROME_BLUETOOTH_DELEGATE_H_
#include <string>
#include <vector>
#include "content/public/browser/bluetooth_delegate.h"
#include "third_party/blink/public/mojom/bluetooth/web_bluetooth.mojom-forward.h"
......@@ -58,6 +59,8 @@ class ChromeBluetoothDelegate : public content::BluetoothDelegate {
bool IsAllowedToAccessAtLeastOneService(
content::RenderFrameHost* frame,
const blink::WebBluetoothDeviceId& device_id) override;
std::vector<blink::mojom::WebBluetoothDevicePtr> GetPermittedDevices(
content::RenderFrameHost* frame) override;
};
#endif // CHROME_BROWSER_BLUETOOTH_CHROME_BLUETOOTH_DELEGATE_H_
......@@ -705,6 +705,25 @@ void WebBluetoothServiceImpl::RequestDevice(
RequestDeviceImpl(std::move(options), std::move(callback), GetAdapter());
}
void WebBluetoothServiceImpl::GetDevices(GetDevicesCallback callback) {
if (GetBluetoothAllowed() != blink::mojom::WebBluetoothResult::SUCCESS ||
!BluetoothAdapterFactoryWrapper::Get().IsLowEnergySupported()) {
std::move(callback).Run({});
return;
}
auto* adapter = GetAdapter();
if (adapter) {
GetDevicesImpl(std::move(callback), adapter);
return;
}
BluetoothAdapterFactoryWrapper::Get().AcquireAdapter(
this,
base::BindOnce(&WebBluetoothServiceImpl::GetDevicesImpl,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebBluetoothServiceImpl::RemoteServerConnect(
const blink::WebBluetoothDeviceId& device_id,
mojo::PendingAssociatedRemote<blink::mojom::WebBluetoothServerClient>
......@@ -1421,6 +1440,38 @@ void WebBluetoothServiceImpl::RequestDeviceImpl(
weak_ptr_factory_.GetWeakPtr(), copyable_callback));
}
void WebBluetoothServiceImpl::GetDevicesImpl(
GetDevicesCallback callback,
scoped_refptr<device::BluetoothAdapter> adapter) {
if (base::FeatureList::IsEnabled(
features::kWebBluetoothNewPermissionsBackend)) {
BluetoothDelegate* delegate =
GetContentClient()->browser()->GetBluetoothDelegate();
if (!delegate) {
std::move(callback).Run({});
return;
}
std::move(callback).Run(delegate->GetPermittedDevices(render_frame_host_));
return;
}
// BluetoothAllowedDevices does not provide a way to get all of the permitted
// devices, so instead return all of the allowed devices that are currently
// known to the system.
std::vector<blink::mojom::WebBluetoothDevicePtr> web_bluetooth_devices;
for (const auto* device : adapter->GetDevices()) {
const blink::WebBluetoothDeviceId* device_id =
allowed_devices().GetDeviceId(device->GetAddress());
if (!device_id || !allowed_devices().IsAllowedToGATTConnect(*device_id))
continue;
web_bluetooth_devices.push_back(
blink::mojom::WebBluetoothDevice::New(*device_id, device->GetName()));
}
std::move(callback).Run(std::move(web_bluetooth_devices));
}
void WebBluetoothServiceImpl::RemoteServerGetPrimaryServicesImpl(
const blink::WebBluetoothDeviceId& device_id,
blink::mojom::WebBluetoothGATTQueryQuantity quantity,
......
......@@ -7,6 +7,7 @@
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "base/gtest_prod_util.h"
......@@ -196,6 +197,7 @@ class CONTENT_EXPORT WebBluetoothServiceImpl
void GetAvailability(GetAvailabilityCallback callback) override;
void RequestDevice(blink::mojom::WebBluetoothRequestDeviceOptionsPtr options,
RequestDeviceCallback callback) override;
void GetDevices(GetDevicesCallback callback) override;
void RemoteServerConnect(
const blink::WebBluetoothDeviceId& device_id,
mojo::PendingAssociatedRemote<blink::mojom::WebBluetoothServerClient>
......@@ -251,6 +253,9 @@ class CONTENT_EXPORT WebBluetoothServiceImpl
RequestDeviceCallback callback,
scoped_refptr<device::BluetoothAdapter> adapter);
void GetDevicesImpl(GetDevicesCallback callback,
scoped_refptr<device::BluetoothAdapter> adapter);
void RequestScanningStartImpl(
mojo::AssociatedRemote<blink::mojom::WebBluetoothScanClient> client,
blink::mojom::WebBluetoothRequestLEScanOptionsPtr options,
......
......@@ -6,6 +6,7 @@
#define CONTENT_PUBLIC_BROWSER_BLUETOOTH_DELEGATE_H_
#include <string>
#include <vector>
#include "base/containers/flat_set.h"
#include "content/common/content_export.h"
......@@ -85,6 +86,15 @@ class CONTENT_EXPORT BluetoothDelegate {
virtual bool IsAllowedToAccessAtLeastOneService(
RenderFrameHost* frame,
const blink::WebBluetoothDeviceId& device_id) = 0;
// This should return a list of devices that the origin in |frame| has been
// allowed to access. Access permission is granted with
// GrantServiceAccessPermission() and can be revoked by the user in the
// embedder's UI. The list of devices returned should be PermittedDevice
// objects, which contain the necessary fields to create the BluetoothDevice
// JavaScript objects.
virtual std::vector<blink::mojom::WebBluetoothDevicePtr> GetPermittedDevices(
RenderFrameHost* frame) = 0;
};
} // namespace content
......
......@@ -86,6 +86,20 @@ bool FakeBluetoothDelegate::IsAllowedToAccessAtLeastOneService(
return !id_to_services_it->second.empty();
}
std::vector<blink::mojom::WebBluetoothDevicePtr>
FakeBluetoothDelegate::GetPermittedDevices(RenderFrameHost* frame) {
std::vector<blink::mojom::WebBluetoothDevicePtr> permitted_devices;
auto& device_address_to_id_map = GetAddressToIdMapForOrigin(frame);
for (const auto& entry : device_address_to_id_map) {
auto permitted_device = blink::mojom::WebBluetoothDevice::New();
WebBluetoothDeviceId device_id = entry.second;
permitted_device->id = device_id;
permitted_device->name = device_id_to_name_map_[device_id];
permitted_devices.push_back(std::move(permitted_device));
}
return permitted_devices;
}
WebBluetoothDeviceId FakeBluetoothDelegate::GetOrCreateDeviceIdForDeviceAddress(
RenderFrameHost* frame,
const std::string& device_address) {
......
......@@ -64,6 +64,8 @@ class FakeBluetoothDelegate : public BluetoothDelegate {
bool IsAllowedToAccessAtLeastOneService(
RenderFrameHost* frame,
const blink::WebBluetoothDeviceId& device_id) override;
std::vector<blink::mojom::WebBluetoothDevicePtr> GetPermittedDevices(
RenderFrameHost* frame) override;
private:
using AddressToIdMap = std::map<std::string, blink::WebBluetoothDeviceId>;
......
......@@ -181,9 +181,20 @@ interface WebBluetoothService {
// Checks if Web Bluetooth is allowed and if a Bluetooth radio is available.
GetAvailability() => (bool is_available);
// Requests access to a Bluetooth device which matches
// |options->filters->services|. Access is limited to the current origin, and
// to the union of |options->filters->services| and
// |options->optional_services|. Multiple devices may be discoverable that
// match and one must be selected via a chooser user interface.
RequestDevice(WebBluetoothRequestDeviceOptions options)
=> (WebBluetoothResult result, WebBluetoothDevice? device);
// Returns a list of permitted Bluetooth devices that the current origin can
// access services on. These devices are granted access via RequestDevice(),
// but the permission can be revoked at any time by the user through the
// browser's UI.
GetDevices() => (array<WebBluetoothDevice> devices);
// Creates a GATT Connection to a Bluetooth Device identified by |device_id|
// if a connection to the device didn't exist already. If a GATT connection
// existed already then this function increases the ref count to keep that
......
......@@ -6,6 +6,7 @@
#include <utility>
#include "build/build_config.h"
#include "mojo/public/cpp/bindings/associated_receiver_set.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
......@@ -50,6 +51,21 @@ const char kHandleGestureForPermissionRequest[] =
"Must be handling a user gesture to show a permission request.";
} // namespace
// Remind developers when they are using Web Bluetooth on unsupported platforms.
// TODO(https://crbug.com/570344): Remove this method when all platforms are
// supported.
void AddUnsupportedPlatformConsoleMessage(ExecutionContext* context) {
#if !defined(OS_CHROMEOS) && !defined(OS_ANDROID) && !defined(OS_MACOSX) && \
!defined(OS_WIN)
context->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kInfo,
"Web Bluetooth is experimental on this platform. See "
"https://github.com/WebBluetoothCG/web-bluetooth/blob/gh-pages/"
"implementation-status.md"));
#endif
}
static void CanonicalizeFilter(
const BluetoothLEScanFilterInit* filter,
mojom::blink::WebBluetoothLeScanFilterPtr& canonicalized_filter,
......@@ -168,6 +184,23 @@ ScriptPromise Bluetooth::getAvailability(ScriptState* script_state,
return promise;
}
void Bluetooth::GetDevicesCallback(
ScriptPromiseResolver* resolver,
Vector<mojom::blink::WebBluetoothDevicePtr> devices) {
if (!resolver->GetExecutionContext() ||
resolver->GetExecutionContext()->IsContextDestroyed()) {
return;
}
HeapVector<Member<BluetoothDevice>> bluetooth_devices;
for (auto& device : devices) {
BluetoothDevice* bluetooth_device = GetBluetoothDeviceRepresentingDevice(
std::move(device), resolver->GetExecutionContext());
bluetooth_devices.push_back(*bluetooth_device);
}
resolver->Resolve(bluetooth_devices);
}
void Bluetooth::RequestDeviceCallback(
ScriptPromiseResolver* resolver,
mojom::blink::WebBluetoothResult result,
......@@ -186,6 +219,27 @@ void Bluetooth::RequestDeviceCallback(
}
}
ScriptPromise Bluetooth::getDevices(ScriptState* script_state,
ExceptionState& exception_state) {
ExecutionContext* context = GetExecutionContext();
if (!context) {
exception_state.ThrowTypeError(kInactiveDocumentError);
return ScriptPromise();
}
AddUnsupportedPlatformConsoleMessage(context);
CHECK(context->IsSecureContext());
EnsureServiceConnection(context);
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
service_->GetDevices(WTF::Bind(&Bluetooth::GetDevicesCallback,
WrapPersistent(this),
WrapPersistent(resolver)));
return promise;
}
// https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-requestdevice
ScriptPromise Bluetooth::requestDevice(ScriptState* script_state,
const RequestDeviceOptions* options,
......@@ -196,17 +250,7 @@ ScriptPromise Bluetooth::requestDevice(ScriptState* script_state,
return ScriptPromise();
}
// Remind developers when they are using Web Bluetooth on unsupported platforms.
#if !defined(OS_CHROMEOS) && !defined(OS_ANDROID) && !defined(OS_MACOSX) && \
!defined(OS_WIN)
context->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kInfo,
"Web Bluetooth is experimental on this platform. See "
"https://github.com/WebBluetoothCG/web-bluetooth/blob/gh-pages/"
"implementation-status.md"));
#endif
AddUnsupportedPlatformConsoleMessage(context);
CHECK(context->IsSecureContext());
// If the algorithm is not allowed to show a popup, reject promise with a
......
......@@ -36,6 +36,7 @@ class Bluetooth final : public EventTargetWithInlineData,
// IDL exposed interface:
ScriptPromise getAvailability(ScriptState*, ExceptionState&);
ScriptPromise getDevices(ScriptState*, ExceptionState&);
ScriptPromise requestDevice(ScriptState*,
const RequestDeviceOptions*,
ExceptionState&);
......@@ -72,6 +73,9 @@ class Bluetooth final : public EventTargetWithInlineData,
mojom::blink::WebBluetoothDevicePtr,
ExecutionContext*);
void GetDevicesCallback(ScriptPromiseResolver*,
Vector<mojom::blink::WebBluetoothDevicePtr>);
void RequestDeviceCallback(ScriptPromiseResolver*,
mojom::blink::WebBluetoothResult,
mojom::blink::WebBluetoothDevicePtr);
......
......@@ -10,6 +10,7 @@
SecureContext
] interface Bluetooth : EventTarget {
[CallWith=ScriptState, RaisesException] Promise<boolean> getAvailability();
[RuntimeEnabled=WebBluetoothGetDevices, CallWith=ScriptState, RaisesException] Promise<sequence<BluetoothDevice>> getDevices();
[CallWith=ScriptState, RaisesException, MeasureAs=WebBluetoothRequestDevice] Promise<BluetoothDevice> requestDevice (optional RequestDeviceOptions options = {});
// https://webbluetoothcg.github.io/web-bluetooth/scanning.html#scanning
......
......@@ -1815,6 +1815,10 @@
"default": "experimental",
},
},
{
name: "WebBluetoothGetDevices",
status: "experimental",
},
{
name: "WebBluetoothScanning",
status: "experimental",
......
// META: script=/resources/testdriver.js
// META: script=/resources/testdriver-vendor.js
// META: script=/bluetooth/resources/bluetooth-helpers.js
'use strict';
const test_desc = 'getDevices() resolves with permitted devices that can be ' +
'GATT connected to.';
bluetooth_test(async () => {
// Set up two connectable Bluetooth devices with their services discovered.
// One device is a Health Thermometer device with the 'health_thermometer'
// service while the other is a Heart Rate device with the 'heart_rate'
// service. Both devices contain the 'generic_access' service.
let fake_peripherals = await setUpHealthThermometerAndHeartRateDevices();
for (let fake_peripheral of fake_peripherals) {
await fake_peripheral.setNextGATTConnectionResponse({code: HCI_SUCCESS});
await fake_peripheral.addFakeService({uuid: 'generic_access'});
if (fake_peripheral.address === '09:09:09:09:09:09')
await fake_peripheral.addFakeService({uuid: 'health_thermometer'});
else
await fake_peripheral.addFakeService({uuid: 'heart_rate'});
await fake_peripheral.setNextGATTDiscoveryResponse({code: HCI_SUCCESS});
}
// Request the Health Thermometer device with access to its 'generic_access'
// service.
await requestDeviceWithTrustedClick(
{filters: [{name: 'Health Thermometer', services: ['generic_access']}]});
let devices = await navigator.bluetooth.getDevices();
assert_equals(
devices.length, 1,
`getDevices() should return the 'Health Thermometer' device.`);
// Only the 'generic_access' service can be accessed.
try {
await devices[0].gatt.connect();
await devices[0].gatt.getPrimaryService('generic_access');
assert_promise_rejects_with_message(
devices[0].gatt.getPrimaryService('health_thermometer'),
{name: 'SecurityError'});
} catch (err) {
assert_unreached(`${err.name}: ${err.message}`);
}
// Request the Heart Rate device with access to both of its services.
await requestDeviceWithTrustedClick({
filters: [{name: 'Heart Rate', services: ['generic_access', 'heart_rate']}]
});
devices = await navigator.bluetooth.getDevices();
assert_equals(
devices.length, 2,
`getDevices() should return the 'Health Thermometer' and 'Health ` +
`Monitor' devices`);
// All of Heart Rate device's services can be accessed, while only the
// 'generic_access' service can be accessed on Health Thermometer.
try {
for (let device of devices) {
await device.gatt.connect();
await device.gatt.getPrimaryService('generic_access');
if (device.name === 'Heart Rate') {
await device.gatt.getPrimaryService('heart_rate');
} else {
assert_promise_rejects_with_message(
devices[0].gatt.getPrimaryService('health_thermometer'),
{name: 'SecurityError'});
}
}
} catch (err) {
assert_unreached(`${err.name}: ${err.message}`);
}
}, test_desc);
\ No newline at end of file
// META: script=/resources/testdriver.js
// META: script=/resources/testdriver-vendor.js
// META: script=/bluetooth/resources/bluetooth-helpers.js
'use strict';
const test_desc = 'getDevices() resolves with empty array if no device ' +
'permissions have been granted.';
bluetooth_test(async () => {
await navigator.bluetooth.test.simulateCentral({state: 'powered-on'});
let devices = await navigator.bluetooth.getDevices();
assert_equals(
0, devices.length, 'getDevices() should resolve with an empty array');
}, test_desc);
\ No newline at end of file
// META: script=/resources/testdriver.js
// META: script=/resources/testdriver-vendor.js
// META: script=/bluetooth/resources/bluetooth-helpers.js
'use strict';
const test_desc = 'multiple calls to getDevices() resolves with the same' +
'BluetoothDevice objects for each granted Bluetooth device.';
bluetooth_test(async () => {
await getConnectedHealthThermometerDevice();
let firstDevices = await navigator.bluetooth.getDevices();
assert_equals(
firstDevices.length, 1, 'getDevices() should return the granted device.');
let secondDevices = await navigator.bluetooth.getDevices();
assert_equals(
secondDevices.length, 1,
'getDevices() should return the granted device.');
assert_equals(
firstDevices[0], secondDevices[0],
'getDevices() should produce the same BluetoothDevice objects for a ' +
'given Bluetooth device.');
}, test_desc);
\ No newline at end of file
......@@ -16,6 +16,7 @@ test(() => {
// Bluetooth implements BluetoothDiscovery;
assert_true('requestDevice' in navigator.bluetooth);
assert_true('getDevices' in navigator.bluetooth);
assert_equals(navigator.bluetooth.requestDevice.length, 0);
}, test_desc);
</script>
This is a testharness.js-based test.
Found 206 tests; 147 PASS, 59 FAIL, 0 TIMEOUT, 0 NOTRUN.
Found 206 tests; 149 PASS, 57 FAIL, 0 TIMEOUT, 0 NOTRUN.
PASS idl_test setup
PASS idl_test validation
PASS Partial interface Navigator: original interface defined
......@@ -30,7 +30,7 @@ PASS Bluetooth interface: existence and properties of interface prototype object
PASS Bluetooth interface: operation getAvailability()
FAIL Bluetooth interface: attribute onavailabilitychanged assert_true: The prototype object must have a property "onavailabilitychanged" expected true got false
FAIL Bluetooth interface: attribute referringDevice assert_true: The prototype object must have a property "referringDevice" expected true got false
FAIL Bluetooth interface: operation getDevices() assert_own_property: interface prototype object missing non-static operation expected property "getDevices" missing
PASS Bluetooth interface: operation getDevices()
PASS Bluetooth interface: operation requestDevice(optional RequestDeviceOptions)
PASS Bluetooth interface: attribute onadvertisementreceived
FAIL Bluetooth interface: attribute ongattserverdisconnected assert_true: The prototype object must have a property "ongattserverdisconnected" expected true got false
......@@ -43,7 +43,7 @@ PASS Stringification of navigator.bluetooth
PASS Bluetooth interface: navigator.bluetooth must inherit property "getAvailability()" with the proper type
FAIL Bluetooth interface: navigator.bluetooth must inherit property "onavailabilitychanged" with the proper type assert_inherits: property "onavailabilitychanged" not found in prototype chain
FAIL Bluetooth interface: navigator.bluetooth must inherit property "referringDevice" with the proper type assert_inherits: property "referringDevice" not found in prototype chain
FAIL Bluetooth interface: navigator.bluetooth must inherit property "getDevices()" with the proper type assert_inherits: property "getDevices" not found in prototype chain
PASS Bluetooth interface: navigator.bluetooth must inherit property "getDevices()" with the proper type
PASS Bluetooth interface: navigator.bluetooth must inherit property "requestDevice(optional RequestDeviceOptions)" with the proper type
PASS Bluetooth interface: calling requestDevice(optional RequestDeviceOptions) on navigator.bluetooth with too few arguments must throw TypeError
PASS Bluetooth interface: navigator.bluetooth must inherit property "onadvertisementreceived" with the proper type
......
......@@ -8,6 +8,7 @@ interface Bluetooth : EventTarget
getter onadvertisementreceived
method constructor
method getAvailability
method getDevices
method requestDevice
method requestLEScan
setter onadvertisementreceived
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment