Chromium Code Reviews| Index: content/browser/bluetooth/bluetooth_device_provider.cc |
| diff --git a/content/browser/bluetooth/bluetooth_device_provider.cc b/content/browser/bluetooth/bluetooth_device_provider.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..4967abebf9cddc964af10a26bffe3d09c3de72dc |
| --- /dev/null |
| +++ b/content/browser/bluetooth/bluetooth_device_provider.cc |
| @@ -0,0 +1,445 @@ |
| +// 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_provider.h" |
| + |
| +#include <set> |
| +#include <string> |
| +#include <unordered_set> |
| + |
| +#include "base/bind.h" |
| +#include "base/bind_helpers.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/first_device_bluetooth_chooser.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_discovery_session.h" |
| + |
| +namespace content { |
| + |
| +namespace { |
| +constexpr size_t kMaxLengthForDeviceName = |
| + 29; // max length of device name in filter. |
| + |
| +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; |
| + VLOG(1) << "\t]"; |
| + } |
| + } |
| +} |
| + |
| +bool IsEmptyOrInvalidFilter( |
| + const blink::mojom::WebBluetoothScanFilterPtr& filter) { |
| + return (filter->name.is_null() && filter->name_prefix.is_null() && |
|
Jeffrey Yasskin
2016/05/13 04:41:58
I'd rather a series of 'if' statements, but this i
ortuno
2016/05/13 20:11:17
Changed to ifs.
|
| + filter->services.is_null()) || |
| + (!filter->name.is_null() && |
| + filter->name.size() > kMaxLengthForDeviceName) || |
| + (!filter->name_prefix.is_null() && |
| + filter->name_prefix.size() > kMaxLengthForDeviceName); |
| +} |
| + |
| +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 device::BluetoothDevice& device, |
| + const blink::mojom::WebBluetoothScanFilterPtr& filter) { |
| + DCHECK(!IsEmptyOrInvalidFilter(filter)); |
| + |
| + const std::string device_name = base::UTF16ToUTF8(device.GetName()); |
| + |
| + if (!filter->name.is_null() && (device_name != filter->name)) { |
| + return false; |
| + } |
| + |
| + if (!filter->name_prefix.is_null() && |
| + (!base::StartsWith(device_name, filter->name_prefix.get(), |
| + base::CompareCase::SENSITIVE))) { |
| + return false; |
| + } |
| + |
| + if (!filter->services.is_null()) { |
| + const auto& device_uuid_list = device.GetUUIDs(); |
| + const std::set<device::BluetoothUUID> device_uuids(device_uuid_list.begin(), |
| + device_uuid_list.end()); |
| + for (const auto& service : filter->services) { |
| + if (!ContainsKey(device_uuids, device::BluetoothUUID(service))) { |
| + return false; |
| + } |
| + } |
| + } |
| + |
| + return true; |
| +} |
| + |
| +bool MatchesFilters( |
| + const device::BluetoothDevice& device, |
| + const mojo::Array<blink::mojom::WebBluetoothScanFilterPtr>& filters) { |
| + DCHECK(!HasEmptyOrInvalidFilter(filters)); |
| + for (const auto& filter : filters) { |
| + if (MatchesFilter(device, filter)) { |
| + return true; |
| + } |
| + } |
| + return false; |
| +} |
| + |
| +std::unique_ptr<device::BluetoothDiscoveryFilter> ComputeScanFilter( |
| + const mojo::Array<blink::mojom::WebBluetoothScanFilterPtr>& filters) { |
| + std::unordered_set<std::string> services; |
| + for (const auto& filter : filters) { |
| + for (const std::string& service : filter->services) { |
| + services.insert(service); |
| + } |
| + } |
| + std::unique_ptr<device::BluetoothDiscoveryFilter> discovery_filter( |
| + new device::BluetoothDiscoveryFilter( |
|
Jeffrey Yasskin
2016/05/13 04:41:59
This can now be
auto discovery_filter = base::M
ortuno
2016/05/13 20:11:17
Nice. Done.
|
| + device::BluetoothDiscoveryFilter::TRANSPORT_DUAL)); |
| + for (const std::string& service : services) { |
| + discovery_filter->AddUUID(device::BluetoothUUID(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: |
| + // Rescanning doesn't result in a IPC message for the request being sent |
| + // so no need to histogram it. |
| + NOTREACHED(); |
| + return UMARequestDeviceOutcome::SUCCESS; |
| + } |
| + NOTREACHED(); |
| + return UMARequestDeviceOutcome::SUCCESS; |
| +} |
| + |
| +} // namespace |
| + |
| +BluetoothDeviceProvider::BluetoothDeviceProvider( |
| + RenderFrameHost* render_frame_host, |
| + device::BluetoothAdapter* adapter, |
| + int scan_duration) |
| + : adapter_(adapter), |
| + 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::FromSecondsD(scan_duration), |
| + base::Bind(&BluetoothDeviceProvider::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_); |
| +} |
| + |
| +BluetoothDeviceProvider::~BluetoothDeviceProvider() {} |
| + |
| +void BluetoothDeviceProvider::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. |
| + CHECK(!HasEmptyOrInvalidFilter(options->filters)); |
|
Jeffrey Yasskin
2016/05/13 04:41:59
We shouldn't CHECK that the renderer is behaving w
ortuno
2016/05/13 20:11:17
Done. This required passing in the WebBluetoothSer
|
| + options_ = std::move(options); |
| + LogRequestDeviceOptions(options_); |
| + |
| + // Check blacklist to reject invalid filters and adjust optional_services. |
| + if (BluetoothBlacklist::Get().IsExcluded(options_->filters)) { |
|
Jeffrey Yasskin
2016/05/13 04:41:59
The comment on this function says that the UUIDs m
ortuno
2016/05/13 20:11:17
Added a check for UUID validity to IsEmptyOrInvali
|
| + RecordRequestDeviceOutcome( |
| + UMARequestDeviceOutcome::BLACKLISTED_SERVICE_IN_FILTER); |
| + PostErrorCallback( |
| + blink::mojom::WebBluetoothError::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::WebBluetoothError:: |
| + 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::WebBluetoothError::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::WebBluetoothError:: |
| + 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::WebBluetoothError:: |
| + CHOOSER_NOT_SHOWN_API_GLOBALLY_DISABLED); |
| + return; |
| + } |
| + case ContentBrowserClient::AllowWebBluetoothResult::ALLOW: |
| + break; |
| + } |
| + |
| + BluetoothChooser::EventHandler chooser_event_handler = |
| + base::Bind(&BluetoothDeviceProvider::OnBluetoothChooserEvent, |
| + base::Unretained(this)); |
| + |
| + if (WebContentsDelegate* delegate = web_contents_->GetDelegate()) { |
| + chooser_ = delegate->RunBluetoothChooser(render_frame_host_, |
| + chooser_event_handler); |
| + } |
| + |
| + if (!chooser_.get()) { |
| + LOG(WARNING) |
| + << "No Bluetooth chooser implementation; falling back to first device."; |
| + chooser_.reset(new FirstDeviceBluetoothChooser(chooser_event_handler)); |
| + } |
| + |
| + if (!chooser_->CanAskForScanningPermission()) { |
| + VLOG(1) << "Closing immediately because Chooser cannot obtain permission."; |
| + OnBluetoothChooserEvent(BluetoothChooser::Event::DENIED_PERMISSION, |
| + "" /* device_address */); |
| + return; |
| + } |
| + |
| + // Populate the initial list of devices. |
| + VLOG(1) << "Populating " << adapter_->GetDevices().size() |
| + << " devices in chooser."; |
| + for (const device::BluetoothDevice* device : adapter_->GetDevices()) { |
| + AddFilteredDevice(*device); |
| + } |
| + |
| + 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 BluetoothDeviceProvider::AddFilteredDevice( |
| + const device::BluetoothDevice& device) { |
| + if (chooser_.get() && MatchesFilters(device, options_->filters)) { |
| + VLOG(1) << "Adding device to chooser: " << device.GetAddress(); |
| + chooser_->AddDevice(device.GetAddress(), device.GetName()); |
| + } |
| +} |
| + |
| +void BluetoothDeviceProvider::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) { |
| + discovery_session_timer_.Stop(); |
| + } |
| +} |
| + |
| +void BluetoothDeviceProvider::StartDeviceDiscovery() { |
| + DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| + |
| + if (discovery_session_.get() && discovery_session_->IsActive()) { |
| + // Already running; just increase the timeout. |
| + discovery_session_timer_.Reset(); |
| + return; |
| + } |
| + |
| + chooser_->ShowDiscoveryState(BluetoothChooser::DiscoveryState::DISCOVERING); |
| + adapter_->StartDiscoverySessionWithFilter( |
| + ComputeScanFilter(options_->filters), |
| + base::Bind(&BluetoothDeviceProvider::OnStartDiscoverySessionSuccess, |
| + weak_ptr_factory_.GetWeakPtr()), |
| + base::Bind(&BluetoothDeviceProvider::OnStartDiscoverySessionFailed, |
| + weak_ptr_factory_.GetWeakPtr())); |
| +} |
| + |
| +void BluetoothDeviceProvider::StopDeviceDiscovery() { |
| + DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| + StopDiscoverySession(std::move(discovery_session_)); |
| + if (chooser_) { |
| + chooser_->ShowDiscoveryState(BluetoothChooser::DiscoveryState::IDLE); |
| + } |
| +} |
| + |
| +void BluetoothDeviceProvider::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 BluetoothDeviceProvider::OnStartDiscoverySessionFailed() { |
| + if (chooser_.get()) { |
| + chooser_->ShowDiscoveryState( |
| + BluetoothChooser::DiscoveryState::FAILED_TO_START); |
| + } |
| +} |
| + |
| +void BluetoothDeviceProvider::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: |
| + StartDeviceDiscovery(); |
| + // No need to close the chooser so we return. |
| + return; |
| + case BluetoothChooser::Event::DENIED_PERMISSION: |
| + RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event)); |
| + PostErrorCallback(blink::mojom::WebBluetoothError:: |
| + CHOOSER_NOT_SHOWN_USER_DENIED_PERMISSION_TO_SCAN); |
| + break; |
| + case BluetoothChooser::Event::CANCELLED: |
| + RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event)); |
| + PostErrorCallback(blink::mojom::WebBluetoothError::CHOOSER_CANCELLED); |
| + break; |
| + case BluetoothChooser::Event::SHOW_OVERVIEW_HELP: |
| + VLOG(1) << "Overview Help link pressed."; |
| + RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event)); |
| + PostErrorCallback(blink::mojom::WebBluetoothError::CHOOSER_CANCELLED); |
| + break; |
| + case BluetoothChooser::Event::SHOW_ADAPTER_OFF_HELP: |
| + VLOG(1) << "Adapter Off Help link pressed."; |
| + RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event)); |
| + PostErrorCallback(blink::mojom::WebBluetoothError::CHOOSER_CANCELLED); |
| + break; |
| + case BluetoothChooser::Event::SHOW_NEED_LOCATION_HELP: |
| + VLOG(1) << "Need Location Help link pressed."; |
| + RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event)); |
| + PostErrorCallback(blink::mojom::WebBluetoothError::CHOOSER_CANCELLED); |
| + break; |
| + case BluetoothChooser::Event::SELECTED: |
| + PostSuccessCallback(device_address); |
| + break; |
| + } |
| + // Close chooser. |
| + chooser_.reset(); |
| +} |
| + |
| +void BluetoothDeviceProvider::PostSuccessCallback( |
| + const std::string& device_address) { |
| + if (!base::ThreadTaskRunnerHandle::Get()->PostTask( |
| + FROM_HERE, base::Bind(success_callback_, device_address))) { |
| + LOG(WARNING) << "No TaskRunner."; |
| + } |
| +} |
| + |
| +void BluetoothDeviceProvider::PostErrorCallback( |
| + blink::mojom::WebBluetoothError error) { |
| + if (!base::ThreadTaskRunnerHandle::Get()->PostTask( |
| + FROM_HERE, base::Bind(error_callback_, error))) { |
| + LOG(WARNING) << "No TaskRunner."; |
| + } |
| +} |
| + |
| +} // namespace content |