blob: 90f11cf29260eb4f478d6894a5c25eca9434aa8c [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "modules/bluetooth/Bluetooth.h"
#include <memory>
#include <utility>
#include "bindings/core/v8/CallbackPromiseAdapter.h"
#include "bindings/core/v8/ScriptPromise.h"
#include "bindings/core/v8/ScriptPromiseResolver.h"
#include "build/build_config.h"
#include "core/dom/DOMException.h"
#include "core/dom/Document.h"
#include "core/dom/ExceptionCode.h"
#include "core/dom/ExecutionContext.h"
#include "core/frame/Frame.h"
#include "core/frame/LocalFrame.h"
#include "core/inspector/ConsoleMessage.h"
#include "modules/bluetooth/BluetoothDevice.h"
#include "modules/bluetooth/BluetoothError.h"
#include "modules/bluetooth/BluetoothRemoteGATTCharacteristic.h"
#include "modules/bluetooth/BluetoothUUID.h"
#include "modules/bluetooth/RequestDeviceOptions.h"
#include "public/platform/Platform.h"
#include "services/service_manager/public/cpp/interface_provider.h"
namespace blink {
namespace {
// Per the Bluetooth Spec: The name is a user-friendly name associated with the
// device and consists of a maximum of 248 bytes coded according to the UTF-8
// standard.
const size_t kMaxDeviceNameLength = 248;
const char kDeviceNameTooLong[] =
"A device name can't be longer than 248 bytes.";
} // namespace
static void CanonicalizeFilter(
const BluetoothLEScanFilterInit& filter,
mojom::blink::WebBluetoothLeScanFilterPtr& canonicalized_filter,
ExceptionState& exception_state) {
if (!(filter.hasServices() || filter.hasName() || filter.hasNamePrefix())) {
exception_state.ThrowTypeError(
"A filter must restrict the devices in some way.");
return;
}
if (filter.hasServices()) {
if (filter.services().size() == 0) {
exception_state.ThrowTypeError(
"'services', if present, must contain at least one service.");
return;
}
canonicalized_filter->services.emplace();
for (const StringOrUnsignedLong& service : filter.services()) {
const String& validated_service =
BluetoothUUID::getService(service, exception_state);
if (exception_state.HadException())
return;
canonicalized_filter->services->push_back(validated_service);
}
}
if (filter.hasName()) {
size_t name_length = filter.name().Utf8().length();
if (name_length > kMaxDeviceNameLength) {
exception_state.ThrowTypeError(kDeviceNameTooLong);
return;
}
canonicalized_filter->name = filter.name();
}
if (filter.hasNamePrefix()) {
size_t name_prefix_length = filter.namePrefix().Utf8().length();
if (name_prefix_length > kMaxDeviceNameLength) {
exception_state.ThrowTypeError(kDeviceNameTooLong);
return;
}
if (filter.namePrefix().length() == 0) {
exception_state.ThrowTypeError(
"'namePrefix', if present, must me non-empty.");
return;
}
canonicalized_filter->name_prefix = filter.namePrefix();
}
}
static void ConvertRequestDeviceOptions(
const RequestDeviceOptions& options,
mojom::blink::WebBluetoothRequestDeviceOptionsPtr& result,
ExceptionState& exception_state) {
if (!(options.hasFilters() ^ options.acceptAllDevices())) {
exception_state.ThrowTypeError(
"Either 'filters' should be present or 'acceptAllDevices' should be "
"true, but not both.");
return;
}
result->accept_all_devices = options.acceptAllDevices();
if (options.hasFilters()) {
if (options.filters().IsEmpty()) {
exception_state.ThrowTypeError(
"'filters' member must be non-empty to find any devices.");
return;
}
result->filters.emplace();
for (const BluetoothLEScanFilterInit& filter : options.filters()) {
auto canonicalized_filter = mojom::blink::WebBluetoothLeScanFilter::New();
CanonicalizeFilter(filter, canonicalized_filter, exception_state);
if (exception_state.HadException())
return;
result->filters.value().push_back(std::move(canonicalized_filter));
}
}
if (options.hasOptionalServices()) {
for (const StringOrUnsignedLong& optional_service :
options.optionalServices()) {
const String& validated_optional_service =
BluetoothUUID::getService(optional_service, exception_state);
if (exception_state.HadException())
return;
result->optional_services.push_back(validated_optional_service);
}
}
}
void Bluetooth::RequestDeviceCallback(
ScriptPromiseResolver* resolver,
mojom::blink::WebBluetoothResult result,
mojom::blink::WebBluetoothDevicePtr device) {
if (!resolver->GetExecutionContext() ||
resolver->GetExecutionContext()->IsContextDestroyed())
return;
if (result == mojom::blink::WebBluetoothResult::SUCCESS) {
BluetoothDevice* bluetooth_device =
GetBluetoothDeviceRepresentingDevice(std::move(device), resolver);
resolver->Resolve(bluetooth_device);
} else {
resolver->Reject(BluetoothError::CreateDOMException(result));
}
}
// https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetooth-requestdevice
ScriptPromise Bluetooth::requestDevice(ScriptState* script_state,
const RequestDeviceOptions& options,
ExceptionState& exception_state) {
ExecutionContext* context = ExecutionContext::From(script_state);
// Remind developers when they are using Web Bluetooth on unsupported platforms.
#if !defined(OS_CHROMEOS) && !defined(OS_ANDROID) && !defined(OS_MACOSX)
context->AddConsoleMessage(ConsoleMessage::Create(
kJSMessageSource, kInfoMessageLevel,
"Web Bluetooth is experimental on this platform. See "
"https://github.com/WebBluetoothCG/web-bluetooth/blob/gh-pages/"
"implementation-status.md"));
#endif
// If the Relevant settings object is not a secure context, reject promise
// with a SecurityError and abort these steps.
String error_message;
if (!context->IsSecureContext(error_message)) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(kSecurityError, error_message));
}
// If the algorithm is not allowed to show a popup, reject promise with a
// SecurityError and abort these steps.
Document* doc = ToDocumentOrNull(context);
if (!Frame::ConsumeTransientUserActivation(doc ? doc->GetFrame() : nullptr)) {
return ScriptPromise::RejectWithDOMException(
script_state,
DOMException::Create(
kSecurityError,
"Must be handling a user gesture to show a permission request."));
}
if (!service_ && doc) {
LocalFrame* frame = doc->GetFrame();
if (frame) {
frame->GetInterfaceProvider().GetInterface(mojo::MakeRequest(&service_));
}
}
if (!service_) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(kNotSupportedError));
}
// In order to convert the arguments from service names and aliases to just
// UUIDs, do the following substeps:
auto device_options = mojom::blink::WebBluetoothRequestDeviceOptions::New();
ConvertRequestDeviceOptions(options, device_options, exception_state);
if (exception_state.HadException())
return exception_state.Reject(script_state);
// Record the eTLD+1 of the frame using the API.
Document* document = ToDocument(context);
Platform::Current()->RecordRapporURL("Bluetooth.APIUsage.Origin",
document->Url());
// Subsequent steps are handled in the browser process.
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
service_->RequestDevice(
std::move(device_options),
WTF::Bind(&Bluetooth::RequestDeviceCallback, WrapPersistent(this),
WrapPersistent(resolver)));
return promise;
}
void Bluetooth::Trace(blink::Visitor* visitor) {
visitor->Trace(device_instance_map_);
ScriptWrappable::Trace(visitor);
}
Bluetooth::Bluetooth() {}
BluetoothDevice* Bluetooth::GetBluetoothDeviceRepresentingDevice(
mojom::blink::WebBluetoothDevicePtr device_ptr,
ScriptPromiseResolver* resolver) {
WTF::String id = device_ptr->id;
BluetoothDevice* device = device_instance_map_.at(id);
if (!device) {
device = BluetoothDevice::Take(resolver, std::move(device_ptr), this);
auto result = device_instance_map_.insert(id, device);
DCHECK(result.is_new_entry);
}
return device;
}
} // namespace blink