Index: content/browser/bluetooth/bluetooth_device_chooser_controller.cc |
diff --git a/content/browser/bluetooth/bluetooth_device_chooser_controller.cc b/content/browser/bluetooth/bluetooth_device_chooser_controller.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..dc255a8e9afd601d84c9cb6cfe9969474fefb0eb |
--- /dev/null |
+++ b/content/browser/bluetooth/bluetooth_device_chooser_controller.cc |
@@ -0,0 +1,483 @@ |
+// 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/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/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_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 IsValidUUID(const mojo::String& uuid) { |
+ device::BluetoothUUID parsed_uuid(uuid); |
+ return parsed_uuid.IsValid() && |
+ parsed_uuid.format() == device::BluetoothUUID::kFormat128Bit; |
+} |
+ |
+bool HasInvalidOptionalServices( |
+ const mojo::Array<mojo::String>& optional_services) { |
+ return optional_services.end() != std::find_if_not(optional_services.begin(), |
+ optional_services.end(), |
+ IsValidUUID); |
+} |
+ |
+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() > kMaxLengthForDeviceName) |
+ return true; |
+ |
+ if (!filter->services.is_null()) { |
+ const auto& services = filter->services; |
+ return services.end() != |
+ std::find_if_not(services.begin(), services.end(), IsValidUUID); |
+ } |
+ |
+ 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 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); |
+ } |
+ } |
+ auto discovery_filter = base::MakeUnique<device::BluetoothDiscoveryFilter>( |
+ 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 |
+ |
+BluetoothDeviceChooserController::BluetoothDeviceChooserController( |
+ WebBluetoothServiceImpl* web_bluetooth_service, |
+ RenderFrameHost* render_frame_host, |
+ device::BluetoothAdapter* adapter, |
+ base::TimeDelta scan_duration) |
+ : 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. |
+ scan_duration, |
+ 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() {} |
+ |
+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) || |
+ HasInvalidOptionalServices(options->optional_services)) { |
+ 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::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(&BluetoothDeviceChooserController::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 BluetoothDeviceChooserController::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 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) { |
+ discovery_session_timer_.Stop(); |
+ } |
+} |
+ |
+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; |
+ } |
+ |
+ 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); |
+ 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: |
+ 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 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::WebBluetoothError error) { |
+ if (!base::ThreadTaskRunnerHandle::Get()->PostTask( |
+ FROM_HERE, base::Bind(error_callback_, error))) { |
+ LOG(WARNING) << "No TaskRunner."; |
+ } |
+} |
+ |
+} // namespace content |