Commit 2eec1c80 authored by ortuno's avatar ortuno Committed by Commit bot

bluetooth: Disconnect if the page becomes hidden or is closed.

This means closing connection when:
1. BluetoothGATTRemoteServer is destroyed.
2. The parent document is detached.
3. The page becomes hidden.

BUG=579746

Review URL: https://codereview.chromium.org/1611773002

Cr-Commit-Position: refs/heads/master@{#371542}
parent fee9a5ec
...@@ -117,6 +117,30 @@ promise_test(() => { ...@@ -117,6 +117,30 @@ promise_test(() => {
}, testSpec.testName); }, testSpec.testName);
}); });
// TODO(ortuno): Allow connections when the tab is in the background.
// This is a short term solution instead of implementing a tab indicator
// for bluetooth connections.
// https://crbug.com/579746
promise_test(() => {
testRunner.setBluetoothMockDataSet('HeartRateAdapter');
return requestDeviceWithKeyDown({filters: [{services: ['heart_rate']}]})
.then(device => {
testRunner.setPageVisibility('hidden');
return assert_promise_rejects_with_message(
device.connectGATT(),
new DOMException('Connection is only allowed while the page is visible. ' +
'This is a temporary measure until we are able to ' +
'effectively communicate to the user that a page is ' +
'connected to a device.',
'SecurityError'));
})
.then(() => testRunner.setPageVisibility('visible'))
.catch(error => {
testRunner.setPageVisibility('visible');
throw error;
});
}, 'Device should not be able to connect in background.');
promise_test(() => { promise_test(() => {
testRunner.setBluetoothMockDataSet('HeartRateAdapter'); testRunner.setBluetoothMockDataSet('HeartRateAdapter');
return requestDeviceWithKeyDown({filters: [{services: ['heart_rate']}]}) return requestDeviceWithKeyDown({filters: [{services: ['heart_rate']}]})
......
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../resources/bluetooth-helpers.js"></script>
<body>
<script>
"use strict";
async_test(test => {
window.onmessage = messageEvent => test.step(() => {
if (messageEvent.data === 'Ready') {
let iframe = document.querySelector('iframe');
callWithKeyDown(() => {
iframe.contentWindow.postMessage('Go', '*');
});
} else if (messageEvent.data === 'Connected') {
// Detach
iframe.remove();
// GC
runGarbageCollection().then(() => test.done());
} else {
assert_unreached('iframe sent invalid data: ' + messageEvent.data);
}
});
testRunner.setBluetoothMockDataSet('HeartRateAdapter');
let iframe = document.createElement('iframe');
iframe.src = '../resources/connectGATT-iframe.html';
document.body.appendChild(iframe);
}, 'Detach frame then garbage collect. We shouldn\'t crash.');
</script>
</body>
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../resources/bluetooth-helpers.js"></script>
<body>
<script>
"use strict";
async_test(test => {
window.onmessage = messageEvent => test.step(() => {
if (messageEvent.data === 'Ready') {
let iframe = document.querySelector('iframe');
callWithKeyDown(() => {
iframe.contentWindow.postMessage('Go', '*');
});
} else if (messageEvent.data === 'Connected') {
// GC
runGarbageCollection().then(() => {
// Detach
iframe.remove();
test.done();
});
} else {
assert_unreached('iframe sent invalid data: ' + messageEvent.data);
}
});
testRunner.setBluetoothMockDataSet('HeartRateAdapter');
let iframe = document.createElement('iframe');
iframe.src = '../resources/connectGATT-iframe.html';
document.body.appendChild(iframe);
}, 'Garbage collect then detach frame. We shouldn\'t crash.');
</script>
</body>
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../resources/bluetooth-helpers.js"></script>
<body>
<script>
"use strict";
async_test(test => {
window.onmessage = messageEvent => test.step(() => {
if (messageEvent.data === 'Ready') {
let iframe = document.querySelector('iframe');
callWithKeyDown(() => {
iframe.contentWindow.postMessage('Go', '*');
});
} else if (messageEvent.data === 'Connected') {
// Hide
testRunner.setPageVisibility('hidden');
// Detach
iframe.remove();
test.done();
} else {
assert_unreached('iframe sent invalid data: ' + messageEvent.data);
}
});
testRunner.setBluetoothMockDataSet('HeartRateAdapter');
let iframe = document.createElement('iframe');
iframe.src = '../resources/connectGATT-iframe.html';
document.body.appendChild(iframe);
}, 'Hide then detach frame. We shouldn\'t crash.');
</script>
</body>
<!DOCTYPE html>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="resources/bluetooth-helpers.js"></script>
<script>
'use strict';
test(t => { assert_true(window.testRunner instanceof Object); t.done(); },
'window.testRunner is required for the following tests.');
// TODO(ortuno): Allow connections when the tab is in the background.
// This is a short term solution instead of implementing a tab indicator
// for bluetooth connections.
// https://crbug.com/579746
promise_test(() => {
testRunner.setPageVisibility("visible");
testRunner.setBluetoothMockDataSet('HeartRateAdapter');
return requestDeviceWithKeyDown({filters: [{services: ['heart_rate']}]})
.then(device => device.connectGATT())
.then(gatt_server => {
assert_true(gatt_server.connected);
testRunner.setPageVisibility("hidden");
assert_false(gatt_server.connected);
testRunner.setPageVisibility("visible");
assert_false(gatt_server.connected);
});
}, 'Test device disconnects when tab becomes hidden');
promise_test(() => {
testRunner.setPageVisibility('visible');
testRunner.setBluetoothMockDataSet('HeartRateAdapter');
return requestDeviceWithKeyDown({filters: [{services: ['heart_rate']}]})
.then(device => device.connectGATT())
.then(gatt_server => {})
.then(() => runGarbageCollection())
.then(() => testRunner.setPageVisibility('hidden'));
}, 'Test object gets garbage collected before tab becomes hidden. ' +
'We shouldn\'t crash.');
promise_test(() => {
testRunner.setPageVisibility('visible');
testRunner.setBluetoothMockDataSet('HeartRateAdapter');
return requestDeviceWithKeyDown({filters: [{services: ['heart_rate']}]})
.then(device => device.connectGATT())
.then(gatt_server => testRunner.setPageVisibility('hidden'))
.then(() => runGarbageCollection());
}, 'Test object gets garbage collected after tab becomes hidden. ' +
'We shouldn\'t crash.');
promise_test(() => {
testRunner.setPageVisibility('visible');
testRunner.setBluetoothMockDataSet('HeartRateAdapter');
return requestDeviceWithKeyDown({filters: [{services: ['heart_rate']}]})
.then(device => {
let connect_promise = device.connectGATT();
testRunner.setPageVisibility('hidden');
return connect_promise;
}).then(gatt_server => {
assert_false(gatt_server.connected);
});
}, 'Visibility changes during connection. Should disconnect.');
</script>
<!DOCTYPE html>
<script>
window.onmessage = messageEvent => {
if (messageEvent.data === 'Go') {
navigator.bluetooth.requestDevice({
filters: [{services: ['heart_rate']}]
})
.then(device => device.connectGATT())
.then(gattServer => {
parent.postMessage('Connected', '*');
}).catch(err => {
console.error(err);
parent.postMessage('FAIL: ' + err, '*');
});
}
};
parent.postMessage("Ready", "*");
</script>
...@@ -8,7 +8,9 @@ ...@@ -8,7 +8,9 @@
#include "bindings/core/v8/ScriptPromise.h" #include "bindings/core/v8/ScriptPromise.h"
#include "bindings/core/v8/ScriptPromiseResolver.h" #include "bindings/core/v8/ScriptPromiseResolver.h"
#include "core/dom/DOMException.h" #include "core/dom/DOMException.h"
#include "core/dom/Document.h"
#include "core/dom/ExceptionCode.h" #include "core/dom/ExceptionCode.h"
#include "core/page/PageVisibilityState.h"
#include "modules/bluetooth/BluetoothError.h" #include "modules/bluetooth/BluetoothError.h"
#include "modules/bluetooth/BluetoothGATTRemoteServer.h" #include "modules/bluetooth/BluetoothGATTRemoteServer.h"
#include "modules/bluetooth/BluetoothSupplement.h" #include "modules/bluetooth/BluetoothSupplement.h"
...@@ -79,6 +81,13 @@ Vector<String> BluetoothDevice::uuids() ...@@ -79,6 +81,13 @@ Vector<String> BluetoothDevice::uuids()
ScriptPromise BluetoothDevice::connectGATT(ScriptState* scriptState) ScriptPromise BluetoothDevice::connectGATT(ScriptState* scriptState)
{ {
// TODO(ortuno): Allow connections when the tab is in the background.
// This is a short term solution instead of implementing a tab indicator
// for bluetooth connections.
// https://crbug.com/579746
if (!toDocument(scriptState->executionContext())->page()->isPageVisible()) {
return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(SecurityError, "Connection is only allowed while the page is visible. This is a temporary measure until we are able to effectively communicate to the user that a page is connected to a device."));
}
WebBluetooth* webbluetooth = BluetoothSupplement::fromScriptState(scriptState); WebBluetooth* webbluetooth = BluetoothSupplement::fromScriptState(scriptState);
if (!webbluetooth) if (!webbluetooth)
return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(NotSupportedError)); return ScriptPromise::rejectWithDOMException(scriptState, DOMException::create(NotSupportedError));
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include "bindings/core/v8/ScriptPromise.h" #include "bindings/core/v8/ScriptPromise.h"
#include "bindings/core/v8/ScriptPromiseResolver.h" #include "bindings/core/v8/ScriptPromiseResolver.h"
#include "core/dom/DOMException.h" #include "core/dom/DOMException.h"
#include "core/dom/Document.h"
#include "core/dom/ExceptionCode.h" #include "core/dom/ExceptionCode.h"
#include "modules/bluetooth/BluetoothError.h" #include "modules/bluetooth/BluetoothError.h"
#include "modules/bluetooth/BluetoothGATTService.h" #include "modules/bluetooth/BluetoothGATTService.h"
...@@ -18,15 +19,56 @@ ...@@ -18,15 +19,56 @@
namespace blink { namespace blink {
BluetoothGATTRemoteServer::BluetoothGATTRemoteServer(PassOwnPtr<WebBluetoothGATTRemoteServer> webGATT) BluetoothGATTRemoteServer::BluetoothGATTRemoteServer(ExecutionContext* context, PassOwnPtr<WebBluetoothGATTRemoteServer> webGATT)
: m_webGATT(webGATT) : ActiveDOMObject(context)
, PageLifecycleObserver(toDocument(context)->page())
, m_webGATT(webGATT)
{ {
// See example in Source/platform/heap/ThreadState.h
ThreadState::current()->registerPreFinalizer(this);
} }
BluetoothGATTRemoteServer* BluetoothGATTRemoteServer::take(ScriptPromiseResolver*, PassOwnPtr<WebBluetoothGATTRemoteServer> webGATT) BluetoothGATTRemoteServer* BluetoothGATTRemoteServer::take(ScriptPromiseResolver* resolver, PassOwnPtr<WebBluetoothGATTRemoteServer> webGATT)
{ {
ASSERT(webGATT); ASSERT(webGATT);
return new BluetoothGATTRemoteServer(webGATT); BluetoothGATTRemoteServer* server = new BluetoothGATTRemoteServer(resolver->executionContext(), webGATT);
if (!server->page()->isPageVisible()) {
server->disconnectIfConnected();
}
server->suspendIfNeeded();
return server;
}
void BluetoothGATTRemoteServer::dispose()
{
disconnectIfConnected();
}
void BluetoothGATTRemoteServer::stop()
{
disconnectIfConnected();
}
void BluetoothGATTRemoteServer::pageVisibilityChanged()
{
if (!page()->isPageVisible()) {
disconnectIfConnected();
}
}
void BluetoothGATTRemoteServer::disconnectIfConnected()
{
if (m_webGATT->connected) {
m_webGATT->connected = false;
WebBluetooth* webbluetooth = BluetoothSupplement::fromExecutionContext(executionContext());
webbluetooth->disconnect(m_webGATT->deviceId);
}
}
DEFINE_TRACE(BluetoothGATTRemoteServer)
{
ActiveDOMObject::trace(visitor);
PageLifecycleObserver::trace(visitor);
} }
void BluetoothGATTRemoteServer::disconnect(ScriptState* scriptState) void BluetoothGATTRemoteServer::disconnect(ScriptState* scriptState)
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
#include "bindings/core/v8/ScriptWrappable.h" #include "bindings/core/v8/ScriptWrappable.h"
#include "bindings/modules/v8/UnionTypesModules.h" #include "bindings/modules/v8/UnionTypesModules.h"
#include "core/dom/ActiveDOMObject.h"
#include "core/page/PageLifecycleObserver.h"
#include "platform/heap/Heap.h" #include "platform/heap/Heap.h"
#include "public/platform/modules/bluetooth/WebBluetoothGATTRemoteServer.h" #include "public/platform/modules/bluetooth/WebBluetoothGATTRemoteServer.h"
#include "wtf/OwnPtr.h" #include "wtf/OwnPtr.h"
...@@ -27,17 +29,48 @@ class ScriptState; ...@@ -27,17 +29,48 @@ class ScriptState;
// CallbackPromiseAdapter class comments. // CallbackPromiseAdapter class comments.
class BluetoothGATTRemoteServer final class BluetoothGATTRemoteServer final
: public GarbageCollectedFinalized<BluetoothGATTRemoteServer> : public GarbageCollectedFinalized<BluetoothGATTRemoteServer>
, public ActiveDOMObject
, public PageLifecycleObserver
, public ScriptWrappable { , public ScriptWrappable {
USING_PRE_FINALIZER(BluetoothGATTRemoteServer, dispose);
DEFINE_WRAPPERTYPEINFO(); DEFINE_WRAPPERTYPEINFO();
WILL_BE_USING_GARBAGE_COLLECTED_MIXIN(BluetoothGATTRemoteServer);
public: public:
BluetoothGATTRemoteServer(PassOwnPtr<WebBluetoothGATTRemoteServer>); BluetoothGATTRemoteServer(ExecutionContext*, PassOwnPtr<WebBluetoothGATTRemoteServer>);
// Interface required by CallbackPromiseAdapter: // Interface required by CallbackPromiseAdapter:
using WebType = OwnPtr<WebBluetoothGATTRemoteServer>; using WebType = OwnPtr<WebBluetoothGATTRemoteServer>;
static BluetoothGATTRemoteServer* take(ScriptPromiseResolver*, PassOwnPtr<WebBluetoothGATTRemoteServer>); static BluetoothGATTRemoteServer* take(ScriptPromiseResolver*, PassOwnPtr<WebBluetoothGATTRemoteServer>);
// We should disconnect from the device in all of the following cases:
// 1. When the object gets GarbageCollected e.g. it went out of scope.
// dispose() is called in this case.
// 2. When the parent document gets detached e.g. reloading a page.
// stop() is called in this case.
// 3. For now (https://crbug.com/579746), when the tab is no longer in the
// foreground e.g. change tabs.
// pageVisibilityChanged() is called in this case.
// TODO(ortuno): Users should be able to turn on notifications listen for
// events on navigator.bluetooth and still remain connected even if the
// BluetoothGATTRemoteServer object is garbage collected.
// TODO(ortuno): Allow connections when the tab is in the background.
// This is a short term solution instead of implementing a tab indicator
// for bluetooth connections.
// USING_PRE_FINALIZER interface.
// Called before the object gets garbage collected.
void dispose();
// ActiveDOMObject interface.
void stop() override;
// PageLifecycleObserver interface.
void pageVisibilityChanged() override;
void disconnectIfConnected();
// Interface required by Garbage Collectoin: // Interface required by Garbage Collectoin:
DEFINE_INLINE_TRACE() { } DECLARE_VIRTUAL_TRACE();
// IDL exposed interface: // IDL exposed interface:
bool connected() { return m_webGATT->connected; } bool connected() { return m_webGATT->connected; }
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
[ [
GarbageCollected, GarbageCollected,
ActiveDOMObject,
RuntimeEnabled=WebBluetooth, RuntimeEnabled=WebBluetooth,
] interface BluetoothGATTRemoteServer ] interface BluetoothGATTRemoteServer
// Implement ServiceEventHandlers interface: http://crbug.com/421670 // Implement ServiceEventHandlers interface: http://crbug.com/421670
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment