/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ // https://github.com/mozilla-b2g/platform_external_qemu/blob/master/vl-android.c#L765 // static int bt_hci_parse(const char *str) { // ... // bdaddr.b[0] = 0x52; // bdaddr.b[1] = 0x54; // bdaddr.b[2] = 0x00; // bdaddr.b[3] = 0x12; // bdaddr.b[4] = 0x34; // bdaddr.b[5] = 0x56 + nb_hcis; const EMULATOR_ADDRESS = "56:34:12:00:54:52"; // $ adb shell hciconfig /dev/ttyS2 name // hci0: Type: BR/EDR Bus: UART // BD Address: 56:34:12:00:54:52 ACL MTU: 512:1 SCO MTU: 0:0 // Name: 'Full Android on Emulator' const EMULATOR_NAME = "Full Android on Emulator"; // $ adb shell hciconfig /dev/ttyS2 class // hci0: Type: BR/EDR Bus: UART // BD Address: 56:34:12:00:54:52 ACL MTU: 512:1 SCO MTU: 0:0 // Class: 0x58020c // Service Classes: Capturing, Object Transfer, Telephony // Device Class: Phone, Smart phone const EMULATOR_CLASS = 0x58020c; // Use same definition in QEMU for special bluetooth address, // which were defined at external/qemu/hw/bt.h: const BDADDR_ANY = "00:00:00:00:00:00"; const BDADDR_ALL = "ff:ff:ff:ff:ff:ff"; const BDADDR_LOCAL = "ff:ff:ff:00:00:00"; // A user friendly name for remote BT device. const REMOTE_DEVICE_NAME = "Remote_BT_Device"; // Emulate Promise.jsm semantics. Promise.defer = function() { return new Deferred(); } function Deferred() { this.promise = new Promise(function(resolve, reject) { this.resolve = resolve; this.reject = reject; }.bind(this)); Object.freeze(this); } var bluetoothManager; var pendingEmulatorCmdCount = 0; /** * Push required permissions and test if |navigator.mozBluetooth| exists. * Resolve if it does, reject otherwise. * * Fulfill params: * bluetoothManager -- an reference to navigator.mozBluetooth. * Reject params: (none) * * @param aPermissions * Additional permissions to push before any test cases. Could be either * a string or an array of strings. * * @return A deferred promise. */ function ensureBluetoothManager(aPermissions) { let deferred = Promise.defer(); let permissions = ["bluetooth"]; if (aPermissions) { if (Array.isArray(aPermissions)) { permissions = permissions.concat(aPermissions); } else if (typeof aPermissions == "string") { permissions.push(aPermissions); } } let obj = []; for (let perm of permissions) { obj.push({ "type": perm, "allow": 1, "context": document, }); } SpecialPowers.pushPermissions(obj, function() { ok(true, "permissions pushed: " + JSON.stringify(permissions)); bluetoothManager = window.navigator.mozBluetooth; log("navigator.mozBluetooth is " + (bluetoothManager ? "available" : "unavailable")); if (bluetoothManager instanceof BluetoothManager) { deferred.resolve(bluetoothManager); } else { deferred.reject(); } }); return deferred.promise; } /** * Send emulator command with safe guard. * * We should only call |finish()| after all emulator command transactions * end, so here comes with the pending counter. Resolve when the emulator * gives positive response, and reject otherwise. * * Fulfill params: * result -- an array of emulator response lines. * * Reject params: * result -- an array of emulator response lines. * * @return A deferred promise. */ function runEmulatorCmdSafe(aCommand) { let deferred = Promise.defer(); ++pendingEmulatorCmdCount; runEmulatorCmd(aCommand, function(aResult) { --pendingEmulatorCmdCount; ok(true, "Emulator response: " + JSON.stringify(aResult)); if (Array.isArray(aResult) && aResult[aResult.length - 1] === "OK") { deferred.resolve(aResult); } else { ok(false, "Got an abnormal response from emulator."); log("Fail to execute emulator command: [" + aCommand + "]"); deferred.reject(aResult); } }); return deferred.promise; } /** * Add a Bluetooth remote device to scatternet and set its properties. * * Use QEMU command 'bt remote add' to add a virtual Bluetooth remote * and set its properties by setEmulatorDeviceProperty(). * * Fulfill params: * result -- bluetooth address of the remote device. * Reject params: (none) * * @param aProperies * A javascript object with zero or several properties for initializing * the remote device. By now, the properies could be 'name' or * 'discoverable'. It valid to put a null object or a javascript object * which don't have any properies. * * @return A promise object. */ function addEmulatorRemoteDevice(aProperties) { let address; let promise = runEmulatorCmdSafe("bt remote add") .then(function(aResults) { address = aResults[0].toUpperCase(); }); for (let key in aProperties) { let value = aProperties[key]; let propertyName = key; promise = promise.then(function() { return setEmulatorDeviceProperty(address, propertyName, value); }); } return promise.then(function() { return address; }); } /** * Remove Bluetooth remote devices in scatternet. * * Use QEMU command 'bt remote remove ' to remove a specific virtual * Bluetooth remote device in scatternet or remove them all by QEMU command * 'bt remote remove BDADDR_ALL'. * * @param aAddress * The string of Bluetooth address with format xx:xx:xx:xx:xx:xx. * * Fulfill params: * result -- an array of emulator response lines. * Reject params: (none) * * @return A promise object. */ function removeEmulatorRemoteDevice(aAddress) { let cmd = "bt remote remove " + aAddress; return runEmulatorCmdSafe(cmd) .then(function(aResults) { // 'bt remote remove ' returns a list of removed device one at a line. // The last item is "OK". return aResults.slice(0, -1); }); } /** * Set a property for a Bluetooth device. * * Use QEMU command 'bt property ' to set property. * * Fulfill params: * result -- an array of emulator response lines. * Reject params: * result -- an array of emulator response lines. * * @param aAddress * The string of Bluetooth address with format xx:xx:xx:xx:xx:xx. * @param aPropertyName * The property name of Bluetooth device. * @param aValue * The new value of the specifc property. * * @return A deferred promise. */ function setEmulatorDeviceProperty(aAddress, aPropertyName, aValue) { let cmd = "bt property " + aAddress + " " + aPropertyName + " " + aValue; return runEmulatorCmdSafe(cmd); } /** * Get a property from a Bluetooth device. * * Use QEMU command 'bt property ' to get properties. * * Fulfill params: * result -- a string with format : * Reject params: * result -- an array of emulator response lines. * * @param aAddress * The string of Bluetooth address with format xx:xx:xx:xx:xx:xx. * @param aPropertyName * The property name of Bluetooth device. * * @return A deferred promise. */ function getEmulatorDeviceProperty(aAddress, aPropertyName) { let cmd = "bt property " + aAddress + " " + aPropertyName; return runEmulatorCmdSafe(cmd) .then(function(aResults) { return aResults[0]; }); } /** * Get mozSettings value specified by @aKey. * * Resolve if that mozSettings value is retrieved successfully, reject * otherwise. * * Fulfill params: * The corresponding mozSettings value of the key. * Reject params: (none) * * @param aKey * A string. * * @return A deferred promise. */ function getSettings(aKey) { let deferred = Promise.defer(); let request = navigator.mozSettings.createLock().get(aKey); request.addEventListener("success", function(aEvent) { ok(true, "getSettings(" + aKey + ")"); deferred.resolve(aEvent.target.result[aKey]); }); request.addEventListener("error", function() { ok(false, "getSettings(" + aKey + ")"); deferred.reject(); }); return deferred.promise; } /** * Set mozSettings values. * * Resolve if that mozSettings value is set successfully, reject otherwise. * * Fulfill params: (none) * Reject params: (none) * * @param aSettings * An object of format |{key1: value1, key2: value2, ...}|. * * @return A deferred promise. */ function setSettings(aSettings) { let lock = window.navigator.mozSettings.createLock(); let request = lock.set(aSettings); let deferred = Promise.defer(); lock.onsettingstransactionsuccess = function () { ok(true, "setSettings(" + JSON.stringify(aSettings) + ")"); deferred.resolve(); }; lock.onsettingstransactionfailure = function (aEvent) { ok(false, "setSettings(" + JSON.stringify(aSettings) + ")"); deferred.reject(); }; return deferred.promise; } /** * Get the boolean value which indicates defaultAdapter of bluetooth is enabled. * * Resolve if that defaultAdapter is enabled * * Fulfill params: * A boolean value. * Reject params: (none) * * @return A deferred promise. */ function getBluetoothEnabled() { log("bluetoothManager.defaultAdapter.state: " + bluetoothManager.defaultAdapter.state); return (bluetoothManager.defaultAdapter.state == "enabled"); } /** * Set mozSettings value of 'bluetooth.enabled'. * * Resolve if that mozSettings value is set successfully, reject otherwise. * * Fulfill params: (none) * Reject params: (none) * * @param aEnabled * A boolean value. * * @return A deferred promise. */ function setBluetoothEnabled(aEnabled) { let obj = {}; obj["bluetooth.enabled"] = aEnabled; return setSettings(obj); } /** * Wait for one named BluetoothManager event. * * Resolve if that named event occurs. Never reject. * * Fulfill params: the DOMEvent passed. * * @param aEventName * The name of the EventHandler. * * @return A deferred promise. */ function waitForManagerEvent(aEventName) { let deferred = Promise.defer(); bluetoothManager.addEventListener(aEventName, function onevent(aEvent) { bluetoothManager.removeEventListener(aEventName, onevent); ok(true, "BluetoothManager event '" + aEventName + "' got."); deferred.resolve(aEvent); }); return deferred.promise; } /** * Wait for one named BluetoothAdapter event. * * Resolve if that named event occurs. Never reject. * * Fulfill params: the DOMEvent passed. * * @param aAdapter * The BluetoothAdapter you want to use. * @param aEventName * The name of the EventHandler. * * @return A deferred promise. */ function waitForAdapterEvent(aAdapter, aEventName) { let deferred = Promise.defer(); aAdapter.addEventListener(aEventName, function onevent(aEvent) { aAdapter.removeEventListener(aEventName, onevent); ok(true, "BluetoothAdapter event '" + aEventName + "' got."); deferred.resolve(aEvent); }); return deferred.promise; } /** * Wait for 'onattributechanged' events for state changes of BluetoothAdapter * with specified order. * * Resolve if those expected events occur in order. Never reject. * * Fulfill params: an array which contains every changed attributes during * the waiting. * * @param aAdapter * The BluetoothAdapter you want to use. * @param aStateChangesInOrder * An array which contains an expected order of BluetoothAdapterState. * Example 1: [enabling, enabled] * Example 2: [disabling, disabled] * * @return A deferred promise. */ function waitForAdapterStateChanged(aAdapter, aStateChangesInOrder) { let deferred = Promise.defer(); let stateIndex = 0; let prevStateIndex = 0; let statesArray = []; let changedAttrs = []; aAdapter.onattributechanged = function(aEvent) { for (let i in aEvent.attrs) { changedAttrs.push(aEvent.attrs[i]); switch (aEvent.attrs[i]) { case "state": log(" 'state' changed to " + aAdapter.state); // Received state change order may differ from expected one even though // state changes in expected order, because the value of state may change // again before we receive prior 'onattributechanged' event. // // For example, expected state change order [A,B,C] may result in // received ones: // - [A,C,C] if state becomes C before we receive 2nd 'onattributechanged' // - [B,B,C] if state becomes B before we receive 1st 'onattributechanged' // - [C,C,C] if state becomes C before we receive 1st 'onattributechanged' // - [A,B,C] if all 'onattributechanged' are received in perfect timing // // As a result, we ensure only following conditions instead of exactly // matching received and expected state change order. // - Received state change order never reverse expected one. For example, // [B,A,C] should never occur with expected state change order [A,B,C]. // - The changed value of state in received state change order never // appears later than that in expected one. For example, [A,A,C] should // never occur with expected state change order [A,B,C]. let stateIndex = aStateChangesInOrder.indexOf(aAdapter.state); if (stateIndex >= prevStateIndex && stateIndex + 1 > statesArray.length) { statesArray.push(aAdapter.state); prevStateIndex = stateIndex; if (statesArray.length == aStateChangesInOrder.length) { aAdapter.onattributechanged = null; ok(true, "BluetoothAdapter event 'onattributechanged' got."); deferred.resolve(changedAttrs); } } else { ok(false, "The order of 'onattributechanged' events is unexpected."); } break; case "name": log(" 'name' changed to " + aAdapter.name); if (aAdapter.state == "enabling") { isnot(aAdapter.name, "", "adapter.name"); } else if (aAdapter.state == "disabling") { is(aAdapter.name, "", "adapter.name"); } break; case "address": log(" 'address' changed to " + aAdapter.address); if (aAdapter.state == "enabling") { isnot(aAdapter.address, "", "adapter.address"); } else if (aAdapter.state == "disabling") { is(aAdapter.address, "", "adapter.address"); } break; case "discoverable": log(" 'discoverable' changed to " + aAdapter.discoverable); if (aAdapter.state == "enabling") { is(aAdapter.discoverable, true, "adapter.discoverable"); } else if (aAdapter.state == "disabling") { is(aAdapter.discoverable, false, "adapter.discoverable"); } break; case "discovering": log(" 'discovering' changed to " + aAdapter.discovering); if (aAdapter.state == "enabling") { is(aAdapter.discovering, true, "adapter.discovering"); } else if (aAdapter.state == "disabling") { is(aAdapter.discovering, false, "adapter.discovering"); } break; case "unknown": default: ok(false, "Unknown attribute '" + aEvent.attrs[i] + "' changed." ); break; } } }; return deferred.promise; } /** * Wait for an 'onattributechanged' event for a specified attribute and compare * the new value with the expected value. * * Resolve if the specified event occurs. Never reject. * * Fulfill params: a BluetoothAttributeEvent with property attrs that contains * changed BluetoothAdapterAttributes. * * @param aAdapter * The BluetoothAdapter you want to use. * @param aAttrName * The name of the attribute of adapter. * @param aExpectedValue * The expected new value of the attribute. * * @return A deferred promise. */ function waitForAdapterAttributeChanged(aAdapter, aAttrName, aExpectedValue) { let deferred = Promise.defer(); aAdapter.onattributechanged = function(aEvent) { let i = aEvent.attrs.indexOf(aAttrName); if (i >= 0) { switch (aEvent.attrs[i]) { case "state": log(" 'state' changed to " + aAdapter.state); is(aAdapter.state, aExpectedValue, "adapter.state"); break; case "name": log(" 'name' changed to " + aAdapter.name); is(aAdapter.name, aExpectedValue, "adapter.name"); break; case "address": log(" 'address' changed to " + aAdapter.address); is(aAdapter.address, aExpectedValue, "adapter.address"); break; case "discoverable": log(" 'discoverable' changed to " + aAdapter.discoverable); is(aAdapter.discoverable, aExpectedValue, "adapter.discoverable"); break; case "discovering": log(" 'discovering' changed to " + aAdapter.discovering); is(aAdapter.discovering, aExpectedValue, "adapter.discovering"); break; case "unknown": default: ok(false, "Unknown attribute '" + aAttrName + "' changed." ); break; } aAdapter.onattributechanged = null; deferred.resolve(aEvent); } }; return deferred.promise; } /** * Wait for specified number of 'devicefound' events. * * Resolve if specified number of devices has been found. Never reject. * * Fulfill params: an array which contains BluetoothDeviceEvents that we * received from the BluetoothDiscoveryHandle. * * @param aDiscoveryHandle * A BluetoothDiscoveryHandle which is used to notify application of * discovered remote bluetooth devices. * @param aExpectedNumberOfDevices * The number of remote devices we expect to discover. * * @return A deferred promise. */ function waitForDevicesFound(aDiscoveryHandle, aExpectedNumberOfDevices) { let deferred = Promise.defer(); ok(aDiscoveryHandle instanceof BluetoothDiscoveryHandle, "aDiscoveryHandle should be a BluetoothDiscoveryHandle"); let devicesArray = []; aDiscoveryHandle.ondevicefound = function onDeviceFound(aEvent) { ok(aEvent instanceof BluetoothDeviceEvent, "aEvent should be a BluetoothDeviceEvent"); devicesArray.push(aEvent); if (devicesArray.length >= aExpectedNumberOfDevices) { aDiscoveryHandle.ondevicefound = null; deferred.resolve(devicesArray); } }; return deferred.promise; } /** * Wait for 'devicefound' events of specified devices. * * Resolve if every specified device has been found. Never reject. * * Fulfill params: an array which contains the BluetoothDeviceEvents that we * expected to receive from the BluetoothDiscoveryHandle. * * @param aDiscoveryHandle * A BluetoothDiscoveryHandle which is used to notify application of * discovered remote bluetooth devices. * @param aRemoteAddresses * An array which contains addresses of remote devices. * * @return A deferred promise. */ function waitForSpecifiedDevicesFound(aDiscoveryHandle, aRemoteAddresses) { let deferred = Promise.defer(); ok(aDiscoveryHandle instanceof BluetoothDiscoveryHandle, "aDiscoveryHandle should be a BluetoothDiscoveryHandle"); let devicesArray = []; aDiscoveryHandle.ondevicefound = function onDeviceFound(aEvent) { ok(aEvent instanceof BluetoothDeviceEvent, "aEvent should be a BluetoothDeviceEvent"); if (aRemoteAddresses.indexOf(aEvent.device.address) != -1) { devicesArray.push(aEvent); } if (devicesArray.length == aRemoteAddresses.length) { aDiscoveryHandle.ondevicefound = null; ok(true, "BluetoothAdapter has found all remote devices."); deferred.resolve(devicesArray); } }; return deferred.promise; } /** * Verify the correctness of 'devicepaired' or 'deviceunpaired' events. * * Use BluetoothAdapter.getPairedDevices() to get current device array. * Resolve if the device of from 'devicepaired' event exists in device array or * the device of address from 'deviceunpaired' event has been removed from * device array. * Otherwise, reject the promise. * * Fulfill params: the device event from 'devicepaired' or 'deviceunpaired'. * * @param aAdapter * The BluetoothAdapter you want to use. * @param aDeviceEvent * The device event from "devicepaired" or "deviceunpaired". * * @return A deferred promise. */ function verifyDevicePairedUnpairedEvent(aAdapter, aDeviceEvent) { let deferred = Promise.defer(); let devices = aAdapter.getPairedDevices(); let isPromisePending = true; if (aDeviceEvent.device) { log(" - Verify 'devicepaired' event"); for (let i in devices) { if (devices[i].address == aDeviceEvent.device.address) { deferred.resolve(aDeviceEvent); isPromisePending = false; } } if (isPromisePending) { deferred.reject(aDeviceEvent); isPromisePending = false; } } else if (aDeviceEvent.address) { log(" - Verify 'deviceunpaired' event"); for (let i in devices) { if (devices[i].address == aDeviceEvent.address) { deferred.reject(aDeviceEvent); isPromisePending = false; } } if (isPromisePending) { deferred.resolve(aDeviceEvent); isPromisePending = false; } } else { log(" - Exception occurs. Unexpected aDeviceEvent properties."); deferred.reject(aDeviceEvent); isPromisePending = false; } return deferred.promise; } /** * Add event handlers for pairing request listener. * * @param aAdapter * The BluetoothAdapter you want to use. * @param aSpecifiedBdAddress (optional) * Bluetooth address of the specified remote device which adapter wants * to pair with. If aSpecifiedBdAddress is an empty string, 'null' or * 'undefined', this function accepts every pairing request. */ function addEventHandlerForPairingRequest(aAdapter, aSpecifiedBdAddress) { log(" - Add event handlers for pairing requests."); aAdapter.pairingReqs.ondisplaypasskeyreq = function onDisplayPasskeyReq(evt) { let passkey = evt.handle.passkey; // passkey to display ok(typeof passkey === 'string', "type checking for passkey."); log(" - Received 'ondisplaypasskeyreq' event with passkey: " + passkey); let device = evt.device; if (!aSpecifiedBdAddress || device.address == aSpecifiedBdAddress) { cleanupPairingListener(aAdapter.pairingReqs); } }; aAdapter.pairingReqs.onenterpincodereq = function onEnterPinCodeReq(evt) { log(" - Received 'onenterpincodereq' event."); let device = evt.device; if (!aSpecifiedBdAddress || device.address == aSpecifiedBdAddress) { // TODO: Allow user to enter pincode. let UserEnteredPinCode = "0000"; let pinCode = UserEnteredPinCode; evt.handle.setPinCode(pinCode).then( function onResolve() { log(" - 'setPinCode' resolve."); cleanupPairingListener(aAdapter.pairingReqs); }, function onReject() { log(" - 'setPinCode' reject."); cleanupPairingListener(aAdapter.pairingReqs); }); } }; aAdapter.pairingReqs.onpairingconfirmationreq = function onPairingConfirmationReq(evt) { let passkey = evt.handle.passkey; // passkey for user to confirm ok(typeof passkey === 'string', "type checking for passkey."); log(" - Received 'onpairingconfirmationreq' event with passkey: " + passkey); let device = evt.device; if (!aSpecifiedBdAddress || device.address == aSpecifiedBdAddress) { evt.handle.accept().then( function onResolve() { log(" - 'accept' resolve."); cleanupPairingListener(aAdapter.pairingReqs); }, function onReject() { log(" - 'accept' reject."); cleanupPairingListener(aAdapter.pairingReqs); }); } }; aAdapter.pairingReqs.onpairingconsentreq = function onPairingConsentReq(evt) { log(" - Received 'onpairingconsentreq' event."); let device = evt.device; if (!aSpecifiedBdAddress || device.address == aSpecifiedBdAddress) { evt.handle.accept().then( function onResolve() { log(" - 'accept' resolve."); cleanupPairingListener(aAdapter.pairingReqs); }, function onReject() { log(" - 'accept' reject."); cleanupPairingListener(aAdapter.pairingReqs); }); } }; } /** * Clean up event handlers for pairing listener. * * @param aPairingReqs * A BluetoothPairingListener with event handlers. */ function cleanupPairingListener(aPairingReqs) { aPairingReqs.ondisplaypasskeyreq = null; aPairingReqs.onenterpasskeyreq = null; aPairingReqs.onpairingconfirmationreq = null; aPairingReqs.onpairingconsentreq = null; } /** * Compare two uuid arrays to see if them are the same. * * @param aUuidArray1 * An array which contains uuid strings. * @param aUuidArray2 * Another array which contains uuid strings. * * @return A boolean value. */ function isUuidsEqual(aUuidArray1, aUuidArray2) { if (!Array.isArray(aUuidArray1) || !Array.isArray(aUuidArray2)) { return false; } if (aUuidArray1.length != aUuidArray2.length) { return false; } for (let i = 0, l = aUuidArray1.length; i < l; i++) { if (aUuidArray1[i] != aUuidArray2[i]) { return false; } } return true; } /** * Wait for pending emulator transactions and call |finish()|. */ function cleanUp() { // Use ok here so that we have at least one test run. ok(true, ":: CLEANING UP ::"); waitFor(finish, function() { return pendingEmulatorCmdCount === 0; }); } function startBluetoothTestBase(aPermissions, aTestCaseMain) { ensureBluetoothManager(aPermissions) .then(function() { log("Wait for creating bluetooth adapter..."); return waitForManagerStateChanged(bluetoothManager); }) .then(aTestCaseMain) .then(cleanUp, function() { ok(false, "Unhandled rejected promise."); cleanUp(); }); } function startBluetoothTest(aReenable, aTestCaseMain) { startBluetoothTestBase([], function() { let origEnabled, needEnable; return Promise.resolve() .then(function() { origEnabled = getBluetoothEnabled(); needEnable = !origEnabled; log("Original state of bluetooth is " + bluetoothManager.defaultAdapter.state); if (origEnabled && aReenable) { log("Disable Bluetooth ..."); needEnable = true; isnot(bluetoothManager.defaultAdapter, null, "bluetoothManager.defaultAdapter") return bluetoothManager.defaultAdapter.disable(); } }) .then(function() { if (needEnable) { log("Enable Bluetooth ..."); isnot(bluetoothManager.defaultAdapter, null, "bluetoothManager.defaultAdapter") return bluetoothManager.defaultAdapter.enable(); } }) .then(() => bluetoothManager.defaultAdapter) .then(aTestCaseMain) .then(function() { if (!origEnabled) { log("Disable Bluetooth ..."); return bluetoothManager.defaultAdapter.disable(); } }); }); } function waitForManagerStateChanged(aManager) { let deferred = Promise.defer(); aManager.onattributechanged = function(evt) { for (var i in evt.attrs) { switch (evt.attrs[i]) { case 'defaultAdapter': deferred.resolve(evt); break; default: break; } } } return deferred.promise; }