blob: 8f921326a892d6d1d6c516b77c0a65a9d75a90d2 [file] [log] [blame]
// Copyright 2016 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/browser/bluetooth/bluetooth_device_chooser_controller.h"
#include <set>
#include <string>
#include <unordered_set>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/bluetooth/bluetooth_blacklist.h"
#include "content/browser/bluetooth/bluetooth_metrics.h"
#include "content/browser/bluetooth/web_bluetooth_service_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "device/bluetooth/bluetooth_adapter.h"
#include "device/bluetooth/bluetooth_common.h"
#include "device/bluetooth/bluetooth_discovery_session.h"
using device::BluetoothUUID;
using UUIDSet = device::BluetoothDevice::UUIDSet;
namespace {
// Anything worse than or equal to this will show 0 bars.
const int kMinRSSI = -100;
// Anything better than or equal to this will show the maximum bars.
const int kMaxRSSI = -55;
// Number of RSSI levels used in the signal strength image.
const int kNumSignalStrengthLevels = 5;
const content::UMARSSISignalStrengthLevel kRSSISignalStrengthEnumTable[] = {
content::UMARSSISignalStrengthLevel::LEVEL_0,
content::UMARSSISignalStrengthLevel::LEVEL_1,
content::UMARSSISignalStrengthLevel::LEVEL_2,
content::UMARSSISignalStrengthLevel::LEVEL_3,
content::UMARSSISignalStrengthLevel::LEVEL_4};
} // namespace
namespace content {
bool BluetoothDeviceChooserController::use_test_scan_duration_ = false;
namespace {
constexpr size_t kMaxLengthForDeviceName =
29; // max length of device name in filter.
// The duration of a Bluetooth Scan in seconds.
constexpr int kScanDuration = 60;
constexpr int kTestScanDuration = 0;
void LogRequestDeviceOptions(
const blink::mojom::WebBluetoothRequestDeviceOptionsPtr& options) {
VLOG(1) << "requestDevice called with the following filters: ";
int i = 0;
for (const auto& filter : options->filters) {
VLOG(1) << "Filter #" << ++i;
if (!filter->name.is_null())
VLOG(1) << "Name: " << filter->name;
if (!filter->name_prefix.is_null())
VLOG(1) << "Name Prefix: " << filter->name_prefix;
if (!filter->services.is_null()) {
VLOG(1) << "Services: ";
VLOG(1) << "\t[";
for (const auto& service : filter->services)
VLOG(1) << "\t\t" << service->canonical_value();
VLOG(1) << "\t]";
}
}
}
bool IsEmptyOrInvalidFilter(
const blink::mojom::WebBluetoothScanFilterPtr& filter) {
// At least one member needs to be present.
if (filter->name.is_null() && filter->name_prefix.is_null() &&
filter->services.is_null())
return true;
// The renderer will never send a name or a name_prefix longer than
// kMaxLengthForDeviceName.
if (!filter->name.is_null() && filter->name.size() > kMaxLengthForDeviceName)
return true;
if (!filter->name_prefix.is_null() && filter->name_prefix.size() == 0)
return true;
if (!filter->name_prefix.is_null() &&
filter->name_prefix.size() > kMaxLengthForDeviceName)
return true;
return false;
}
bool HasEmptyOrInvalidFilter(
const mojo::Array<blink::mojom::WebBluetoothScanFilterPtr>& filters) {
return filters.empty()
? true
: filters.end() != std::find_if(filters.begin(), filters.end(),
IsEmptyOrInvalidFilter);
}
bool MatchesFilter(const std::string* device_name,
const UUIDSet& device_uuids,
const blink::mojom::WebBluetoothScanFilterPtr& filter) {
if (!filter->name.is_null()) {
if (device_name == nullptr)
return false;
if (filter->name != *device_name)
return false;
}
if (!filter->name_prefix.is_null() && filter->name_prefix.size()) {
if (device_name == nullptr)
return false;
if (!base::StartsWith(*device_name, filter->name_prefix.get(),
base::CompareCase::SENSITIVE))
return false;
}
if (!filter->services.is_null()) {
for (const base::Optional<BluetoothUUID>& service : filter->services) {
if (!base::ContainsKey(device_uuids, service.value())) {
return false;
}
}
}
return true;
}
bool MatchesFilters(
const std::string* device_name,
const UUIDSet& device_uuids,
const mojo::Array<blink::mojom::WebBluetoothScanFilterPtr>& filters) {
DCHECK(!HasEmptyOrInvalidFilter(filters));
for (const auto& filter : filters) {
if (MatchesFilter(device_name, device_uuids, filter)) {
return true;
}
}
return false;
}
std::unique_ptr<device::BluetoothDiscoveryFilter> ComputeScanFilter(
const mojo::Array<blink::mojom::WebBluetoothScanFilterPtr>& filters) {
std::unordered_set<BluetoothUUID, device::BluetoothUUIDHash> services;
for (const auto& filter : filters) {
for (const base::Optional<BluetoothUUID>& service : filter->services) {
services.insert(service.value());
}
}
// There isn't much support for GATT over BR/EDR from neither platforms nor
// devices so performing a Dual scan will find devices that the API is not
// able to interact with. To avoid wasting power and confusing users with
// devices they are not able to interact with, we only perform an LE Scan.
auto discovery_filter = base::MakeUnique<device::BluetoothDiscoveryFilter>(
device::BLUETOOTH_TRANSPORT_LE);
for (const BluetoothUUID& service : services) {
discovery_filter->AddUUID(service);
}
return discovery_filter;
}
void StopDiscoverySession(
std::unique_ptr<device::BluetoothDiscoverySession> discovery_session) {
// Nothing goes wrong if the discovery session fails to stop, and we don't
// need to wait for it before letting the user's script proceed, so we ignore
// the results here.
discovery_session->Stop(base::Bind(&base::DoNothing),
base::Bind(&base::DoNothing));
}
UMARequestDeviceOutcome OutcomeFromChooserEvent(BluetoothChooser::Event event) {
switch (event) {
case BluetoothChooser::Event::DENIED_PERMISSION:
return UMARequestDeviceOutcome::BLUETOOTH_CHOOSER_DENIED_PERMISSION;
case BluetoothChooser::Event::CANCELLED:
return UMARequestDeviceOutcome::BLUETOOTH_CHOOSER_CANCELLED;
case BluetoothChooser::Event::SHOW_OVERVIEW_HELP:
return UMARequestDeviceOutcome::BLUETOOTH_OVERVIEW_HELP_LINK_PRESSED;
case BluetoothChooser::Event::SHOW_ADAPTER_OFF_HELP:
return UMARequestDeviceOutcome::ADAPTER_OFF_HELP_LINK_PRESSED;
case BluetoothChooser::Event::SHOW_NEED_LOCATION_HELP:
return UMARequestDeviceOutcome::NEED_LOCATION_HELP_LINK_PRESSED;
case BluetoothChooser::Event::SELECTED:
// We can't know if we are going to send a success message yet because
// the device could have vanished. This event should be histogramed
// manually after checking if the device is still around.
NOTREACHED();
return UMARequestDeviceOutcome::SUCCESS;
case BluetoothChooser::Event::RESCAN:
return UMARequestDeviceOutcome::BLUETOOTH_CHOOSER_RESCAN;
}
NOTREACHED();
return UMARequestDeviceOutcome::SUCCESS;
}
void RecordScanningDuration(const base::TimeDelta& duration) {
UMA_HISTOGRAM_LONG_TIMES("Bluetooth.Web.RequestDevice.ScanningDuration",
duration);
}
} // namespace
BluetoothDeviceChooserController::BluetoothDeviceChooserController(
WebBluetoothServiceImpl* web_bluetooth_service,
RenderFrameHost* render_frame_host,
device::BluetoothAdapter* adapter)
: adapter_(adapter),
web_bluetooth_service_(web_bluetooth_service),
render_frame_host_(render_frame_host),
web_contents_(WebContents::FromRenderFrameHost(render_frame_host_)),
discovery_session_timer_(
FROM_HERE,
// TODO(jyasskin): Add a way for tests to control the dialog
// directly, and change this to a reasonable discovery timeout.
base::TimeDelta::FromSeconds(
use_test_scan_duration_ ? kTestScanDuration : kScanDuration),
base::Bind(&BluetoothDeviceChooserController::StopDeviceDiscovery,
// base::Timer guarantees it won't call back after its
// destructor starts.
base::Unretained(this)),
/*is_repeating=*/false),
weak_ptr_factory_(this) {
CHECK(adapter_);
}
BluetoothDeviceChooserController::~BluetoothDeviceChooserController() {
if (scanning_start_time_) {
RecordScanningDuration(base::TimeTicks::Now() -
scanning_start_time_.value());
}
if (chooser_) {
DCHECK(!error_callback_.is_null());
error_callback_.Run(blink::mojom::WebBluetoothResult::CHOOSER_CANCELLED);
}
}
void BluetoothDeviceChooserController::GetDevice(
blink::mojom::WebBluetoothRequestDeviceOptionsPtr options,
const SuccessCallback& success_callback,
const ErrorCallback& error_callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// GetDevice should only be called once.
DCHECK(success_callback_.is_null());
DCHECK(error_callback_.is_null());
success_callback_ = success_callback;
error_callback_ = error_callback;
// The renderer should never send empty filters.
if (HasEmptyOrInvalidFilter(options->filters)) {
web_bluetooth_service_->CrashRendererAndClosePipe(
bad_message::BDH_EMPTY_OR_INVALID_FILTERS);
return;
}
options_ = std::move(options);
LogRequestDeviceOptions(options_);
// Check blacklist to reject invalid filters and adjust optional_services.
if (BluetoothBlacklist::Get().IsExcluded(options_->filters)) {
RecordRequestDeviceOutcome(
UMARequestDeviceOutcome::BLACKLISTED_SERVICE_IN_FILTER);
PostErrorCallback(
blink::mojom::WebBluetoothResult::REQUEST_DEVICE_WITH_BLACKLISTED_UUID);
return;
}
BluetoothBlacklist::Get().RemoveExcludedUUIDs(options_.get());
const url::Origin requesting_origin =
render_frame_host_->GetLastCommittedOrigin();
const url::Origin embedding_origin =
web_contents_->GetMainFrame()->GetLastCommittedOrigin();
// TODO(crbug.com/518042): Enforce correctly-delegated permissions instead of
// matching origins. When relaxing this, take care to handle non-sandboxed
// unique origins.
if (!embedding_origin.IsSameOriginWith(requesting_origin)) {
PostErrorCallback(blink::mojom::WebBluetoothResult::
REQUEST_DEVICE_FROM_CROSS_ORIGIN_IFRAME);
return;
}
// The above also excludes unique origins, which are not even same-origin with
// themselves.
DCHECK(!requesting_origin.unique());
if (!adapter_->IsPresent()) {
VLOG(1) << "Bluetooth Adapter not present. Can't serve requestDevice.";
RecordRequestDeviceOutcome(
UMARequestDeviceOutcome::BLUETOOTH_ADAPTER_NOT_PRESENT);
PostErrorCallback(blink::mojom::WebBluetoothResult::NO_BLUETOOTH_ADAPTER);
return;
}
switch (GetContentClient()->browser()->AllowWebBluetooth(
web_contents_->GetBrowserContext(), requesting_origin,
embedding_origin)) {
case ContentBrowserClient::AllowWebBluetoothResult::BLOCK_POLICY: {
RecordRequestDeviceOutcome(
UMARequestDeviceOutcome::BLUETOOTH_CHOOSER_POLICY_DISABLED);
PostErrorCallback(blink::mojom::WebBluetoothResult::
CHOOSER_NOT_SHOWN_API_LOCALLY_DISABLED);
return;
}
case ContentBrowserClient::AllowWebBluetoothResult::
BLOCK_GLOBALLY_DISABLED: {
// Log to the developer console.
web_contents_->GetMainFrame()->AddMessageToConsole(
content::CONSOLE_MESSAGE_LEVEL_LOG,
"Bluetooth permission has been blocked.");
// Block requests.
RecordRequestDeviceOutcome(
UMARequestDeviceOutcome::BLUETOOTH_GLOBALLY_DISABLED);
PostErrorCallback(blink::mojom::WebBluetoothResult::
CHOOSER_NOT_SHOWN_API_GLOBALLY_DISABLED);
return;
}
case ContentBrowserClient::AllowWebBluetoothResult::ALLOW:
break;
}
BluetoothChooser::EventHandler chooser_event_handler =
base::Bind(&BluetoothDeviceChooserController::OnBluetoothChooserEvent,
base::Unretained(this));
if (WebContentsDelegate* delegate = web_contents_->GetDelegate()) {
chooser_ = delegate->RunBluetoothChooser(render_frame_host_,
chooser_event_handler);
}
if (!chooser_.get()) {
PostErrorCallback(
blink::mojom::WebBluetoothResult::WEB_BLUETOOTH_NOT_SUPPORTED);
return;
}
if (!chooser_->CanAskForScanningPermission()) {
VLOG(1) << "Closing immediately because Chooser cannot obtain permission.";
OnBluetoothChooserEvent(BluetoothChooser::Event::DENIED_PERMISSION,
"" /* device_address */);
return;
}
PopulateConnectedDevices();
if (!chooser_.get()) {
// If the dialog's closing, no need to do any of the rest of this.
return;
}
if (!adapter_->IsPowered()) {
chooser_->SetAdapterPresence(
BluetoothChooser::AdapterPresence::POWERED_OFF);
return;
}
StartDeviceDiscovery();
}
void BluetoothDeviceChooserController::AddFilteredDevice(
const device::BluetoothDevice& device) {
base::Optional<std::string> device_name = device.GetName();
if (chooser_.get() &&
MatchesFilters(device_name ? &device_name.value() : nullptr,
device.GetUUIDs(), options_->filters)) {
base::Optional<int8_t> rssi = device.GetInquiryRSSI();
chooser_->AddOrUpdateDevice(
device.GetAddress(), !!device.GetName() /* should_update_name */,
device.GetNameForDisplay(), device.IsGattConnected(),
web_bluetooth_service_->IsDevicePaired(device.GetAddress()),
rssi ? CalculateSignalStrengthLevel(rssi.value()) : -1);
}
}
void BluetoothDeviceChooserController::AdapterPoweredChanged(bool powered) {
if (!powered && discovery_session_.get()) {
StopDiscoverySession(std::move(discovery_session_));
}
if (chooser_.get()) {
chooser_->SetAdapterPresence(
powered ? BluetoothChooser::AdapterPresence::POWERED_ON
: BluetoothChooser::AdapterPresence::POWERED_OFF);
if (powered) {
OnBluetoothChooserEvent(BluetoothChooser::Event::RESCAN,
"" /* device_address */);
}
}
if (!powered) {
discovery_session_timer_.Stop();
}
}
int BluetoothDeviceChooserController::CalculateSignalStrengthLevel(
int8_t rssi) {
RecordRSSISignalStrength(rssi);
if (rssi <= kMinRSSI) {
RecordRSSISignalStrengthLevel(
UMARSSISignalStrengthLevel::LESS_THAN_OR_EQUAL_TO_MIN_RSSI);
return 0;
}
if (rssi >= kMaxRSSI) {
RecordRSSISignalStrengthLevel(
UMARSSISignalStrengthLevel::GREATER_THAN_OR_EQUAL_TO_MAX_RSSI);
return kNumSignalStrengthLevels - 1;
}
double input_range = kMaxRSSI - kMinRSSI;
double output_range = kNumSignalStrengthLevels - 1;
int level = static_cast<int>((rssi - kMinRSSI) * output_range / input_range);
DCHECK(kNumSignalStrengthLevels == arraysize(kRSSISignalStrengthEnumTable));
RecordRSSISignalStrengthLevel(kRSSISignalStrengthEnumTable[level]);
return level;
}
void BluetoothDeviceChooserController::SetTestScanDurationForTesting() {
BluetoothDeviceChooserController::use_test_scan_duration_ = true;
}
void BluetoothDeviceChooserController::PopulateConnectedDevices() {
for (const device::BluetoothDevice* device : adapter_->GetDevices()) {
if (device->IsGattConnected()) {
AddFilteredDevice(*device);
}
}
}
void BluetoothDeviceChooserController::StartDeviceDiscovery() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (discovery_session_.get() && discovery_session_->IsActive()) {
// Already running; just increase the timeout.
discovery_session_timer_.Reset();
return;
}
scanning_start_time_ = base::TimeTicks::Now();
chooser_->ShowDiscoveryState(BluetoothChooser::DiscoveryState::DISCOVERING);
adapter_->StartDiscoverySessionWithFilter(
ComputeScanFilter(options_->filters),
base::Bind(
&BluetoothDeviceChooserController::OnStartDiscoverySessionSuccess,
weak_ptr_factory_.GetWeakPtr()),
base::Bind(
&BluetoothDeviceChooserController::OnStartDiscoverySessionFailed,
weak_ptr_factory_.GetWeakPtr()));
}
void BluetoothDeviceChooserController::StopDeviceDiscovery() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (scanning_start_time_) {
RecordScanningDuration(base::TimeTicks::Now() -
scanning_start_time_.value());
scanning_start_time_.reset();
}
StopDiscoverySession(std::move(discovery_session_));
if (chooser_) {
chooser_->ShowDiscoveryState(BluetoothChooser::DiscoveryState::IDLE);
}
}
void BluetoothDeviceChooserController::OnStartDiscoverySessionSuccess(
std::unique_ptr<device::BluetoothDiscoverySession> discovery_session) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
VLOG(1) << "Started discovery session.";
if (chooser_.get()) {
discovery_session_ = std::move(discovery_session);
discovery_session_timer_.Reset();
} else {
StopDiscoverySession(std::move(discovery_session));
}
}
void BluetoothDeviceChooserController::OnStartDiscoverySessionFailed() {
if (chooser_.get()) {
chooser_->ShowDiscoveryState(
BluetoothChooser::DiscoveryState::FAILED_TO_START);
}
}
void BluetoothDeviceChooserController::OnBluetoothChooserEvent(
BluetoothChooser::Event event,
const std::string& device_address) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Shouldn't recieve an event from a closed chooser.
DCHECK(chooser_.get());
switch (event) {
case BluetoothChooser::Event::RESCAN:
RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event));
PopulateConnectedDevices();
DCHECK(chooser_);
StartDeviceDiscovery();
// No need to close the chooser so we return.
return;
case BluetoothChooser::Event::DENIED_PERMISSION:
RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event));
PostErrorCallback(blink::mojom::WebBluetoothResult::
CHOOSER_NOT_SHOWN_USER_DENIED_PERMISSION_TO_SCAN);
break;
case BluetoothChooser::Event::CANCELLED:
RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event));
PostErrorCallback(blink::mojom::WebBluetoothResult::CHOOSER_CANCELLED);
break;
case BluetoothChooser::Event::SHOW_OVERVIEW_HELP:
VLOG(1) << "Overview Help link pressed.";
RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event));
PostErrorCallback(blink::mojom::WebBluetoothResult::CHOOSER_CANCELLED);
break;
case BluetoothChooser::Event::SHOW_ADAPTER_OFF_HELP:
VLOG(1) << "Adapter Off Help link pressed.";
RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event));
PostErrorCallback(blink::mojom::WebBluetoothResult::CHOOSER_CANCELLED);
break;
case BluetoothChooser::Event::SHOW_NEED_LOCATION_HELP:
VLOG(1) << "Need Location Help link pressed.";
RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event));
PostErrorCallback(blink::mojom::WebBluetoothResult::CHOOSER_CANCELLED);
break;
case BluetoothChooser::Event::SELECTED:
// RecordRequestDeviceOutcome is called in the callback, because the
// device may have vanished.
PostSuccessCallback(device_address);
break;
}
// Close chooser.
chooser_.reset();
}
void BluetoothDeviceChooserController::PostSuccessCallback(
const std::string& device_address) {
if (!base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::Bind(success_callback_, base::Passed(std::move(options_)),
device_address))) {
LOG(WARNING) << "No TaskRunner.";
}
}
void BluetoothDeviceChooserController::PostErrorCallback(
blink::mojom::WebBluetoothResult error) {
if (!base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(error_callback_, error))) {
LOG(WARNING) << "No TaskRunner.";
}
}
} // namespace content