michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: * vim: sw=2 ts=2 sts=2 et filetype=javascript michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: // https://github.com/mozilla-b2g/platform_external_qemu/blob/master/vl-android.c#L765 michael@0: // static int bt_hci_parse(const char *str) { michael@0: // ... michael@0: // bdaddr.b[0] = 0x52; michael@0: // bdaddr.b[1] = 0x54; michael@0: // bdaddr.b[2] = 0x00; michael@0: // bdaddr.b[3] = 0x12; michael@0: // bdaddr.b[4] = 0x34; michael@0: // bdaddr.b[5] = 0x56 + nb_hcis; michael@0: const EMULATOR_ADDRESS = "56:34:12:00:54:52"; michael@0: michael@0: // $ adb shell hciconfig /dev/ttyS2 name michael@0: // hci0: Type: BR/EDR Bus: UART michael@0: // BD Address: 56:34:12:00:54:52 ACL MTU: 512:1 SCO MTU: 0:0 michael@0: // Name: 'Full Android on Emulator' michael@0: const EMULATOR_NAME = "Full Android on Emulator"; michael@0: michael@0: // $ adb shell hciconfig /dev/ttyS2 class michael@0: // hci0: Type: BR/EDR Bus: UART michael@0: // BD Address: 56:34:12:00:54:52 ACL MTU: 512:1 SCO MTU: 0:0 michael@0: // Class: 0x58020c michael@0: // Service Classes: Capturing, Object Transfer, Telephony michael@0: // Device Class: Phone, Smart phone michael@0: const EMULATOR_CLASS = 0x58020c; michael@0: michael@0: // Use same definition in QEMU for special bluetooth address, michael@0: // which were defined at external/qemu/hw/bt.h: michael@0: const BDADDR_ANY = "00:00:00:00:00:00"; michael@0: const BDADDR_ALL = "ff:ff:ff:ff:ff:ff"; michael@0: const BDADDR_LOCAL = "ff:ff:ff:00:00:00"; michael@0: michael@0: // A user friendly name for remote BT device. michael@0: const REMOTE_DEVICE_NAME = "Remote BT Device"; michael@0: michael@0: let Promise = michael@0: SpecialPowers.Cu.import("resource://gre/modules/Promise.jsm").Promise; michael@0: michael@0: let bluetoothManager; michael@0: michael@0: let pendingEmulatorCmdCount = 0; michael@0: michael@0: /** michael@0: * Send emulator command with safe guard. michael@0: * michael@0: * We should only call |finish()| after all emulator command transactions michael@0: * end, so here comes with the pending counter. Resolve when the emulator michael@0: * gives positive response, and reject otherwise. michael@0: * michael@0: * Fulfill params: michael@0: * result -- an array of emulator response lines. michael@0: * michael@0: * Reject params: michael@0: * result -- an array of emulator response lines. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function runEmulatorCmdSafe(aCommand) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: ++pendingEmulatorCmdCount; michael@0: runEmulatorCmd(aCommand, function(aResult) { michael@0: --pendingEmulatorCmdCount; michael@0: michael@0: ok(true, "Emulator response: " + JSON.stringify(aResult)); michael@0: if (Array.isArray(aResult) && aResult[aResult.length - 1] === "OK") { michael@0: deferred.resolve(aResult); michael@0: } else { michael@0: ok(false, "Got an abnormal response from emulator."); michael@0: log("Fail to execute emulator command: [" + aCommand + "]"); michael@0: deferred.reject(aResult); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Add a Bluetooth remote device to scatternet and set its properties. michael@0: * michael@0: * Use QEMU command 'bt remote add' to add a virtual Bluetooth remote michael@0: * and set its properties by setEmulatorDeviceProperty(). michael@0: * michael@0: * Fulfill params: michael@0: * result -- bluetooth address of the remote device. michael@0: * Reject params: (none) michael@0: * michael@0: * @param aProperies michael@0: * A javascript object with zero or several properties for initializing michael@0: * the remote device. By now, the properies could be 'name' or michael@0: * 'discoverable'. It valid to put a null object or a javascript object michael@0: * which don't have any properies. michael@0: * michael@0: * @return A promise object. michael@0: */ michael@0: function addEmulatorRemoteDevice(aProperties) { michael@0: let address; michael@0: let promise = runEmulatorCmdSafe("bt remote add") michael@0: .then(function(aResults) { michael@0: address = aResults[0].toUpperCase(); michael@0: }); michael@0: michael@0: for (let key in aProperties) { michael@0: let value = aProperties[key]; michael@0: let propertyName = key; michael@0: promise = promise.then(function() { michael@0: return setEmulatorDeviceProperty(address, propertyName, value); michael@0: }); michael@0: } michael@0: michael@0: return promise.then(function() { michael@0: return address; michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Remove Bluetooth remote devices in scatternet. michael@0: * michael@0: * Use QEMU command 'bt remote remove ' to remove a specific virtual michael@0: * Bluetooth remote device in scatternet or remove them all by QEMU command michael@0: * 'bt remote remove BDADDR_ALL'. michael@0: * michael@0: * @param aAddress michael@0: * The string of Bluetooth address with format xx:xx:xx:xx:xx:xx. michael@0: * michael@0: * Fulfill params: michael@0: * result -- an array of emulator response lines. michael@0: * Reject params: (none) michael@0: * michael@0: * @return A promise object. michael@0: */ michael@0: function removeEmulatorRemoteDevice(aAddress) { michael@0: let cmd = "bt remote remove " + aAddress; michael@0: return runEmulatorCmdSafe(cmd) michael@0: .then(function(aResults) { michael@0: // 'bt remote remove ' returns a list of removed device one at a line. michael@0: // The last item is "OK". michael@0: return aResults.slice(0, -1); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Set a property for a Bluetooth device. michael@0: * michael@0: * Use QEMU command 'bt property ' to set property. michael@0: * michael@0: * Fulfill params: michael@0: * result -- an array of emulator response lines. michael@0: * Reject params: michael@0: * result -- an array of emulator response lines. michael@0: * michael@0: * @param aAddress michael@0: * The string of Bluetooth address with format xx:xx:xx:xx:xx:xx. michael@0: * @param aPropertyName michael@0: * The property name of Bluetooth device. michael@0: * @param aValue michael@0: * The new value of the specifc property. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function setEmulatorDeviceProperty(aAddress, aPropertyName, aValue) { michael@0: let cmd = "bt property " + aAddress + " " + aPropertyName + " " + aValue; michael@0: return runEmulatorCmdSafe(cmd); michael@0: } michael@0: michael@0: /** michael@0: * Get a property from a Bluetooth device. michael@0: * michael@0: * Use QEMU command 'bt property ' to get properties. michael@0: * michael@0: * Fulfill params: michael@0: * result -- a string with format : michael@0: * Reject params: michael@0: * result -- an array of emulator response lines. michael@0: * michael@0: * @param aAddress michael@0: * The string of Bluetooth address with format xx:xx:xx:xx:xx:xx. michael@0: * @param aPropertyName michael@0: * The property name of Bluetooth device. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function getEmulatorDeviceProperty(aAddress, aPropertyName) { michael@0: let cmd = "bt property " + aAddress + " " + aPropertyName; michael@0: return runEmulatorCmdSafe(cmd) michael@0: .then(function(aResults) { michael@0: return aResults[0]; michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Start dicovering Bluetooth devices. michael@0: * michael@0: * Allows the device's adapter to start seeking for remote devices. michael@0: * michael@0: * Fulfill params: (none) michael@0: * Reject params: a DOMError michael@0: * michael@0: * @param aAdapter michael@0: * A BluetoothAdapter which is used to interact with local BT dev michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function startDiscovery(aAdapter) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let request = aAdapter.startDiscovery(); michael@0: request.onsuccess = function () { michael@0: log(" Start discovery - Success"); michael@0: // TODO (bug 892207): Make Bluetooth APIs available for 3rd party apps. michael@0: // Currently, discovering state wouldn't change immediately here. michael@0: // We would turn on this check when the redesigned API are landed. michael@0: // is(aAdapter.discovering, true, "BluetoothAdapter.discovering"); michael@0: deferred.resolve(); michael@0: } michael@0: request.onerror = function (aEvent) { michael@0: ok(false, "Start discovery - Fail"); michael@0: deferred.reject(aEvent.target.error); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Stop dicovering Bluetooth devices. michael@0: * michael@0: * Allows the device's adapter to stop seeking for remote devices. michael@0: * michael@0: * Fulfill params: (none) michael@0: * Reject params: a DOMError michael@0: * michael@0: * @param aAdapter michael@0: * A BluetoothAdapter which is used to interact with local BT device. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function stopDiscovery(aAdapter) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let request = aAdapter.stopDiscovery(); michael@0: request.onsuccess = function () { michael@0: log(" Stop discovery - Success"); michael@0: // TODO (bug 892207): Make Bluetooth APIs available for 3rd party apps. michael@0: // Currently, discovering state wouldn't change immediately here. michael@0: // We would turn on this check when the redesigned API are landed. michael@0: // is(aAdapter.discovering, false, "BluetoothAdapter.discovering"); michael@0: deferred.resolve(); michael@0: } michael@0: request.onerror = function (aEvent) { michael@0: ok(false, "Stop discovery - Fail"); michael@0: deferred.reject(aEvent.target.error); michael@0: } michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Get mozSettings value specified by @aKey. michael@0: * michael@0: * Resolve if that mozSettings value is retrieved successfully, reject michael@0: * otherwise. michael@0: * michael@0: * Fulfill params: michael@0: * The corresponding mozSettings value of the key. michael@0: * Reject params: (none) michael@0: * michael@0: * @param aKey michael@0: * A string. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function getSettings(aKey) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let request = navigator.mozSettings.createLock().get(aKey); michael@0: request.addEventListener("success", function(aEvent) { michael@0: ok(true, "getSettings(" + aKey + ")"); michael@0: deferred.resolve(aEvent.target.result[aKey]); michael@0: }); michael@0: request.addEventListener("error", function() { michael@0: ok(false, "getSettings(" + aKey + ")"); michael@0: deferred.reject(); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Set mozSettings values. michael@0: * michael@0: * Resolve if that mozSettings value is set successfully, reject otherwise. michael@0: * michael@0: * Fulfill params: (none) michael@0: * Reject params: (none) michael@0: * michael@0: * @param aSettings michael@0: * An object of format |{key1: value1, key2: value2, ...}|. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function setSettings(aSettings) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let request = navigator.mozSettings.createLock().set(aSettings); michael@0: request.addEventListener("success", function() { michael@0: ok(true, "setSettings(" + JSON.stringify(aSettings) + ")"); michael@0: deferred.resolve(); michael@0: }); michael@0: request.addEventListener("error", function() { michael@0: ok(false, "setSettings(" + JSON.stringify(aSettings) + ")"); michael@0: deferred.reject(); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Get mozSettings value of 'bluetooth.enabled'. michael@0: * michael@0: * Resolve if that mozSettings value is retrieved successfully, reject michael@0: * otherwise. michael@0: * michael@0: * Fulfill params: michael@0: * A boolean value. michael@0: * Reject params: (none) michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function getBluetoothEnabled() { michael@0: return getSettings("bluetooth.enabled"); michael@0: } michael@0: michael@0: /** michael@0: * Set mozSettings value of 'bluetooth.enabled'. michael@0: * michael@0: * Resolve if that mozSettings value is set successfully, reject otherwise. michael@0: * michael@0: * Fulfill params: (none) michael@0: * Reject params: (none) michael@0: * michael@0: * @param aEnabled michael@0: * A boolean value. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function setBluetoothEnabled(aEnabled) { michael@0: let obj = {}; michael@0: obj["bluetooth.enabled"] = aEnabled; michael@0: return setSettings(obj); michael@0: } michael@0: michael@0: /** michael@0: * Push required permissions and test if |navigator.mozBluetooth| exists. michael@0: * Resolve if it does, reject otherwise. michael@0: * michael@0: * Fulfill params: michael@0: * bluetoothManager -- an reference to navigator.mozBluetooth. michael@0: * Reject params: (none) michael@0: * michael@0: * @param aPermissions michael@0: * Additional permissions to push before any test cases. Could be either michael@0: * a string or an array of strings. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function ensureBluetoothManager(aPermissions) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let permissions = ["bluetooth"]; michael@0: if (aPermissions) { michael@0: if (Array.isArray(aPermissions)) { michael@0: permissions = permissions.concat(aPermissions); michael@0: } else if (typeof aPermissions == "string") { michael@0: permissions.push(aPermissions); michael@0: } michael@0: } michael@0: michael@0: let obj = []; michael@0: for (let perm of permissions) { michael@0: obj.push({ michael@0: "type": perm, michael@0: "allow": 1, michael@0: "context": document, michael@0: }); michael@0: } michael@0: michael@0: SpecialPowers.pushPermissions(obj, function() { michael@0: ok(true, "permissions pushed: " + JSON.stringify(permissions)); michael@0: michael@0: bluetoothManager = window.navigator.mozBluetooth; michael@0: log("navigator.mozBluetooth is " + michael@0: (bluetoothManager ? "available" : "unavailable")); michael@0: michael@0: if (bluetoothManager instanceof BluetoothManager) { michael@0: deferred.resolve(bluetoothManager); michael@0: } else { michael@0: deferred.reject(); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Wait for one named BluetoothManager event. michael@0: * michael@0: * Resolve if that named event occurs. Never reject. michael@0: * michael@0: * Fulfill params: the DOMEvent passed. michael@0: * michael@0: * @param aEventName michael@0: * The name of the EventHandler. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function waitForManagerEvent(aEventName) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: bluetoothManager.addEventListener(aEventName, function onevent(aEvent) { michael@0: bluetoothManager.removeEventListener(aEventName, onevent); michael@0: michael@0: ok(true, "BluetoothManager event '" + aEventName + "' got."); michael@0: deferred.resolve(aEvent); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Wait for one named BluetoothAdapter event. michael@0: * michael@0: * Resolve if that named event occurs. Never reject. michael@0: * michael@0: * Fulfill params: the DOMEvent passed. michael@0: * michael@0: * @param aAdapter michael@0: * The BluetoothAdapter you want to use. michael@0: * @param aEventName michael@0: * The name of the EventHandler. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function waitForAdapterEvent(aAdapter, aEventName) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: aAdapter.addEventListener(aEventName, function onevent(aEvent) { michael@0: aAdapter.removeEventListener(aEventName, onevent); michael@0: michael@0: ok(true, "BluetoothAdapter event '" + aEventName + "' got."); michael@0: deferred.resolve(aEvent); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Convenient function for setBluetoothEnabled and waitForManagerEvent michael@0: * combined. michael@0: * michael@0: * Resolve if that named event occurs. Reject if we can't set settings. michael@0: * michael@0: * Fulfill params: the DOMEvent passed. michael@0: * Reject params: (none) michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function setBluetoothEnabledAndWait(aEnabled) { michael@0: let promises = []; michael@0: michael@0: // Bug 969109 - Intermittent test_dom_BluetoothManager_adapteradded.js michael@0: // michael@0: // Here we want to wait for two events coming up -- Bluetooth "settings-set" michael@0: // event and one of "enabled"/"disabled" events. Special care is taken here michael@0: // to ensure that we can always receive that "enabled"/"disabled" event by michael@0: // installing the event handler *before* we ever enable/disable Bluetooth. Or michael@0: // we might just miss those events and get a timeout error. michael@0: promises.push(waitForManagerEvent(aEnabled ? "enabled" : "disabled")); michael@0: promises.push(setBluetoothEnabled(aEnabled)); michael@0: michael@0: return Promise.all(promises); michael@0: } michael@0: michael@0: /** michael@0: * Get default adapter. michael@0: * michael@0: * Resolve if that default adapter is got, reject otherwise. michael@0: * michael@0: * Fulfill params: a BluetoothAdapter instance. michael@0: * Reject params: a DOMError, or null if if there is no adapter ready yet. michael@0: * michael@0: * @return A deferred promise. michael@0: */ michael@0: function getDefaultAdapter() { michael@0: let deferred = Promise.defer(); michael@0: michael@0: let request = bluetoothManager.getDefaultAdapter(); michael@0: request.onsuccess = function(aEvent) { michael@0: let adapter = aEvent.target.result; michael@0: if (!(adapter instanceof BluetoothAdapter)) { michael@0: ok(false, "no BluetoothAdapter ready yet."); michael@0: deferred.reject(null); michael@0: return; michael@0: } michael@0: michael@0: ok(true, "BluetoothAdapter got."); michael@0: // TODO: We have an adapter instance now, but some of its attributes may michael@0: // still remain unassigned/out-dated. Here we waste a few seconds to michael@0: // wait for the property changed events. michael@0: // michael@0: // See https://bugzilla.mozilla.org/show_bug.cgi?id=932914 michael@0: window.setTimeout(function() { michael@0: deferred.resolve(adapter); michael@0: }, 3000); michael@0: }; michael@0: request.onerror = function(aEvent) { michael@0: ok(false, "Failed to get default adapter."); michael@0: deferred.reject(aEvent.target.error); michael@0: }; michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Flush permission settings and call |finish()|. michael@0: */ michael@0: function cleanUp() { michael@0: waitFor(function() { michael@0: SpecialPowers.flushPermissions(function() { michael@0: // Use ok here so that we have at least one test run. michael@0: ok(true, "permissions flushed"); michael@0: michael@0: finish(); michael@0: }); michael@0: }, function() { michael@0: return pendingEmulatorCmdCount === 0; michael@0: }); michael@0: } michael@0: michael@0: function startBluetoothTestBase(aPermissions, aTestCaseMain) { michael@0: ensureBluetoothManager(aPermissions) michael@0: .then(aTestCaseMain) michael@0: .then(cleanUp, function() { michael@0: ok(false, "Unhandled rejected promise."); michael@0: cleanUp(); michael@0: }); michael@0: } michael@0: michael@0: function startBluetoothTest(aReenable, aTestCaseMain) { michael@0: startBluetoothTestBase(["settings-read", "settings-write"], function() { michael@0: let origEnabled, needEnable; michael@0: michael@0: return getBluetoothEnabled() michael@0: .then(function(aEnabled) { michael@0: origEnabled = aEnabled; michael@0: needEnable = !aEnabled; michael@0: log("Original 'bluetooth.enabled' is " + origEnabled); michael@0: michael@0: if (aEnabled && aReenable) { michael@0: log(" Disable 'bluetooth.enabled' ..."); michael@0: needEnable = true; michael@0: return setBluetoothEnabledAndWait(false); michael@0: } michael@0: }) michael@0: .then(function() { michael@0: if (needEnable) { michael@0: log(" Enable 'bluetooth.enabled' ..."); michael@0: michael@0: // See setBluetoothEnabledAndWait(). We must install all event michael@0: // handlers *before* enabling Bluetooth. michael@0: let promises = []; michael@0: promises.push(waitForManagerEvent("adapteradded")); michael@0: promises.push(setBluetoothEnabledAndWait(true)); michael@0: return Promise.all(promises); michael@0: } michael@0: }) michael@0: .then(getDefaultAdapter) michael@0: .then(aTestCaseMain) michael@0: .then(function() { michael@0: if (!origEnabled) { michael@0: return setBluetoothEnabledAndWait(false); michael@0: } michael@0: }); michael@0: }); michael@0: }