From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../bluetooth/resources/bluetooth-fake-devices.js | 1203 ++++++++++++++++++++ 1 file changed, 1203 insertions(+) create mode 100644 testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js (limited to 'testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js') diff --git a/testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js b/testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js new file mode 100644 index 0000000000..b718ab579a --- /dev/null +++ b/testing/web-platform/tests/bluetooth/resources/bluetooth-fake-devices.js @@ -0,0 +1,1203 @@ +'use strict'; + +/* Bluetooth Constants */ + +/** + * HCI Error Codes. + * Used for simulateGATT{Dis}ConnectionResponse. For a complete list of + * possible error codes see BT 4.2 Vol 2 Part D 1.3 List Of Error Codes. + */ +const HCI_SUCCESS = 0x0000; +const HCI_CONNECTION_TIMEOUT = 0x0008; + +/** + * GATT Error codes. + * Used for GATT operations responses. BT 4.2 Vol 3 Part F 3.4.1.1 Error + * Response + */ +const GATT_SUCCESS = 0x0000; +const GATT_INVALID_HANDLE = 0x0001; + +/* Bluetooth UUID Constants */ + +/* Service UUIDs */ +var blocklist_test_service_uuid = '611c954a-263b-4f4a-aab6-01ddb953f985'; +var request_disconnection_service_uuid = '01d7d889-7451-419f-aeb8-d65e7b9277af'; + +/* Characteristic UUIDs */ +var blocklist_exclude_reads_characteristic_uuid = + 'bad1c9a2-9a5b-4015-8b60-1579bbbf2135'; +var request_disconnection_characteristic_uuid = + '01d7d88a-7451-419f-aeb8-d65e7b9277af'; + +/* Descriptor UUIDs */ +var blocklist_test_descriptor_uuid = 'bad2ddcf-60db-45cd-bef9-fd72b153cf7c'; +var blocklist_exclude_reads_descriptor_uuid = + 'bad3ec61-3cc3-4954-9702-7977df514114'; + +/** + * Helper objects that associate Bluetooth names, aliases, and UUIDs. These are + * useful for tests that check that the same result is produces when using all + * three methods of referring to a Bluetooth UUID. + */ +var generic_access = { + alias: 0x1800, + name: 'generic_access', + uuid: '00001800-0000-1000-8000-00805f9b34fb' +}; +var device_name = { + alias: 0x2a00, + name: 'gap.device_name', + uuid: '00002a00-0000-1000-8000-00805f9b34fb' +}; +var reconnection_address = { + alias: 0x2a03, + name: 'gap.reconnection_address', + uuid: '00002a03-0000-1000-8000-00805f9b34fb' +}; +var heart_rate = { + alias: 0x180d, + name: 'heart_rate', + uuid: '0000180d-0000-1000-8000-00805f9b34fb' +}; +var health_thermometer = { + alias: 0x1809, + name: 'health_thermometer', + uuid: '00001809-0000-1000-8000-00805f9b34fb' +}; +var body_sensor_location = { + alias: 0x2a38, + name: 'body_sensor_location', + uuid: '00002a38-0000-1000-8000-00805f9b34fb' +}; +var glucose = { + alias: 0x1808, + name: 'glucose', + uuid: '00001808-0000-1000-8000-00805f9b34fb' +}; +var battery_service = { + alias: 0x180f, + name: 'battery_service', + uuid: '0000180f-0000-1000-8000-00805f9b34fb' +}; +var battery_level = { + alias: 0x2A19, + name: 'battery_level', + uuid: '00002a19-0000-1000-8000-00805f9b34fb' +}; +var user_description = { + alias: 0x2901, + name: 'gatt.characteristic_user_description', + uuid: '00002901-0000-1000-8000-00805f9b34fb' +}; +var client_characteristic_configuration = { + alias: 0x2902, + name: 'gatt.client_characteristic_configuration', + uuid: '00002902-0000-1000-8000-00805f9b34fb' +}; +var measurement_interval = { + alias: 0x2a21, + name: 'measurement_interval', + uuid: '00002a21-0000-1000-8000-00805f9b34fb' +}; + +/** + * An advertisement packet object that simulates a Health Thermometer device. + * @type {ScanResult} + */ +const health_thermometer_ad_packet = { + deviceAddress: '09:09:09:09:09:09', + rssi: -10, + scanRecord: { + name: 'Health Thermometer', + uuids: [health_thermometer.uuid], + }, +}; + +/** + * An advertisement packet object that simulates a Heart Rate device. + * @type {ScanResult} + */ +const heart_rate_ad_packet = { + deviceAddress: '08:08:08:08:08:08', + rssi: -10, + scanRecord: { + name: 'Heart Rate', + uuids: [heart_rate.uuid], + }, +}; + +const uuid1234 = BluetoothUUID.getService(0x1234); +const uuid5678 = BluetoothUUID.getService(0x5678); +const uuidABCD = BluetoothUUID.getService(0xABCD); +const manufacturer1Data = new Uint8Array([1, 2]); +const manufacturer2Data = new Uint8Array([3, 4]); +const uuid1234Data = new Uint8Array([5, 6]); +const uuid5678Data = new Uint8Array([7, 8]); +const uuidABCDData = new Uint8Array([9, 10]); + +// TODO(crbug.com/1163207): Add the blocklist link. +// Fake manufacturer data following iBeacon format listed in +// https://en.wikipedia.org/wiki/IBeacon, which will be blocked according to [TBD blocklist link]. +const blocklistedManufacturerId = 0x4c; +const blocklistedManufacturerData = new Uint8Array([ + 0x02, 0x15, 0xb3, 0xeb, 0x8d, 0xb1, 0x30, 0xa5, 0x44, 0x8d, 0xb4, 0xac, + 0xfb, 0x68, 0xc9, 0x23, 0xa3, 0x0e, 0x00, 0x00, 0x00, 0x00, 0xbf +]); +// Fake manufacturer data that is not in [TBD blocklist link]. +const nonBlocklistedManufacturerId = 0x0001; +const nonBlocklistedManufacturerData = new Uint8Array([1, 2]); + +/** + * An advertisement packet object that simulates a device that advertises + * service and manufacturer data. + * @type {ScanResult} + */ +const service_and_manufacturer_data_ad_packet = { + deviceAddress: '07:07:07:07:07:07', + rssi: -10, + scanRecord: { + name: 'LE Device', + uuids: [uuid1234], + manufacturerData: {0x0001: manufacturer1Data, 0x0002: manufacturer2Data}, + serviceData: { + [uuid1234]: uuid1234Data, + [uuid5678]: uuid5678Data, + [uuidABCD]: uuidABCDData + } + } +}; + +/** Bluetooth Helpers */ + +/** + * Helper class to create a BluetoothCharacteristicProperties object using an + * array of strings corresponding to the property bit to set. + */ +class TestCharacteristicProperties { + /** @param {Array} properties */ + constructor(properties) { + this.broadcast = false; + this.read = false; + this.writeWithoutResponse = false; + this.write = false; + this.notify = false; + this.indicate = false; + this.authenticatedSignedWrites = false; + this.reliableWrite = false; + this.writableAuxiliaries = false; + + properties.forEach(val => { + if (this.hasOwnProperty(val)) + this[val] = true; + else + throw `Invalid member '${val}'`; + }); + } +} + +/** + * Produces an array of BluetoothLEScanFilterInit objects containing the list of + * services in |services| and various permutations of the other + * BluetoothLEScanFilterInit properties. This method is used to test that the + * |services| are valid so the other properties do not matter. + * @param {BluetoothServiceUUID} services + * @returns {Array} A list of options containing + * |services| and various permutations of other options. + */ +function generateRequestDeviceArgsWithServices(services = ['heart_rate']) { + return [ + {filters: [{services: services}]}, + {filters: [{services: services, name: 'Name'}]}, + {filters: [{services: services, namePrefix: 'Pre'}]}, { + filters: [ + {services: services, manufacturerData: [{companyIdentifier: 0x0001}]} + ] + }, + { + filters: [{ + services: services, + name: 'Name', + namePrefix: 'Pre', + manufacturerData: [{companyIdentifier: 0x0001}] + }] + }, + {filters: [{services: services}], optionalServices: ['heart_rate']}, { + filters: [{services: services, name: 'Name'}], + optionalServices: ['heart_rate'] + }, + { + filters: [{services: services, namePrefix: 'Pre'}], + optionalServices: ['heart_rate'] + }, + { + filters: [ + {services: services, manufacturerData: [{companyIdentifier: 0x0001}]} + ], + optionalServices: ['heart_rate'] + }, + { + filters: [{ + services: services, + name: 'Name', + namePrefix: 'Pre', + manufacturerData: [{companyIdentifier: 0x0001}] + }], + optionalServices: ['heart_rate'] + } + ]; +} + +/** + * Causes |fake_peripheral| to disconnect and returns a promise that resolves + * once `gattserverdisconnected` has been fired on |device|. + * @param {BluetoothDevice} device The device to check if the + * `gattserverdisconnected` promise was fired. + * @param {FakePeripheral} fake_peripheral The device fake that represents + * |device|. + * @returns {Promise>} A promise that resolves when the device has + * successfully disconnected. + */ +function simulateGATTDisconnectionAndWait(device, fake_peripheral) { + return Promise.all([ + eventPromise(device, 'gattserverdisconnected'), + fake_peripheral.simulateGATTDisconnection(), + ]); +} + +/** @type {FakeCentral} The fake adapter for the current test. */ +let fake_central = null; + +async function initializeFakeCentral({state = 'powered-on'}) { + if (!fake_central) { + fake_central = await navigator.bluetooth.test.simulateCentral({state}); + } +} + +/** + * A dictionary for specifying fake Bluetooth device setup options. + * @typedef {{address: !string, name: !string, + * manufacturerData: !Object>, + * knownServiceUUIDs: !Array, connectable: !boolean, + * serviceDiscoveryComplete: !boolean}} + */ +let FakeDeviceOptions; + +/** + * @typedef {{fakeDeviceOptions: FakeDeviceOptions, + * requestDeviceOptions: RequestDeviceOptions}} + */ +let SetupOptions; + +/** + * Default options for setting up a Bluetooth device. + * @type {FakeDeviceOptions} + */ +const fakeDeviceOptionsDefault = { + address: '00:00:00:00:00:00', + name: 'LE Device', + manufacturerData: {}, + knownServiceUUIDs: [], + connectable: false, + serviceDiscoveryComplete: false, +}; + +/** + * A dictionary containing the fake Bluetooth device object. The dictionary can + * optionally contain its fake services and its BluetoothDevice counterpart. + * @typedef {{fake_peripheral: !FakePeripheral, + * fake_services: Object, + * device: BluetoothDevice}} + */ +let FakeDevice; + +/** + * Creates a SetupOptions object using |setupOptionsDefault| as the base options + * object with the options from |setupOptionsOverride| overriding these + * defaults. + * @param {SetupOptions} setupOptionsDefault The default options object to use + * as the base. + * @param {SetupOptions} setupOptionsOverride The options to override the + * defaults with. + * @returns {SetupOptions} The merged setup options containing the defaults with + * the overrides applied. + */ +function createSetupOptions(setupOptionsDefault, setupOptionsOverride) { + // Merge the properties of |setupOptionsDefault| and |setupOptionsOverride| + // without modifying |setupOptionsDefault|. + let fakeDeviceOptions = Object.assign( + {...setupOptionsDefault.fakeDeviceOptions}, + setupOptionsOverride.fakeDeviceOptions); + let requestDeviceOptions = Object.assign( + {...setupOptionsDefault.requestDeviceOptions}, + setupOptionsOverride.requestDeviceOptions); + + return {fakeDeviceOptions, requestDeviceOptions}; +} + +/** + * Adds a preconnected device with the given options. A preconnected device is a + * device that has been paired with the system previously. This can be done if, + * for example, the user pairs the device using the OS'es settings. + * + * By default, the preconnected device will be set up using the + * |fakeDeviceOptionsDefault| and will not use a RequestDeviceOption object. + * This means that the device will not be requested during the setup. + * + * If |setupOptionsOverride| is provided, these options will override the + * defaults. If |setupOptionsOverride| includes the requestDeviceOptions + * property, then the device will be requested using those options. + * @param {SetupOptions} setupOptionsOverride An object containing options for + * setting up a fake Bluetooth device and for requesting the device. + * @returns {Promise} The device fake initialized with the + * parameter values. + */ +async function setUpPreconnectedFakeDevice(setupOptionsOverride) { + await initializeFakeCentral({state: 'powered-on'}); + + let setupOptions = createSetupOptions( + {fakeDeviceOptions: fakeDeviceOptionsDefault}, setupOptionsOverride); + + // Simulate the fake peripheral. + let preconnectedDevice = {}; + preconnectedDevice.fake_peripheral = + await fake_central.simulatePreconnectedPeripheral({ + address: setupOptions.fakeDeviceOptions.address, + name: setupOptions.fakeDeviceOptions.name, + manufacturerData: setupOptions.fakeDeviceOptions.manufacturerData, + knownServiceUUIDs: setupOptions.fakeDeviceOptions.knownServiceUUIDs, + }); + + if (setupOptions.fakeDeviceOptions.connectable) { + await preconnectedDevice.fake_peripheral.setNextGATTConnectionResponse( + {code: HCI_SUCCESS}); + } + + // Add known services. + preconnectedDevice.fake_services = new Map(); + for (let service of setupOptions.fakeDeviceOptions.knownServiceUUIDs) { + let fake_service = await preconnectedDevice.fake_peripheral.addFakeService( + {uuid: service}); + preconnectedDevice.fake_services.set(service, fake_service); + } + + // Request the device if the request option isn't empty. + if (Object.keys(setupOptions.requestDeviceOptions).length !== 0) { + preconnectedDevice.device = + await requestDeviceWithTrustedClick(setupOptions.requestDeviceOptions); + } + + // Set up services discovered state. + if (setupOptions.fakeDeviceOptions.serviceDiscoveryComplete) { + await preconnectedDevice.fake_peripheral.setNextGATTDiscoveryResponse( + {code: HCI_SUCCESS}); + } + + return preconnectedDevice; +} + +/** Blocklisted GATT Device Helper Methods */ + +/** @type {FakeDeviceOptions} */ +const blocklistFakeDeviceOptionsDefault = { + address: '11:11:11:11:11:11', + name: 'Blocklist Device', + knownServiceUUIDs: ['generic_access', blocklist_test_service_uuid], + connectable: true, + serviceDiscoveryComplete: true +}; + +/** @type {RequestDeviceOptions} */ +const blocklistRequestDeviceOptionsDefault = { + filters: [{services: [blocklist_test_service_uuid]}] +}; + +/** @type {SetupOptions} */ +const blocklistSetupOptionsDefault = { + fakeDeviceOptions: blocklistFakeDeviceOptionsDefault, + requestDeviceOptions: blocklistRequestDeviceOptionsDefault +}; + +/** + * Returns an object containing a BluetoothDevice discovered using |options|, + * its corresponding FakePeripheral and FakeRemoteGATTServices. + * The simulated device is called 'Blocklist Device' and it has one known + * service UUID |blocklist_test_service_uuid|. The |blocklist_test_service_uuid| + * service contains two characteristics: + * - |blocklist_exclude_reads_characteristic_uuid| (read, write) + * - 'gap.peripheral_privacy_flag' (read, write) + * The 'gap.peripheral_privacy_flag' characteristic contains three descriptors: + * - |blocklist_test_descriptor_uuid| + * - |blocklist_exclude_reads_descriptor_uuid| + * - 'gatt.client_characteristic_configuration' + * These are special UUIDs that have been added to the blocklist found at + * https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt + * There are also test UUIDs that have been added to the test environment which + * other implementations should add as test UUIDs as well. + * The device has been connected to and its attributes are ready to be + * discovered. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor}>} An + * object containing the BluetoothDevice object and its corresponding + * GATT fake objects. + */ +async function getBlocklistDevice(setupOptionsOverride = {}) { + let setupOptions = + createSetupOptions(blocklistSetupOptionsDefault, setupOptionsOverride); + let fakeDevice = await setUpPreconnectedFakeDevice(setupOptions); + await fakeDevice.device.gatt.connect(); + + let fake_blocklist_test_service = + fakeDevice.fake_services.get(blocklist_test_service_uuid); + + let fake_blocklist_exclude_reads_characteristic = + await fake_blocklist_test_service.addFakeCharacteristic({ + uuid: blocklist_exclude_reads_characteristic_uuid, + properties: ['read', 'write'], + }); + let fake_blocklist_exclude_writes_characteristic = + await fake_blocklist_test_service.addFakeCharacteristic({ + uuid: 'gap.peripheral_privacy_flag', + properties: ['read', 'write'], + }); + + let fake_blocklist_descriptor = + await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor( + {uuid: blocklist_test_descriptor_uuid}); + let fake_blocklist_exclude_reads_descriptor = + await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor( + {uuid: blocklist_exclude_reads_descriptor_uuid}); + let fake_blocklist_exclude_writes_descriptor = + await fake_blocklist_exclude_writes_characteristic.addFakeDescriptor( + {uuid: 'gatt.client_characteristic_configuration'}); + return { + device: fakeDevice.device, + fake_peripheral: fakeDevice.fake_peripheral, + fake_blocklist_test_service, + fake_blocklist_exclude_reads_characteristic, + fake_blocklist_exclude_writes_characteristic, + fake_blocklist_descriptor, + fake_blocklist_exclude_reads_descriptor, + fake_blocklist_exclude_writes_descriptor, + }; +} + +/** + * Returns an object containing a Blocklist Test BluetoothRemoteGattService and + * its corresponding FakeRemoteGATTService. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, + * service: BluetoothRemoteGATTService, + * fake_service: FakeBluetoothRemoteGATTService}>} An object containing the + * BluetoothDevice object and its corresponding GATT fake objects. + */ +async function getBlocklistTestService() { + let result = await getBlocklistDevice(); + let service = + await result.device.gatt.getPrimaryService(blocklist_test_service_uuid); + return Object.assign(result, { + service, + fake_service: result.fake_blocklist_test_service, + }); +} + +/** + * Returns an object containing a blocklisted BluetoothRemoteGATTCharacteristic + * that excludes reads and its corresponding FakeRemoteGATTCharacteristic. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, + * service: BluetoothRemoteGATTService, + * fake_service: FakeBluetoothRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic}>} An object + * containing the BluetoothDevice object and its corresponding GATT fake + * objects. + */ +async function getBlocklistExcludeReadsCharacteristic() { + let result = await getBlocklistTestService(); + let characteristic = await result.service.getCharacteristic( + blocklist_exclude_reads_characteristic_uuid); + return Object.assign(result, { + characteristic, + fake_characteristic: result.fake_blocklist_exclude_reads_characteristic + }); +} + +/** + * Returns an object containing a blocklisted BluetoothRemoteGATTCharacteristic + * that excludes writes and its corresponding FakeRemoteGATTCharacteristic. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, + * service: BluetoothRemoteGATTService, + * fake_service: FakeBluetoothRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic}>} An object + * containing the BluetoothDevice object and its corresponding GATT fake + * objects. + */ +async function getBlocklistExcludeWritesCharacteristic() { + let result = await getBlocklistTestService(); + let characteristic = + await result.service.getCharacteristic('gap.peripheral_privacy_flag'); + return Object.assign(result, { + characteristic, + fake_characteristic: result.fake_blocklist_exclude_writes_characteristic + }); +} + +/** + * Returns an object containing a blocklisted BluetoothRemoteGATTDescriptor that + * excludes reads and its corresponding FakeRemoteGATTDescriptor. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, + * service: BluetoothRemoteGATTService, + * fake_service: FakeBluetoothRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic, + * descriptor: BluetoothRemoteGATTDescriptor, + * fake_descriptor: FakeBluetoothRemoteGATTDescriptor}>} An object + * containing the BluetoothDevice object and its corresponding GATT fake + * objects. + */ +async function getBlocklistExcludeReadsDescriptor() { + let result = await getBlocklistExcludeWritesCharacteristic(); + let descriptor = await result.characteristic.getDescriptor( + blocklist_exclude_reads_descriptor_uuid); + return Object.assign(result, { + descriptor, + fake_descriptor: result.fake_blocklist_exclude_reads_descriptor + }); +} + +/** + * Returns an object containing a blocklisted BluetoothRemoteGATTDescriptor that + * excludes writes and its corresponding FakeRemoteGATTDescriptor. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_blocklist_test_service: FakeRemoteGATTService, + * fake_blocklist_exclude_reads_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_exclude_writes_characteristic: + * FakeRemoteGATTCharacteristic, + * fake_blocklist_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_reads_descriptor: FakeRemoteGATTDescriptor, + * fake_blocklist_exclude_writes_descriptor: FakeRemoteGATTDescriptor, + * service: BluetoothRemoteGATTService, + * fake_service: FakeBluetoothRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeBluetoothRemoteGATTCharacteristic, + * descriptor: BluetoothRemoteGATTDescriptor, + * fake_descriptor: FakeBluetoothRemoteGATTDescriptor}>} An object + * containing the BluetoothDevice object and its corresponding GATT fake + * objects. + */ +async function getBlocklistExcludeWritesDescriptor() { + let result = await getBlocklistExcludeWritesCharacteristic(); + let descriptor = await result.characteristic.getDescriptor( + 'gatt.client_characteristic_configuration'); + return Object.assign(result, { + descriptor: descriptor, + fake_descriptor: result.fake_blocklist_exclude_writes_descriptor, + }); +} + +/** Bluetooth HID Device Helper Methods */ + +/** @type {FakeDeviceOptions} */ +const connectedHIDFakeDeviceOptionsDefault = { + address: '10:10:10:10:10:10', + name: 'HID Device', + knownServiceUUIDs: [ + 'generic_access', + 'device_information', + 'human_interface_device', + ], + connectable: true, + serviceDiscoveryComplete: false +}; + +/** @type {RequestDeviceOptions} */ +const connectedHIDRequestDeviceOptionsDefault = { + filters: [{services: ['device_information']}], + optionalServices: ['human_interface_device'] +}; + +/** @type {SetupOptions} */ +const connectedHIDSetupOptionsDefault = { + fakeDeviceOptions: connectedHIDFakeDeviceOptionsDefault, + requestDeviceOptions: connectedHIDRequestDeviceOptionsDefault +}; + +/** + * Similar to getHealthThermometerDevice except the GATT discovery + * response has not been set yet so more attributes can still be added. + * TODO(crbug.com/719816): Add descriptors. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object + * containing a requested BluetoothDevice and its fake counter part. + */ +async function getConnectedHIDDevice( + requestDeviceOptionsOverride, fakeDeviceOptionsOverride) { + let setupOptions = createSetupOptions(connectedHIDSetupOptionsDefault, { + fakeDeviceOptions: fakeDeviceOptionsOverride, + requestDeviceOptions: requestDeviceOptionsOverride + }); + + let fakeDevice = await setUpPreconnectedFakeDevice(setupOptions); + await fakeDevice.device.gatt.connect(); + + // Blocklisted Characteristic: + // https://github.com/WebBluetoothCG/registries/blob/master/gatt_blocklist.txt + let dev_info = fakeDevice.fake_services.get('device_information'); + await dev_info.addFakeCharacteristic({ + uuid: 'serial_number_string', + properties: ['read'], + }); + return fakeDevice; +} + +/** + * Returns a BluetoothDevice discovered using |options| and its + * corresponding FakePeripheral. + * The simulated device is called 'HID Device' it has three known service + * UUIDs: 'generic_access', 'device_information', 'human_interface_device'. + * The primary service with 'device_information' UUID has a characteristics + * with UUID 'serial_number_string'. The device has been connected to and its + * attributes are ready to be discovered. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object + * containing a requested BluetoothDevice and its fake counter part. + */ +async function getHIDDevice(options) { + let result = + await getConnectedHIDDevice(options, {serviceDiscoveryComplete: true}); + return result; +} + +/** Health Thermometer Bluetooth Device Helper Methods */ + +/** @type {FakeDeviceOptions} */ +const healthTherometerFakeDeviceOptionsDefault = { + address: '09:09:09:09:09:09', + name: 'Health Thermometer', + manufacturerData: {0x0001: manufacturer1Data, 0x0002: manufacturer2Data}, + knownServiceUUIDs: ['generic_access', 'health_thermometer'], +}; + +/** + * Returns a FakeDevice that corresponds to a simulated pre-connected device + * called 'Health Thermometer'. The device has two known serviceUUIDs: + * 'generic_access' and 'health_thermometer' and some fake manufacturer data. + * @returns {Promise} The device fake initialized as a Health + * Thermometer device. + */ +async function setUpHealthThermometerDevice(setupOptionsOverride = {}) { + let setupOptions = createSetupOptions( + {fakeDeviceOptions: healthTherometerFakeDeviceOptionsDefault}, + setupOptionsOverride); + return await setUpPreconnectedFakeDevice(setupOptions); +} + +/** + * Returns the same fake device as setUpHealthThermometerDevice() except + * that connecting to the peripheral will succeed. + * @returns {Promise} The device fake initialized as a + * connectable Health Thermometer device. + */ +async function setUpConnectableHealthThermometerDevice() { + let fake_device = await setUpHealthThermometerDevice( + {fakeDeviceOptions: {connectable: true}}); + return fake_device; +} + +/** + * Populates a fake_device with various fakes appropriate for a health + * thermometer. This resolves to an associative array composed of the fakes, + * including the |fake_peripheral|. + * @param {FakeDevice} fake_device The Bluetooth fake to populate GATT + * services, characteristics, and descriptors on. + * @returns {Promise<{fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic}>} The FakePeripheral + * passed into this method along with the fake GATT services, characteristics, + * and descriptors added to it. + */ +async function populateHealthThermometerFakes(fake_device) { + let fake_peripheral = fake_device.fake_peripheral; + let fake_generic_access = fake_device.fake_services.get('generic_access'); + let fake_health_thermometer = + fake_device.fake_services.get('health_thermometer'); + let fake_measurement_interval = + await fake_health_thermometer.addFakeCharacteristic({ + uuid: 'measurement_interval', + properties: ['read', 'write', 'indicate'], + }); + let fake_user_description = + await fake_measurement_interval.addFakeDescriptor({ + uuid: 'gatt.characteristic_user_description', + }); + let fake_cccd = await fake_measurement_interval.addFakeDescriptor({ + uuid: 'gatt.client_characteristic_configuration', + }); + let fake_temperature_measurement = + await fake_health_thermometer.addFakeCharacteristic({ + uuid: 'temperature_measurement', + properties: ['indicate'], + }); + let fake_temperature_type = + await fake_health_thermometer.addFakeCharacteristic({ + uuid: 'temperature_type', + properties: ['read'], + }); + return { + fake_peripheral, + fake_generic_access, + fake_health_thermometer, + fake_measurement_interval, + fake_cccd, + fake_user_description, + fake_temperature_measurement, + fake_temperature_type, + }; +} + +/** + * Returns the same device and fake peripheral as getHealthThermometerDevice() + * after another frame (an iframe we insert) discovered the device, + * connected to it and discovered its services. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getHealthThermometerDeviceWithServicesDiscovered(options) { + let iframe = document.createElement('iframe'); + let fake_device = await setUpConnectableHealthThermometerDevice(); + let fakes = populateHealthThermometerFakes(fake_device); + await fake_device.fake_peripheral.setNextGATTDiscoveryResponse({ + code: HCI_SUCCESS, + }); + await new Promise(resolve => { + let src = '/bluetooth/resources/health-thermometer-iframe.html'; + // TODO(509038): Can be removed once LayoutTests/bluetooth/* that + // use health-thermometer-iframe.html have been moved to + // LayoutTests/external/wpt/bluetooth/* + if (window.location.pathname.includes('/LayoutTests/')) { + src = + '../../../external/wpt/bluetooth/resources/health-thermometer-iframe.html'; + } + iframe.src = src; + document.body.appendChild(iframe); + iframe.addEventListener('load', resolve); + }); + await new Promise((resolve, reject) => { + callWithTrustedClick(() => { + iframe.contentWindow.postMessage( + {type: 'DiscoverServices', options: options}, '*'); + }); + + function messageHandler(messageEvent) { + if (messageEvent.data == 'DiscoveryComplete') { + window.removeEventListener('message', messageHandler); + resolve(); + } else { + reject(new Error(`Unexpected message: ${messageEvent.data}`)); + } + } + window.addEventListener('message', messageHandler); + }); + let device = await requestDeviceWithTrustedClick(options); + await device.gatt.connect(); + return Object.assign({device}, fakes); +} + +/** + * Returns the device requested and connected in the given iframe context and + * fakes from populateHealthThermometerFakes(). + * @param {object} iframe The iframe element set up by the caller document. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getHealthThermometerDeviceFromIframe(iframe) { + const fake_device = await setUpConnectableHealthThermometerDevice(); + const fakes = await populateHealthThermometerFakes(fake_device); + await new Promise(resolve => { + let src = '/bluetooth/resources/health-thermometer-iframe.html'; + iframe.src = src; + document.body.appendChild(iframe); + iframe.addEventListener('load', resolve, {once: true}); + }); + await new Promise((resolve, reject) => { + callWithTrustedClick(() => { + iframe.contentWindow.postMessage( + { + type: 'RequestAndConnect', + options: {filters: [{services: [health_thermometer.name]}]} + }, + '*'); + }); + + function messageHandler(messageEvent) { + if (messageEvent.data == 'Connected') { + window.removeEventListener('message', messageHandler); + resolve(); + } else { + reject(new Error(`Unexpected message: ${messageEvent.data}`)); + } + } + window.addEventListener('message', messageHandler, {once: true}); + }); + const devices = await iframe.contentWindow.navigator.bluetooth.getDevices(); + assert_equals(devices.length, 1); + return Object.assign({device: devices[0]}, {fakes}); +} + +/** + * Similar to getHealthThermometerDevice() except the device + * is not connected and thus its services have not been + * discovered. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object + * containing a requested BluetoothDevice and its fake counter part. + */ +async function getDiscoveredHealthThermometerDevice(options = { + filters: [{services: ['health_thermometer']}] +}) { + return await setUpHealthThermometerDevice({requestDeviceOptions: options}); +} + +/** + * Similar to getHealthThermometerDevice() except the device has no services, + * characteristics, or descriptors. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {device: BluetoothDevice, fake_peripheral: FakePeripheral} An object + * containing a requested BluetoothDevice and its fake counter part. + */ +async function getEmptyHealthThermometerDevice(options) { + let fake_device = await getDiscoveredHealthThermometerDevice(options); + let fake_generic_access = fake_device.fake_services.get('generic_access'); + let fake_health_thermometer = + fake_device.fake_services.get('health_thermometer'); + // Remove services that have been set up by previous steps. + await fake_generic_access.remove(); + await fake_health_thermometer.remove(); + await fake_device.fake_peripheral.setNextGATTConnectionResponse( + {code: HCI_SUCCESS}); + await fake_device.device.gatt.connect(); + await fake_device.fake_peripheral.setNextGATTDiscoveryResponse( + {code: HCI_SUCCESS}); + return fake_device; +} + +/** + * Similar to getHealthThermometerService() except the service has no + * characteristics or included services. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {service: BluetoothRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService} An object containing the + * health themometer service object and its corresponding fake. + */ +async function getEmptyHealthThermometerService(options) { + let result = await getDiscoveredHealthThermometerDevice(options); + await result.fake_peripheral.setNextGATTConnectionResponse( + {code: HCI_SUCCESS}); + await result.device.gatt.connect(); + let fake_health_thermometer = + await result.fake_peripheral.addFakeService({uuid: 'health_thermometer'}); + await result.fake_peripheral.setNextGATTDiscoveryResponse( + {code: HCI_SUCCESS}); + let service = + await result.device.gatt.getPrimaryService('health_thermometer'); + return { + service: service, + fake_health_thermometer: fake_health_thermometer, + }; +} + +/** + * Similar to getHealthThermometerDevice except the GATT discovery + * response has not been set yet so more attributes can still be added. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getConnectedHealthThermometerDevice(options) { + let fake_device = await getDiscoveredHealthThermometerDevice(options); + await fake_device.fake_peripheral.setNextGATTConnectionResponse({ + code: HCI_SUCCESS, + }); + let fakes = await populateHealthThermometerFakes(fake_device); + await fake_device.device.gatt.connect(); + return Object.assign({device: fake_device.device}, fakes); +} + +/** + * Returns an object containing a BluetoothDevice discovered using |options|, + * its corresponding FakePeripheral and FakeRemoteGATTServices. + * The simulated device is called 'Health Thermometer' it has two known service + * UUIDs: 'generic_access' and 'health_thermometer' which correspond to two + * services with the same UUIDs. The 'health thermometer' service contains three + * characteristics: + * - 'temperature_measurement' (indicate), + * - 'temperature_type' (read), + * - 'measurement_interval' (read, write, indicate) + * The 'measurement_interval' characteristic contains a + * 'gatt.client_characteristic_configuration' descriptor and a + * 'characteristic_user_description' descriptor. + * The device has been connected to and its attributes are ready to be + * discovered. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getHealthThermometerDevice(options) { + let result = await getConnectedHealthThermometerDevice(options); + await result.fake_peripheral.setNextGATTDiscoveryResponse({ + code: HCI_SUCCESS, + }); + return result; +} + +/** + * Similar to getHealthThermometerDevice except that the peripheral has two + * 'health_thermometer' services. + * @param {RequestDeviceOptions} options The options for requesting a Bluetooth + * Device. + * @returns {Promise<{device: BluetoothDevice, fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, fake_health_thermometer1: + * FakeRemoteGATTService, fake_health_thermometer2: FakeRemoteGATTService}>} An + * object containing a requested Bluetooth device and two fake health + * thermometer GATT services. + */ +async function getTwoHealthThermometerServicesDevice(options) { + let result = await getConnectedHealthThermometerDevice(options); + let fake_health_thermometer2 = + await result.fake_peripheral.addFakeService({uuid: 'health_thermometer'}); + await result.fake_peripheral.setNextGATTDiscoveryResponse( + {code: HCI_SUCCESS}); + return { + device: result.device, + fake_peripheral: result.fake_peripheral, + fake_generic_access: result.fake_generic_access, + fake_health_thermometer1: result.fake_health_thermometer, + fake_health_thermometer2: fake_health_thermometer2 + }; +} + +/** + * Returns an object containing a Health Thermometer BluetoothRemoteGattService + * and its corresponding FakeRemoteGATTService. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic, + * service: BluetoothRemoteGATTService, + * fake_service: FakeRemoteGATTService}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getHealthThermometerService() { + let result = await getHealthThermometerDevice(); + let service = + await result.device.gatt.getPrimaryService('health_thermometer'); + return Object.assign(result, { + service, + fake_service: result.fake_health_thermometer, + }); +} + +/** + * Returns an object containing a Measurement Interval + * BluetoothRemoteGATTCharacteristic and its corresponding + * FakeRemoteGATTCharacteristic. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic, + * service: BluetoothRemoteGATTService, + * fake_service: FakeRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeRemoteGATTCharacteristic}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getMeasurementIntervalCharacteristic() { + let result = await getHealthThermometerService(); + let characteristic = + await result.service.getCharacteristic('measurement_interval'); + return Object.assign(result, { + characteristic, + fake_characteristic: result.fake_measurement_interval, + }); +} + +/** + * Returns an object containing a User Description + * BluetoothRemoteGATTDescriptor and its corresponding + * FakeRemoteGATTDescriptor. + * @returns {Promise<{device: BluetoothDevice, fakes: { + * fake_peripheral: FakePeripheral, + * fake_generic_access: FakeRemoteGATTService, + * fake_health_thermometer: FakeRemoteGATTService, + * fake_measurement_interval: FakeRemoteGATTCharacteristic, + * fake_cccd: FakeRemoteGATTDescriptor, + * fake_user_description: FakeRemoteGATTDescriptor, + * fake_temperature_measurement: FakeRemoteGATTCharacteristic, + * fake_temperature_type: FakeRemoteGATTCharacteristic, + * service: BluetoothRemoteGATTService, + * fake_service: FakeRemoteGATTService, + * characteristic: BluetoothRemoteGATTCharacteristic, + * fake_characteristic: FakeRemoteGATTCharacteristic + * descriptor: BluetoothRemoteGATTDescriptor, + * fake_descriptor: FakeRemoteGATTDescriptor}}>} An object + * containing a requested BluetoothDevice and all of the GATT fake + * objects. + */ +async function getUserDescriptionDescriptor() { + let result = await getMeasurementIntervalCharacteristic(); + let descriptor = await result.characteristic.getDescriptor( + 'gatt.characteristic_user_description'); + return Object.assign(result, { + descriptor, + fake_descriptor: result.fake_user_description, + }); +} + +/** Heart Rate Bluetooth Device Helper Methods */ + +/** @type {FakeDeviceOptions} */ +const heartRateFakeDeviceOptionsDefault = { + address: '08:08:08:08:08:08', + name: 'Heart Rate', + knownServiceUUIDs: ['generic_access', 'heart_rate'], + connectable: false, + serviceDiscoveryComplete: false, +}; + +/** @type {RequestDeviceOptions} */ +const heartRateRequestDeviceOptionsDefault = { + filters: [{services: ['heart_rate']}] +}; + +async function getHeartRateDevice(setupOptionsOverride) { + let setupOptions = createSetupOptions( + {fakeDeviceOptions: heartRateFakeDeviceOptionsDefault}, + setupOptionsOverride); + return await setUpPreconnectedFakeDevice(setupOptions); +} + +/** + * Returns an array containing two FakePeripherals corresponding + * to the simulated devices. + * @returns {Promise>} The device fakes initialized as + * Health Thermometer and Heart Rate devices. + */ +async function setUpHealthThermometerAndHeartRateDevices() { + await initializeFakeCentral({state: 'powered-on'}); + return Promise.all([ + fake_central.simulatePreconnectedPeripheral({ + address: '09:09:09:09:09:09', + name: 'Health Thermometer', + manufacturerData: {}, + knownServiceUUIDs: ['generic_access', 'health_thermometer'], + }), + fake_central.simulatePreconnectedPeripheral({ + address: '08:08:08:08:08:08', + name: 'Heart Rate', + manufacturerData: {}, + knownServiceUUIDs: ['generic_access', 'heart_rate'], + }) + ]); +} -- cgit v1.2.3