|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- |
|
2 * vim: sw=2 ts=2 sts=2 et filetype=javascript |
|
3 * This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 // https://github.com/mozilla-b2g/platform_external_qemu/blob/master/vl-android.c#L765 |
|
8 // static int bt_hci_parse(const char *str) { |
|
9 // ... |
|
10 // bdaddr.b[0] = 0x52; |
|
11 // bdaddr.b[1] = 0x54; |
|
12 // bdaddr.b[2] = 0x00; |
|
13 // bdaddr.b[3] = 0x12; |
|
14 // bdaddr.b[4] = 0x34; |
|
15 // bdaddr.b[5] = 0x56 + nb_hcis; |
|
16 const EMULATOR_ADDRESS = "56:34:12:00:54:52"; |
|
17 |
|
18 // $ adb shell hciconfig /dev/ttyS2 name |
|
19 // hci0: Type: BR/EDR Bus: UART |
|
20 // BD Address: 56:34:12:00:54:52 ACL MTU: 512:1 SCO MTU: 0:0 |
|
21 // Name: 'Full Android on Emulator' |
|
22 const EMULATOR_NAME = "Full Android on Emulator"; |
|
23 |
|
24 // $ adb shell hciconfig /dev/ttyS2 class |
|
25 // hci0: Type: BR/EDR Bus: UART |
|
26 // BD Address: 56:34:12:00:54:52 ACL MTU: 512:1 SCO MTU: 0:0 |
|
27 // Class: 0x58020c |
|
28 // Service Classes: Capturing, Object Transfer, Telephony |
|
29 // Device Class: Phone, Smart phone |
|
30 const EMULATOR_CLASS = 0x58020c; |
|
31 |
|
32 // Use same definition in QEMU for special bluetooth address, |
|
33 // which were defined at external/qemu/hw/bt.h: |
|
34 const BDADDR_ANY = "00:00:00:00:00:00"; |
|
35 const BDADDR_ALL = "ff:ff:ff:ff:ff:ff"; |
|
36 const BDADDR_LOCAL = "ff:ff:ff:00:00:00"; |
|
37 |
|
38 // A user friendly name for remote BT device. |
|
39 const REMOTE_DEVICE_NAME = "Remote BT Device"; |
|
40 |
|
41 let Promise = |
|
42 SpecialPowers.Cu.import("resource://gre/modules/Promise.jsm").Promise; |
|
43 |
|
44 let bluetoothManager; |
|
45 |
|
46 let pendingEmulatorCmdCount = 0; |
|
47 |
|
48 /** |
|
49 * Send emulator command with safe guard. |
|
50 * |
|
51 * We should only call |finish()| after all emulator command transactions |
|
52 * end, so here comes with the pending counter. Resolve when the emulator |
|
53 * gives positive response, and reject otherwise. |
|
54 * |
|
55 * Fulfill params: |
|
56 * result -- an array of emulator response lines. |
|
57 * |
|
58 * Reject params: |
|
59 * result -- an array of emulator response lines. |
|
60 * |
|
61 * @return A deferred promise. |
|
62 */ |
|
63 function runEmulatorCmdSafe(aCommand) { |
|
64 let deferred = Promise.defer(); |
|
65 |
|
66 ++pendingEmulatorCmdCount; |
|
67 runEmulatorCmd(aCommand, function(aResult) { |
|
68 --pendingEmulatorCmdCount; |
|
69 |
|
70 ok(true, "Emulator response: " + JSON.stringify(aResult)); |
|
71 if (Array.isArray(aResult) && aResult[aResult.length - 1] === "OK") { |
|
72 deferred.resolve(aResult); |
|
73 } else { |
|
74 ok(false, "Got an abnormal response from emulator."); |
|
75 log("Fail to execute emulator command: [" + aCommand + "]"); |
|
76 deferred.reject(aResult); |
|
77 } |
|
78 }); |
|
79 |
|
80 return deferred.promise; |
|
81 } |
|
82 |
|
83 /** |
|
84 * Add a Bluetooth remote device to scatternet and set its properties. |
|
85 * |
|
86 * Use QEMU command 'bt remote add' to add a virtual Bluetooth remote |
|
87 * and set its properties by setEmulatorDeviceProperty(). |
|
88 * |
|
89 * Fulfill params: |
|
90 * result -- bluetooth address of the remote device. |
|
91 * Reject params: (none) |
|
92 * |
|
93 * @param aProperies |
|
94 * A javascript object with zero or several properties for initializing |
|
95 * the remote device. By now, the properies could be 'name' or |
|
96 * 'discoverable'. It valid to put a null object or a javascript object |
|
97 * which don't have any properies. |
|
98 * |
|
99 * @return A promise object. |
|
100 */ |
|
101 function addEmulatorRemoteDevice(aProperties) { |
|
102 let address; |
|
103 let promise = runEmulatorCmdSafe("bt remote add") |
|
104 .then(function(aResults) { |
|
105 address = aResults[0].toUpperCase(); |
|
106 }); |
|
107 |
|
108 for (let key in aProperties) { |
|
109 let value = aProperties[key]; |
|
110 let propertyName = key; |
|
111 promise = promise.then(function() { |
|
112 return setEmulatorDeviceProperty(address, propertyName, value); |
|
113 }); |
|
114 } |
|
115 |
|
116 return promise.then(function() { |
|
117 return address; |
|
118 }); |
|
119 } |
|
120 |
|
121 /** |
|
122 * Remove Bluetooth remote devices in scatternet. |
|
123 * |
|
124 * Use QEMU command 'bt remote remove <addr>' to remove a specific virtual |
|
125 * Bluetooth remote device in scatternet or remove them all by QEMU command |
|
126 * 'bt remote remove BDADDR_ALL'. |
|
127 * |
|
128 * @param aAddress |
|
129 * The string of Bluetooth address with format xx:xx:xx:xx:xx:xx. |
|
130 * |
|
131 * Fulfill params: |
|
132 * result -- an array of emulator response lines. |
|
133 * Reject params: (none) |
|
134 * |
|
135 * @return A promise object. |
|
136 */ |
|
137 function removeEmulatorRemoteDevice(aAddress) { |
|
138 let cmd = "bt remote remove " + aAddress; |
|
139 return runEmulatorCmdSafe(cmd) |
|
140 .then(function(aResults) { |
|
141 // 'bt remote remove <bd_addr>' returns a list of removed device one at a line. |
|
142 // The last item is "OK". |
|
143 return aResults.slice(0, -1); |
|
144 }); |
|
145 } |
|
146 |
|
147 /** |
|
148 * Set a property for a Bluetooth device. |
|
149 * |
|
150 * Use QEMU command 'bt property <bd_addr> <prop_name> <value>' to set property. |
|
151 * |
|
152 * Fulfill params: |
|
153 * result -- an array of emulator response lines. |
|
154 * Reject params: |
|
155 * result -- an array of emulator response lines. |
|
156 * |
|
157 * @param aAddress |
|
158 * The string of Bluetooth address with format xx:xx:xx:xx:xx:xx. |
|
159 * @param aPropertyName |
|
160 * The property name of Bluetooth device. |
|
161 * @param aValue |
|
162 * The new value of the specifc property. |
|
163 * |
|
164 * @return A deferred promise. |
|
165 */ |
|
166 function setEmulatorDeviceProperty(aAddress, aPropertyName, aValue) { |
|
167 let cmd = "bt property " + aAddress + " " + aPropertyName + " " + aValue; |
|
168 return runEmulatorCmdSafe(cmd); |
|
169 } |
|
170 |
|
171 /** |
|
172 * Get a property from a Bluetooth device. |
|
173 * |
|
174 * Use QEMU command 'bt property <bd_addr> <prop_name>' to get properties. |
|
175 * |
|
176 * Fulfill params: |
|
177 * result -- a string with format <prop_name>: <value_of_prop> |
|
178 * Reject params: |
|
179 * result -- an array of emulator response lines. |
|
180 * |
|
181 * @param aAddress |
|
182 * The string of Bluetooth address with format xx:xx:xx:xx:xx:xx. |
|
183 * @param aPropertyName |
|
184 * The property name of Bluetooth device. |
|
185 * |
|
186 * @return A deferred promise. |
|
187 */ |
|
188 function getEmulatorDeviceProperty(aAddress, aPropertyName) { |
|
189 let cmd = "bt property " + aAddress + " " + aPropertyName; |
|
190 return runEmulatorCmdSafe(cmd) |
|
191 .then(function(aResults) { |
|
192 return aResults[0]; |
|
193 }); |
|
194 } |
|
195 |
|
196 /** |
|
197 * Start dicovering Bluetooth devices. |
|
198 * |
|
199 * Allows the device's adapter to start seeking for remote devices. |
|
200 * |
|
201 * Fulfill params: (none) |
|
202 * Reject params: a DOMError |
|
203 * |
|
204 * @param aAdapter |
|
205 * A BluetoothAdapter which is used to interact with local BT dev |
|
206 * |
|
207 * @return A deferred promise. |
|
208 */ |
|
209 function startDiscovery(aAdapter) { |
|
210 let deferred = Promise.defer(); |
|
211 |
|
212 let request = aAdapter.startDiscovery(); |
|
213 request.onsuccess = function () { |
|
214 log(" Start discovery - Success"); |
|
215 // TODO (bug 892207): Make Bluetooth APIs available for 3rd party apps. |
|
216 // Currently, discovering state wouldn't change immediately here. |
|
217 // We would turn on this check when the redesigned API are landed. |
|
218 // is(aAdapter.discovering, true, "BluetoothAdapter.discovering"); |
|
219 deferred.resolve(); |
|
220 } |
|
221 request.onerror = function (aEvent) { |
|
222 ok(false, "Start discovery - Fail"); |
|
223 deferred.reject(aEvent.target.error); |
|
224 } |
|
225 |
|
226 return deferred.promise; |
|
227 } |
|
228 |
|
229 /** |
|
230 * Stop dicovering Bluetooth devices. |
|
231 * |
|
232 * Allows the device's adapter to stop seeking for remote devices. |
|
233 * |
|
234 * Fulfill params: (none) |
|
235 * Reject params: a DOMError |
|
236 * |
|
237 * @param aAdapter |
|
238 * A BluetoothAdapter which is used to interact with local BT device. |
|
239 * |
|
240 * @return A deferred promise. |
|
241 */ |
|
242 function stopDiscovery(aAdapter) { |
|
243 let deferred = Promise.defer(); |
|
244 |
|
245 let request = aAdapter.stopDiscovery(); |
|
246 request.onsuccess = function () { |
|
247 log(" Stop discovery - Success"); |
|
248 // TODO (bug 892207): Make Bluetooth APIs available for 3rd party apps. |
|
249 // Currently, discovering state wouldn't change immediately here. |
|
250 // We would turn on this check when the redesigned API are landed. |
|
251 // is(aAdapter.discovering, false, "BluetoothAdapter.discovering"); |
|
252 deferred.resolve(); |
|
253 } |
|
254 request.onerror = function (aEvent) { |
|
255 ok(false, "Stop discovery - Fail"); |
|
256 deferred.reject(aEvent.target.error); |
|
257 } |
|
258 return deferred.promise; |
|
259 } |
|
260 |
|
261 /** |
|
262 * Get mozSettings value specified by @aKey. |
|
263 * |
|
264 * Resolve if that mozSettings value is retrieved successfully, reject |
|
265 * otherwise. |
|
266 * |
|
267 * Fulfill params: |
|
268 * The corresponding mozSettings value of the key. |
|
269 * Reject params: (none) |
|
270 * |
|
271 * @param aKey |
|
272 * A string. |
|
273 * |
|
274 * @return A deferred promise. |
|
275 */ |
|
276 function getSettings(aKey) { |
|
277 let deferred = Promise.defer(); |
|
278 |
|
279 let request = navigator.mozSettings.createLock().get(aKey); |
|
280 request.addEventListener("success", function(aEvent) { |
|
281 ok(true, "getSettings(" + aKey + ")"); |
|
282 deferred.resolve(aEvent.target.result[aKey]); |
|
283 }); |
|
284 request.addEventListener("error", function() { |
|
285 ok(false, "getSettings(" + aKey + ")"); |
|
286 deferred.reject(); |
|
287 }); |
|
288 |
|
289 return deferred.promise; |
|
290 } |
|
291 |
|
292 /** |
|
293 * Set mozSettings values. |
|
294 * |
|
295 * Resolve if that mozSettings value is set successfully, reject otherwise. |
|
296 * |
|
297 * Fulfill params: (none) |
|
298 * Reject params: (none) |
|
299 * |
|
300 * @param aSettings |
|
301 * An object of format |{key1: value1, key2: value2, ...}|. |
|
302 * |
|
303 * @return A deferred promise. |
|
304 */ |
|
305 function setSettings(aSettings) { |
|
306 let deferred = Promise.defer(); |
|
307 |
|
308 let request = navigator.mozSettings.createLock().set(aSettings); |
|
309 request.addEventListener("success", function() { |
|
310 ok(true, "setSettings(" + JSON.stringify(aSettings) + ")"); |
|
311 deferred.resolve(); |
|
312 }); |
|
313 request.addEventListener("error", function() { |
|
314 ok(false, "setSettings(" + JSON.stringify(aSettings) + ")"); |
|
315 deferred.reject(); |
|
316 }); |
|
317 |
|
318 return deferred.promise; |
|
319 } |
|
320 |
|
321 /** |
|
322 * Get mozSettings value of 'bluetooth.enabled'. |
|
323 * |
|
324 * Resolve if that mozSettings value is retrieved successfully, reject |
|
325 * otherwise. |
|
326 * |
|
327 * Fulfill params: |
|
328 * A boolean value. |
|
329 * Reject params: (none) |
|
330 * |
|
331 * @return A deferred promise. |
|
332 */ |
|
333 function getBluetoothEnabled() { |
|
334 return getSettings("bluetooth.enabled"); |
|
335 } |
|
336 |
|
337 /** |
|
338 * Set mozSettings value of 'bluetooth.enabled'. |
|
339 * |
|
340 * Resolve if that mozSettings value is set successfully, reject otherwise. |
|
341 * |
|
342 * Fulfill params: (none) |
|
343 * Reject params: (none) |
|
344 * |
|
345 * @param aEnabled |
|
346 * A boolean value. |
|
347 * |
|
348 * @return A deferred promise. |
|
349 */ |
|
350 function setBluetoothEnabled(aEnabled) { |
|
351 let obj = {}; |
|
352 obj["bluetooth.enabled"] = aEnabled; |
|
353 return setSettings(obj); |
|
354 } |
|
355 |
|
356 /** |
|
357 * Push required permissions and test if |navigator.mozBluetooth| exists. |
|
358 * Resolve if it does, reject otherwise. |
|
359 * |
|
360 * Fulfill params: |
|
361 * bluetoothManager -- an reference to navigator.mozBluetooth. |
|
362 * Reject params: (none) |
|
363 * |
|
364 * @param aPermissions |
|
365 * Additional permissions to push before any test cases. Could be either |
|
366 * a string or an array of strings. |
|
367 * |
|
368 * @return A deferred promise. |
|
369 */ |
|
370 function ensureBluetoothManager(aPermissions) { |
|
371 let deferred = Promise.defer(); |
|
372 |
|
373 let permissions = ["bluetooth"]; |
|
374 if (aPermissions) { |
|
375 if (Array.isArray(aPermissions)) { |
|
376 permissions = permissions.concat(aPermissions); |
|
377 } else if (typeof aPermissions == "string") { |
|
378 permissions.push(aPermissions); |
|
379 } |
|
380 } |
|
381 |
|
382 let obj = []; |
|
383 for (let perm of permissions) { |
|
384 obj.push({ |
|
385 "type": perm, |
|
386 "allow": 1, |
|
387 "context": document, |
|
388 }); |
|
389 } |
|
390 |
|
391 SpecialPowers.pushPermissions(obj, function() { |
|
392 ok(true, "permissions pushed: " + JSON.stringify(permissions)); |
|
393 |
|
394 bluetoothManager = window.navigator.mozBluetooth; |
|
395 log("navigator.mozBluetooth is " + |
|
396 (bluetoothManager ? "available" : "unavailable")); |
|
397 |
|
398 if (bluetoothManager instanceof BluetoothManager) { |
|
399 deferred.resolve(bluetoothManager); |
|
400 } else { |
|
401 deferred.reject(); |
|
402 } |
|
403 }); |
|
404 |
|
405 return deferred.promise; |
|
406 } |
|
407 |
|
408 /** |
|
409 * Wait for one named BluetoothManager event. |
|
410 * |
|
411 * Resolve if that named event occurs. Never reject. |
|
412 * |
|
413 * Fulfill params: the DOMEvent passed. |
|
414 * |
|
415 * @param aEventName |
|
416 * The name of the EventHandler. |
|
417 * |
|
418 * @return A deferred promise. |
|
419 */ |
|
420 function waitForManagerEvent(aEventName) { |
|
421 let deferred = Promise.defer(); |
|
422 |
|
423 bluetoothManager.addEventListener(aEventName, function onevent(aEvent) { |
|
424 bluetoothManager.removeEventListener(aEventName, onevent); |
|
425 |
|
426 ok(true, "BluetoothManager event '" + aEventName + "' got."); |
|
427 deferred.resolve(aEvent); |
|
428 }); |
|
429 |
|
430 return deferred.promise; |
|
431 } |
|
432 |
|
433 /** |
|
434 * Wait for one named BluetoothAdapter event. |
|
435 * |
|
436 * Resolve if that named event occurs. Never reject. |
|
437 * |
|
438 * Fulfill params: the DOMEvent passed. |
|
439 * |
|
440 * @param aAdapter |
|
441 * The BluetoothAdapter you want to use. |
|
442 * @param aEventName |
|
443 * The name of the EventHandler. |
|
444 * |
|
445 * @return A deferred promise. |
|
446 */ |
|
447 function waitForAdapterEvent(aAdapter, aEventName) { |
|
448 let deferred = Promise.defer(); |
|
449 |
|
450 aAdapter.addEventListener(aEventName, function onevent(aEvent) { |
|
451 aAdapter.removeEventListener(aEventName, onevent); |
|
452 |
|
453 ok(true, "BluetoothAdapter event '" + aEventName + "' got."); |
|
454 deferred.resolve(aEvent); |
|
455 }); |
|
456 |
|
457 return deferred.promise; |
|
458 } |
|
459 |
|
460 /** |
|
461 * Convenient function for setBluetoothEnabled and waitForManagerEvent |
|
462 * combined. |
|
463 * |
|
464 * Resolve if that named event occurs. Reject if we can't set settings. |
|
465 * |
|
466 * Fulfill params: the DOMEvent passed. |
|
467 * Reject params: (none) |
|
468 * |
|
469 * @return A deferred promise. |
|
470 */ |
|
471 function setBluetoothEnabledAndWait(aEnabled) { |
|
472 let promises = []; |
|
473 |
|
474 // Bug 969109 - Intermittent test_dom_BluetoothManager_adapteradded.js |
|
475 // |
|
476 // Here we want to wait for two events coming up -- Bluetooth "settings-set" |
|
477 // event and one of "enabled"/"disabled" events. Special care is taken here |
|
478 // to ensure that we can always receive that "enabled"/"disabled" event by |
|
479 // installing the event handler *before* we ever enable/disable Bluetooth. Or |
|
480 // we might just miss those events and get a timeout error. |
|
481 promises.push(waitForManagerEvent(aEnabled ? "enabled" : "disabled")); |
|
482 promises.push(setBluetoothEnabled(aEnabled)); |
|
483 |
|
484 return Promise.all(promises); |
|
485 } |
|
486 |
|
487 /** |
|
488 * Get default adapter. |
|
489 * |
|
490 * Resolve if that default adapter is got, reject otherwise. |
|
491 * |
|
492 * Fulfill params: a BluetoothAdapter instance. |
|
493 * Reject params: a DOMError, or null if if there is no adapter ready yet. |
|
494 * |
|
495 * @return A deferred promise. |
|
496 */ |
|
497 function getDefaultAdapter() { |
|
498 let deferred = Promise.defer(); |
|
499 |
|
500 let request = bluetoothManager.getDefaultAdapter(); |
|
501 request.onsuccess = function(aEvent) { |
|
502 let adapter = aEvent.target.result; |
|
503 if (!(adapter instanceof BluetoothAdapter)) { |
|
504 ok(false, "no BluetoothAdapter ready yet."); |
|
505 deferred.reject(null); |
|
506 return; |
|
507 } |
|
508 |
|
509 ok(true, "BluetoothAdapter got."); |
|
510 // TODO: We have an adapter instance now, but some of its attributes may |
|
511 // still remain unassigned/out-dated. Here we waste a few seconds to |
|
512 // wait for the property changed events. |
|
513 // |
|
514 // See https://bugzilla.mozilla.org/show_bug.cgi?id=932914 |
|
515 window.setTimeout(function() { |
|
516 deferred.resolve(adapter); |
|
517 }, 3000); |
|
518 }; |
|
519 request.onerror = function(aEvent) { |
|
520 ok(false, "Failed to get default adapter."); |
|
521 deferred.reject(aEvent.target.error); |
|
522 }; |
|
523 |
|
524 return deferred.promise; |
|
525 } |
|
526 |
|
527 /** |
|
528 * Flush permission settings and call |finish()|. |
|
529 */ |
|
530 function cleanUp() { |
|
531 waitFor(function() { |
|
532 SpecialPowers.flushPermissions(function() { |
|
533 // Use ok here so that we have at least one test run. |
|
534 ok(true, "permissions flushed"); |
|
535 |
|
536 finish(); |
|
537 }); |
|
538 }, function() { |
|
539 return pendingEmulatorCmdCount === 0; |
|
540 }); |
|
541 } |
|
542 |
|
543 function startBluetoothTestBase(aPermissions, aTestCaseMain) { |
|
544 ensureBluetoothManager(aPermissions) |
|
545 .then(aTestCaseMain) |
|
546 .then(cleanUp, function() { |
|
547 ok(false, "Unhandled rejected promise."); |
|
548 cleanUp(); |
|
549 }); |
|
550 } |
|
551 |
|
552 function startBluetoothTest(aReenable, aTestCaseMain) { |
|
553 startBluetoothTestBase(["settings-read", "settings-write"], function() { |
|
554 let origEnabled, needEnable; |
|
555 |
|
556 return getBluetoothEnabled() |
|
557 .then(function(aEnabled) { |
|
558 origEnabled = aEnabled; |
|
559 needEnable = !aEnabled; |
|
560 log("Original 'bluetooth.enabled' is " + origEnabled); |
|
561 |
|
562 if (aEnabled && aReenable) { |
|
563 log(" Disable 'bluetooth.enabled' ..."); |
|
564 needEnable = true; |
|
565 return setBluetoothEnabledAndWait(false); |
|
566 } |
|
567 }) |
|
568 .then(function() { |
|
569 if (needEnable) { |
|
570 log(" Enable 'bluetooth.enabled' ..."); |
|
571 |
|
572 // See setBluetoothEnabledAndWait(). We must install all event |
|
573 // handlers *before* enabling Bluetooth. |
|
574 let promises = []; |
|
575 promises.push(waitForManagerEvent("adapteradded")); |
|
576 promises.push(setBluetoothEnabledAndWait(true)); |
|
577 return Promise.all(promises); |
|
578 } |
|
579 }) |
|
580 .then(getDefaultAdapter) |
|
581 .then(aTestCaseMain) |
|
582 .then(function() { |
|
583 if (!origEnabled) { |
|
584 return setBluetoothEnabledAndWait(false); |
|
585 } |
|
586 }); |
|
587 }); |
|
588 } |