Commit d42a5bc8 authored by Ovidio Henriquez's avatar Ovidio Henriquez Committed by Commit Bot

bluetooth: FakeBluetoothChooser stub impl.

This change adds a new FakeBluetoothChooser interface stub to expand on.
The design for this class is detailed in the following document:
https://docs.google.com/document/d/1XFl_4ZAgO8ddM6U53A9AfUuZeWgJnlYD5wtbXqEpzeg

BUG=719827

Change-Id: I34169ce62dd5b35796639b7643d899b8315cc4c7
Reviewed-on: https://chromium-review.googlesource.com/909726
Commit-Queue: Ovidio Henriquez <odejesush@chromium.org>
Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarConley Owens <cco3@chromium.org>
Reviewed-by: default avatarGiovanni Ortuño Urquidi <ortuno@chromium.org>
Cr-Commit-Position: refs/heads/master@{#543479}
parent 1a058abb
...@@ -60,6 +60,8 @@ static_library("content_shell_lib") { ...@@ -60,6 +60,8 @@ static_library("content_shell_lib") {
"browser/layout_test/blink_test_controller.h", "browser/layout_test/blink_test_controller.h",
"browser/layout_test/devtools_protocol_test_bindings.cc", "browser/layout_test/devtools_protocol_test_bindings.cc",
"browser/layout_test/devtools_protocol_test_bindings.h", "browser/layout_test/devtools_protocol_test_bindings.h",
"browser/layout_test/fake_bluetooth_chooser.cc",
"browser/layout_test/fake_bluetooth_chooser.h",
"browser/layout_test/layout_test_bluetooth_adapter_provider.cc", "browser/layout_test/layout_test_bluetooth_adapter_provider.cc",
"browser/layout_test/layout_test_bluetooth_adapter_provider.h", "browser/layout_test/layout_test_bluetooth_adapter_provider.h",
"browser/layout_test/layout_test_bluetooth_chooser_factory.cc", "browser/layout_test/layout_test_bluetooth_chooser_factory.cc",
...@@ -816,6 +818,7 @@ if (is_mac) { ...@@ -816,6 +818,7 @@ if (is_mac) {
mojom("mojo_bindings") { mojom("mojo_bindings") {
sources = [ sources = [
"common/layout_test.mojom", "common/layout_test.mojom",
"common/layout_test/fake_bluetooth_chooser.mojom",
"common/layout_test/layout_test_bluetooth_fake_adapter_setter.mojom", "common/layout_test/layout_test_bluetooth_fake_adapter_setter.mojom",
"common/power_monitor_test.mojom", "common/power_monitor_test.mojom",
] ]
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
"renderer": [ "renderer": [
"content::mojom::LayoutTestBluetoothFakeAdapterSetter", "content::mojom::LayoutTestBluetoothFakeAdapterSetter",
"content::mojom::MojoLayoutTestHelper", "content::mojom::MojoLayoutTestHelper",
"content::mojom::FakeBluetoothChooser",
"bluetooth::mojom::FakeBluetooth" "bluetooth::mojom::FakeBluetooth"
] ]
}, },
......
...@@ -53,6 +53,7 @@ ...@@ -53,6 +53,7 @@
#include "content/public/common/url_constants.h" #include "content/public/common/url_constants.h"
#include "content/public/test/layouttest_support.h" #include "content/public/test/layouttest_support.h"
#include "content/shell/browser/layout_test/devtools_protocol_test_bindings.h" #include "content/shell/browser/layout_test/devtools_protocol_test_bindings.h"
#include "content/shell/browser/layout_test/fake_bluetooth_chooser.h"
#include "content/shell/browser/layout_test/layout_test_bluetooth_chooser_factory.h" #include "content/shell/browser/layout_test/layout_test_bluetooth_chooser_factory.h"
#include "content/shell/browser/layout_test/layout_test_content_browser_client.h" #include "content/shell/browser/layout_test/layout_test_content_browser_client.h"
#include "content/shell/browser/layout_test/layout_test_devtools_bindings.h" #include "content/shell/browser/layout_test/layout_test_devtools_bindings.h"
...@@ -553,10 +554,18 @@ bool BlinkTestController::IsMainWindow(WebContents* web_contents) const { ...@@ -553,10 +554,18 @@ bool BlinkTestController::IsMainWindow(WebContents* web_contents) const {
std::unique_ptr<BluetoothChooser> BlinkTestController::RunBluetoothChooser( std::unique_ptr<BluetoothChooser> BlinkTestController::RunBluetoothChooser(
RenderFrameHost* frame, RenderFrameHost* frame,
const BluetoothChooser::EventHandler& event_handler) { const BluetoothChooser::EventHandler& event_handler) {
// TODO(https://crbug.com/509038): Remove |bluetooth_chooser_factory_| once
// all of the Web Bluetooth tests are migrated to external/wpt/.
if (bluetooth_chooser_factory_) { if (bluetooth_chooser_factory_) {
return bluetooth_chooser_factory_->RunBluetoothChooser(frame, return bluetooth_chooser_factory_->RunBluetoothChooser(frame,
event_handler); event_handler);
} }
auto next_fake_bluetooth_chooser =
LayoutTestContentBrowserClient::Get()->GetNextFakeBluetoothChooser();
if (next_fake_bluetooth_chooser) {
next_fake_bluetooth_chooser->SetEventHandler(event_handler);
return next_fake_bluetooth_chooser;
}
return std::make_unique<LayoutTestFirstDeviceBluetoothChooser>(event_handler); return std::make_unique<LayoutTestFirstDeviceBluetoothChooser>(event_handler);
} }
......
// Copyright 2018 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 "content/shell/browser/layout_test/fake_bluetooth_chooser.h"
#include <string>
#include <utility>
#include "content/public/browser/bluetooth_chooser.h"
#include "content/shell/common/layout_test/fake_bluetooth_chooser.mojom.h"
namespace content {
FakeBluetoothChooser::~FakeBluetoothChooser() = default;
// static
std::unique_ptr<FakeBluetoothChooser> FakeBluetoothChooser::Create(
mojom::FakeBluetoothChooserRequest request) {
return std::unique_ptr<FakeBluetoothChooser>(
new FakeBluetoothChooser(std::move(request)));
}
void FakeBluetoothChooser::SetEventHandler(const EventHandler& event_handler) {
event_handler_ = event_handler;
}
// mojom::FakeBluetoothChooser overrides
void FakeBluetoothChooser::WaitForEvents(uint32_t num_of_events,
WaitForEventsCallback callback) {
// TODO(https://crbug.com/719826): Implement this function according to the
// Web Bluetooth Test Scanning design document.
// https://docs.google.com/document/d/1XFl_4ZAgO8ddM6U53A9AfUuZeWgJnlYD5wtbXqEpzeg
NOTREACHED();
}
void FakeBluetoothChooser::SelectPeripheral(
const std::string& peripheral_address,
SelectPeripheralCallback callback) {
// TODO(https://crbug.com/719826): Record the event and send a
// BluetoothChooser::SELECTED event to |event_handler_|.
NOTREACHED();
}
void FakeBluetoothChooser::Cancel(CancelCallback callback) {
// TODO(https://crbug.com/719826): Record the event and send a
// BluetoothChooser::CANCELLED event to |event_handler_|.
NOTREACHED();
}
void FakeBluetoothChooser::Rescan(RescanCallback callback) {
// TODO(https://crbug.com/719826): Record the event and send a
// BluetoothChooser::RESCAN event to |event_handler_|.
NOTREACHED();
}
// BluetoothChooser overrides
void FakeBluetoothChooser::SetAdapterPresence(AdapterPresence presence) {
// TODO(https://crbug.com/719826): Record the event.
NOTREACHED();
}
void FakeBluetoothChooser::ShowDiscoveryState(DiscoveryState state) {
// TODO(https://crbug.com/719826): Record the event.
NOTREACHED();
}
void FakeBluetoothChooser::AddOrUpdateDevice(const std::string& device_id,
bool should_update_name,
const base::string16& device_name,
bool is_gatt_connected,
bool is_paired,
int signal_strength_level) {
// TODO(https://crbug.com/719826): Record the event.
NOTREACHED();
}
// private
FakeBluetoothChooser::FakeBluetoothChooser(
mojom::FakeBluetoothChooserRequest request)
: binding_(this, std::move(request)) {}
} // namespace content
// Copyright 2018 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.
#ifndef CONTENT_SHELL_BROWSER_LAYOUT_TEST_FAKE_BLUETOOTH_CHOOSER_H_
#define CONTENT_SHELL_BROWSER_LAYOUT_TEST_FAKE_BLUETOOTH_CHOOSER_H_
#include <memory>
#include "content/public/browser/bluetooth_chooser.h"
#include "content/shell/common/layout_test/fake_bluetooth_chooser.mojom.h"
#include "mojo/public/cpp/bindings/binding.h"
namespace content {
// Implementation of FakeBluetoothChooser in
// src/content/shell/common/layout_test/fake_bluetooth_chooser.mojom
// to provide a method of controlling the Bluetooth chooser during a test.
// Serves as a Bluetooth chooser factory for choosers that can be manually
// controlled through the Mojo API. Only one instance of this class will exist
// while the chooser is active.
//
// The implementation details for FakeBluetoothChooser can be found in the Web
// Bluetooth Test Scanning design document.
// https://docs.google.com/document/d/1XFl_4ZAgO8ddM6U53A9AfUuZeWgJnlYD5wtbXqEpzeg
//
// Intended to only be used through the FakeBluetoothChooser Mojo interface.
class FakeBluetoothChooser : public mojom::FakeBluetoothChooser,
public BluetoothChooser {
public:
~FakeBluetoothChooser() override;
// LayoutTestContentBrowserClient will create an instance of this class when a
// request is bound. It will maintain ownership of the instance temporarily
// until the chooser is opened. When the chooser is opened, ownership of this
// instance will shift to the caller of
// WebContentsDelegate::RunBluetoothChooser.
static std::unique_ptr<FakeBluetoothChooser> Create(
mojom::FakeBluetoothChooserRequest request);
// Sets the EventHandler that will handle events produced by the chooser.
void SetEventHandler(const EventHandler& event_handler);
// mojom::FakeBluetoothChooser overrides:
void WaitForEvents(uint32_t num_of_events,
WaitForEventsCallback callback) override;
void SelectPeripheral(const std::string& peripheral_address,
SelectPeripheralCallback callback) override;
void Cancel(CancelCallback callback) override;
void Rescan(RescanCallback callback) override;
// BluetoothChooser overrides:
void SetAdapterPresence(AdapterPresence presence) override;
void ShowDiscoveryState(DiscoveryState state) override;
void AddOrUpdateDevice(const std::string& device_id,
bool should_update_name,
const base::string16& device_name,
bool is_gatt_connected,
bool is_paired,
int signal_strength_level) override;
private:
explicit FakeBluetoothChooser(mojom::FakeBluetoothChooserRequest request);
// Stores the callback function that handles chooser events.
EventHandler event_handler_;
mojo::Binding<mojom::FakeBluetoothChooser> binding_;
DISALLOW_COPY_AND_ASSIGN(FakeBluetoothChooser);
};
} // namespace content
#endif // CONTENT_SHELL_BROWSER_LAYOUT_TEST_FAKE_BLUETOOTH_CHOOSER_H_
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
#include "content/public/browser/resource_dispatcher_host.h" #include "content/public/browser/resource_dispatcher_host.h"
#include "content/public/browser/storage_partition.h" #include "content/public/browser/storage_partition.h"
#include "content/shell/browser/layout_test/blink_test_controller.h" #include "content/shell/browser/layout_test/blink_test_controller.h"
#include "content/shell/browser/layout_test/fake_bluetooth_chooser.h"
#include "content/shell/browser/layout_test/layout_test_bluetooth_fake_adapter_setter_impl.h" #include "content/shell/browser/layout_test/layout_test_bluetooth_fake_adapter_setter_impl.h"
#include "content/shell/browser/layout_test/layout_test_browser_context.h" #include "content/shell/browser/layout_test/layout_test_browser_context.h"
#include "content/shell/browser/layout_test/layout_test_browser_main_parts.h" #include "content/shell/browser/layout_test/layout_test_browser_main_parts.h"
...@@ -65,6 +66,11 @@ void LayoutTestContentBrowserClient::SetPopupBlockingEnabled( ...@@ -65,6 +66,11 @@ void LayoutTestContentBrowserClient::SetPopupBlockingEnabled(
block_popups_ = block_popups; block_popups_ = block_popups;
} }
std::unique_ptr<FakeBluetoothChooser>
LayoutTestContentBrowserClient::GetNextFakeBluetoothChooser() {
return std::move(next_fake_bluetooth_chooser_);
}
LayoutTestNotificationManager* LayoutTestNotificationManager*
LayoutTestContentBrowserClient::GetLayoutTestNotificationManager() { LayoutTestContentBrowserClient::GetLayoutTestNotificationManager() {
return layout_test_notification_manager_.get(); return layout_test_notification_manager_.get();
...@@ -93,12 +99,21 @@ void LayoutTestContentBrowserClient::ExposeInterfacesToRenderer( ...@@ -93,12 +99,21 @@ void LayoutTestContentBrowserClient::ExposeInterfacesToRenderer(
content::BrowserThread::GetTaskRunnerForThread( content::BrowserThread::GetTaskRunnerForThread(
content::BrowserThread::UI); content::BrowserThread::UI);
registry->AddInterface( registry->AddInterface(
base::Bind(&LayoutTestBluetoothFakeAdapterSetterImpl::Create), base::BindRepeating(&LayoutTestBluetoothFakeAdapterSetterImpl::Create),
ui_task_runner); ui_task_runner);
registry->AddInterface(base::Bind(&bluetooth::FakeBluetooth::Create), registry->AddInterface(base::BindRepeating(&bluetooth::FakeBluetooth::Create),
ui_task_runner);
// This class outlives |render_process_host|, which owns |registry|. Since
// CreateFakeBluetoothChooser will not be called after |registry| is deleted
// and |registry| is outlived by this class, it is safe to use
// base::Unretained.
registry->AddInterface(
base::BindRepeating(
&LayoutTestContentBrowserClient::CreateFakeBluetoothChooser,
base::Unretained(this)),
ui_task_runner); ui_task_runner);
registry->AddInterface(base::Bind(&MojoLayoutTestHelper::Create)); registry->AddInterface(base::BindRepeating(&MojoLayoutTestHelper::Create));
} }
void LayoutTestContentBrowserClient::OverrideWebkitPrefs( void LayoutTestContentBrowserClient::OverrideWebkitPrefs(
...@@ -201,4 +216,12 @@ LayoutTestContentBrowserClient::CreateLoginDelegate( ...@@ -201,4 +216,12 @@ LayoutTestContentBrowserClient::CreateLoginDelegate(
return nullptr; return nullptr;
} }
// private
void LayoutTestContentBrowserClient::CreateFakeBluetoothChooser(
mojom::FakeBluetoothChooserRequest request) {
DCHECK(!next_fake_bluetooth_chooser_);
next_fake_bluetooth_chooser_ =
FakeBluetoothChooser::Create(std::move(request));
}
} // namespace content } // namespace content
...@@ -6,9 +6,11 @@ ...@@ -6,9 +6,11 @@
#define CONTENT_SHELL_BROWSER_LAYOUT_TEST_LAYOUT_TEST_CONTENT_BROWSER_CLIENT_H_ #define CONTENT_SHELL_BROWSER_LAYOUT_TEST_LAYOUT_TEST_CONTENT_BROWSER_CLIENT_H_
#include "content/shell/browser/shell_content_browser_client.h" #include "content/shell/browser/shell_content_browser_client.h"
#include "content/shell/common/layout_test/fake_bluetooth_chooser.mojom.h"
namespace content { namespace content {
class FakeBluetoothChooser;
class LayoutTestBrowserContext; class LayoutTestBrowserContext;
class LayoutTestNotificationManager; class LayoutTestNotificationManager;
...@@ -23,6 +25,9 @@ class LayoutTestContentBrowserClient : public ShellContentBrowserClient { ...@@ -23,6 +25,9 @@ class LayoutTestContentBrowserClient : public ShellContentBrowserClient {
LayoutTestBrowserContext* GetLayoutTestBrowserContext(); LayoutTestBrowserContext* GetLayoutTestBrowserContext();
void SetPopupBlockingEnabled(bool block_popups_); void SetPopupBlockingEnabled(bool block_popups_);
// Retrieves the last created FakeBluetoothChooser instance.
std::unique_ptr<FakeBluetoothChooser> GetNextFakeBluetoothChooser();
// Implements the PlatformNotificationService interface. // Implements the PlatformNotificationService interface.
LayoutTestNotificationManager* GetLayoutTestNotificationManager(); LayoutTestNotificationManager* GetLayoutTestNotificationManager();
...@@ -77,9 +82,16 @@ class LayoutTestContentBrowserClient : public ShellContentBrowserClient { ...@@ -77,9 +82,16 @@ class LayoutTestContentBrowserClient : public ShellContentBrowserClient {
auth_required_callback) override; auth_required_callback) override;
private: private:
// Creates and stores a FakeBluetoothChooser instance.
void CreateFakeBluetoothChooser(mojom::FakeBluetoothChooserRequest request);
std::unique_ptr<LayoutTestNotificationManager> std::unique_ptr<LayoutTestNotificationManager>
layout_test_notification_manager_; layout_test_notification_manager_;
bool block_popups_ = false; bool block_popups_ = false;
// Stores the next instance of FakeBluetoothChooser that is to be returned
// when GetNextFakeBluetoothChooser is called.
std::unique_ptr<FakeBluetoothChooser> next_fake_bluetooth_chooser_;
}; };
} // content } // content
......
per-file *_messages*.h=set noparent per-file *_messages*.h=set noparent
per-file *_messages*.h=file://ipc/SECURITY_OWNERS per-file *_messages*.h=file://ipc/SECURITY_OWNERS
per-file *.mojom=set noparent
per-file *.mojom=file://ipc/SECURITY_OWNERS
// Copyright 2018 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.
module content.mojom;
// This interface is being developed to support Web Platform Tests for Web
// Bluetooth.
// https://docs.google.com/document/d/1Nhv_oVDCodd1pEH_jj9k8gF4rPGb_84VYaZ9IG8M_WY
//
// These interfaces are not intended to be used directly.
// `web-bluetooth-test.js` makes the Fake Bluetooth interface easier to work
// with.
// * Calls are synchronous.
// * IDs are cached.
//
// If another C++ client intends to use FakeBluetooth a C++ wrapper similar to
// `web-bluetooth-test.js` should be created.
//
// The implementation details of FakeBluetoothChooser can be found in the Web
// Bluetooth Test Scanning design document.
// https://docs.google.com/document/d/1XFl_4ZAgO8ddM6U53A9AfUuZeWgJnlYD5wtbXqEpzeg
// Indicates the types of Bluetooth chooser events.
enum ChooserEventType {
CHOOSER_OPENED,
SCAN_STARTED,
DEVICE_UPDATE,
ADAPTER_REMOVED,
ADAPTER_DISABLED,
ADAPTER_ENABLED,
DISCOVERY_FAILED_TO_START,
DISCOVERING,
DISCOVERY_IDLE,
ADD_DEVICE,
};
// FakeBluetoothChooser allows clients to control the global state of the
// Bluetooth chooser during a layout test.
interface FakeBluetoothChooser {
// Waits until at least |num_of_events| have been recorded before returning
// |num_of_events| FakeBluetoothChooserEvents.
WaitForEvents(
uint32 num_of_events) => (array<FakeBluetoothChooserEvent> events);
// Simulates a user selecting the given |peripheral_address| in the Bluetooth
// chooser.
SelectPeripheral(string peripheral_address) => ();
// Calls the event handler function with the CANCELLED event.
Cancel() => ();
// Calls the event handler function with the RESCAN event.
Rescan() => ();
};
// FakeBluetoothChooserEvent describes the type of chooser event that has been
// produced by the FakeBluetoothChooser.
struct FakeBluetoothChooserEvent {
ChooserEventType type;
// Describes the origin the chooser is currently displaying.
// This field will be used by the |CHOOSER_OPENED| event type.
string? origin;
// Describes the MAC address of the Bluetooth device.
string? peripheral_address;
};
<!DOCTYPE html>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../resources/testdriver.js"></script>
<script src="../../../resources/testdriver-vendor.js"></script>
<script src="../../../external/wpt/bluetooth/resources/bluetooth-helpers.js"></script>
<script>
'use strict';
// TODO(https://crbug.com/719826): This is a temporary test to try the
// FakeBluetoothChooser API as it is implemented. This test should be delete after
// the feature is completed. The implementation details can be found in the design
// document.
// https://docs.google.com/document/d/1XFl_4ZAgO8ddM6U53A9AfUuZeWgJnlYD5wtbXqEpzeg
const test_desc = 'Ensure that the FakeBluetoothChooser API works correctly.';
bluetooth_test(() => navigator.bluetooth.test.simulateCentral({
state: 'powered-on'
})
.then(() => navigator.bluetooth.test.getManualChooser())
.then(chooser => assert_true(typeof chooser !== 'undefined')),
test_desc);
</script>
...@@ -42,6 +42,7 @@ function performChromiumSetup() { ...@@ -42,6 +42,7 @@ function performChromiumSetup() {
`${prefix}/mojo_layouttest_test.mojom.js`, `${prefix}/mojo_layouttest_test.mojom.js`,
`${prefix}/uuid.mojom.js`, `${prefix}/uuid.mojom.js`,
`${prefix}/fake_bluetooth.mojom.js`, `${prefix}/fake_bluetooth.mojom.js`,
`${prefix}/fake_bluetooth_chooser.mojom.js`,
`${prefix}/web-bluetooth-test.js`, `${prefix}/web-bluetooth-test.js`,
].concat(extra)) ].concat(extra))
// Call setBluetoothFakeAdapter() to clean up any fake adapters left over // Call setBluetoothFakeAdapter() to clean up any fake adapters left over
......
...@@ -65,7 +65,7 @@ class FakeBluetooth { ...@@ -65,7 +65,7 @@ class FakeBluetooth {
constructor() { constructor() {
this.fake_bluetooth_ptr_ = new bluetooth.mojom.FakeBluetoothPtr(); this.fake_bluetooth_ptr_ = new bluetooth.mojom.FakeBluetoothPtr();
Mojo.bindInterface(bluetooth.mojom.FakeBluetooth.name, Mojo.bindInterface(bluetooth.mojom.FakeBluetooth.name,
mojo.makeRequest(this.fake_bluetooth_ptr_).handle, "process"); mojo.makeRequest(this.fake_bluetooth_ptr_).handle, 'process');
} }
// Set it to indicate whether the platform supports BLE. For example, // Set it to indicate whether the platform supports BLE. For example,
...@@ -102,6 +102,15 @@ class FakeBluetooth { ...@@ -102,6 +102,15 @@ class FakeBluetooth {
let {consumed} = await this.fake_bluetooth_ptr_.allResponsesConsumed(); let {consumed} = await this.fake_bluetooth_ptr_.allResponsesConsumed();
return consumed; return consumed;
} }
// Returns a promise that resolves with a FakeChooser that clients can use to
// simulate chooser events.
async getManualChooser() {
if (typeof this.fake_chooser_ === 'undefined') {
this.fake_chooser_ = new FakeChooser();
}
return this.fake_chooser_;
}
} }
// FakeCentral allows clients to simulate events that a device in the // FakeCentral allows clients to simulate events that a device in the
...@@ -437,6 +446,17 @@ class FakeRemoteGATTDescriptor { ...@@ -437,6 +446,17 @@ class FakeRemoteGATTDescriptor {
} }
} }
// FakeChooser allows clients to simulate events that a user would trigger when
// using the Bluetooth chooser, and monitor the events that are produced.
class FakeChooser {
constructor() {
this.fake_bluetooth_chooser_ptr_ =
new content.mojom.FakeBluetoothChooserPtr();
Mojo.bindInterface(content.mojom.FakeBluetoothChooser.name,
mojo.makeRequest(this.fake_bluetooth_chooser_ptr_).handle, 'process');
}
}
// If this line fails, it means that current environment does not support the // If this line fails, it means that current environment does not support the
// Web Bluetooth Test API. // Web Bluetooth Test API.
try { try {
......
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