| // 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. |
| |
| // ID Not In Map Note: |
| // A service, characteristic, or descriptor ID not in the corresponding |
| // WebBluetoothServiceImpl map [service_id_to_device_address_, |
| // characteristic_id_to_service_id_, descriptor_to_characteristic_] implies a |
| // hostile renderer because a renderer obtains the corresponding ID from this |
| // class and it will be added to the map at that time. |
| |
| #include "content/browser/bluetooth/web_bluetooth_service_impl.h" |
| |
| #include <algorithm> |
| |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "content/browser/bluetooth/bluetooth_adapter_factory_wrapper.h" |
| #include "content/browser/bluetooth/bluetooth_blacklist.h" |
| #include "content/browser/bluetooth/bluetooth_device_chooser_controller.h" |
| #include "content/browser/bluetooth/bluetooth_metrics.h" |
| #include "content/browser/bluetooth/frame_connected_bluetooth_devices.h" |
| #include "content/browser/renderer_host/render_process_host_impl.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "device/bluetooth/bluetooth_remote_gatt_characteristic.h" |
| |
| using device::BluetoothUUID; |
| |
| namespace content { |
| |
| namespace { |
| |
| blink::mojom::WebBluetoothError TranslateConnectErrorAndRecord( |
| device::BluetoothDevice::ConnectErrorCode error_code) { |
| switch (error_code) { |
| case device::BluetoothDevice::ERROR_UNKNOWN: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::UNKNOWN); |
| return blink::mojom::WebBluetoothError::CONNECT_UNKNOWN_ERROR; |
| case device::BluetoothDevice::ERROR_INPROGRESS: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::IN_PROGRESS); |
| return blink::mojom::WebBluetoothError::CONNECT_ALREADY_IN_PROGRESS; |
| case device::BluetoothDevice::ERROR_FAILED: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::FAILED); |
| return blink::mojom::WebBluetoothError::CONNECT_UNKNOWN_FAILURE; |
| case device::BluetoothDevice::ERROR_AUTH_FAILED: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::AUTH_FAILED); |
| return blink::mojom::WebBluetoothError::CONNECT_AUTH_FAILED; |
| case device::BluetoothDevice::ERROR_AUTH_CANCELED: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::AUTH_CANCELED); |
| return blink::mojom::WebBluetoothError::CONNECT_AUTH_CANCELED; |
| case device::BluetoothDevice::ERROR_AUTH_REJECTED: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::AUTH_REJECTED); |
| return blink::mojom::WebBluetoothError::CONNECT_AUTH_REJECTED; |
| case device::BluetoothDevice::ERROR_AUTH_TIMEOUT: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::AUTH_TIMEOUT); |
| return blink::mojom::WebBluetoothError::CONNECT_AUTH_TIMEOUT; |
| case device::BluetoothDevice::ERROR_UNSUPPORTED_DEVICE: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::UNSUPPORTED_DEVICE); |
| return blink::mojom::WebBluetoothError::CONNECT_UNSUPPORTED_DEVICE; |
| case device::BluetoothDevice::ERROR_ATTRIBUTE_LENGTH_INVALID: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::ATTRIBUTE_LENGTH_INVALID); |
| return blink::mojom::WebBluetoothError::CONNECT_ATTRIBUTE_LENGTH_INVALID; |
| case device::BluetoothDevice::ERROR_CONNECTION_CONGESTED: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::CONNECTION_CONGESTED); |
| return blink::mojom::WebBluetoothError::CONNECT_CONNECTION_CONGESTED; |
| case device::BluetoothDevice::ERROR_INSUFFICIENT_ENCRYPTION: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::INSUFFICIENT_ENCRYPTION); |
| return blink::mojom::WebBluetoothError::CONNECT_INSUFFICIENT_ENCRYPTION; |
| case device::BluetoothDevice::ERROR_OFFSET_INVALID: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::OFFSET_INVALID); |
| return blink::mojom::WebBluetoothError::CONNECT_OFFSET_INVALID; |
| case device::BluetoothDevice::ERROR_READ_NOT_PERMITTED: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::READ_NOT_PERMITTED); |
| return blink::mojom::WebBluetoothError::CONNECT_READ_NOT_PERMITTED; |
| case device::BluetoothDevice::ERROR_REQUEST_NOT_SUPPORTED: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::REQUEST_NOT_SUPPORTED); |
| return blink::mojom::WebBluetoothError::CONNECT_REQUEST_NOT_SUPPORTED; |
| case device::BluetoothDevice::ERROR_WRITE_NOT_PERMITTED: |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::WRITE_NOT_PERMITTED); |
| return blink::mojom::WebBluetoothError::CONNECT_WRITE_NOT_PERMITTED; |
| case device::BluetoothDevice::NUM_CONNECT_ERROR_CODES: |
| NOTREACHED(); |
| return blink::mojom::WebBluetoothError::UNTRANSLATED_CONNECT_ERROR_CODE; |
| } |
| NOTREACHED(); |
| return blink::mojom::WebBluetoothError::UNTRANSLATED_CONNECT_ERROR_CODE; |
| } |
| |
| blink::mojom::WebBluetoothError TranslateGATTErrorAndRecord( |
| device::BluetoothRemoteGattService::GattErrorCode error_code, |
| UMAGATTOperation operation) { |
| switch (error_code) { |
| case device::BluetoothRemoteGattService::GATT_ERROR_UNKNOWN: |
| RecordGATTOperationOutcome(operation, UMAGATTOperationOutcome::UNKNOWN); |
| return blink::mojom::WebBluetoothError::GATT_UNKNOWN_ERROR; |
| case device::BluetoothRemoteGattService::GATT_ERROR_FAILED: |
| RecordGATTOperationOutcome(operation, UMAGATTOperationOutcome::FAILED); |
| return blink::mojom::WebBluetoothError::GATT_UNKNOWN_FAILURE; |
| case device::BluetoothRemoteGattService::GATT_ERROR_IN_PROGRESS: |
| RecordGATTOperationOutcome(operation, |
| UMAGATTOperationOutcome::IN_PROGRESS); |
| return blink::mojom::WebBluetoothError::GATT_OPERATION_IN_PROGRESS; |
| case device::BluetoothRemoteGattService::GATT_ERROR_INVALID_LENGTH: |
| RecordGATTOperationOutcome(operation, |
| UMAGATTOperationOutcome::INVALID_LENGTH); |
| return blink::mojom::WebBluetoothError::GATT_INVALID_ATTRIBUTE_LENGTH; |
| case device::BluetoothRemoteGattService::GATT_ERROR_NOT_PERMITTED: |
| RecordGATTOperationOutcome(operation, |
| UMAGATTOperationOutcome::NOT_PERMITTED); |
| return blink::mojom::WebBluetoothError::GATT_NOT_PERMITTED; |
| case device::BluetoothRemoteGattService::GATT_ERROR_NOT_AUTHORIZED: |
| RecordGATTOperationOutcome(operation, |
| UMAGATTOperationOutcome::NOT_AUTHORIZED); |
| return blink::mojom::WebBluetoothError::GATT_NOT_AUTHORIZED; |
| case device::BluetoothRemoteGattService::GATT_ERROR_NOT_PAIRED: |
| RecordGATTOperationOutcome(operation, |
| UMAGATTOperationOutcome::NOT_PAIRED); |
| return blink::mojom::WebBluetoothError::GATT_NOT_PAIRED; |
| case device::BluetoothRemoteGattService::GATT_ERROR_NOT_SUPPORTED: |
| RecordGATTOperationOutcome(operation, |
| UMAGATTOperationOutcome::NOT_SUPPORTED); |
| return blink::mojom::WebBluetoothError::GATT_NOT_SUPPORTED; |
| } |
| NOTREACHED(); |
| return blink::mojom::WebBluetoothError::GATT_UNTRANSLATED_ERROR_CODE; |
| } |
| |
| // TODO(ortuno): This should really be a BluetoothDevice method. |
| // Replace when implemented. http://crbug.com/552022 |
| std::vector<device::BluetoothRemoteGattCharacteristic*> |
| GetCharacteristicsByUUID(device::BluetoothRemoteGattService* service, |
| const BluetoothUUID& characteristic_uuid) { |
| std::vector<device::BluetoothRemoteGattCharacteristic*> characteristics; |
| VLOG(1) << "Looking for characteristic: " |
| << characteristic_uuid.canonical_value(); |
| for (device::BluetoothRemoteGattCharacteristic* characteristic : |
| service->GetCharacteristics()) { |
| VLOG(1) << "Characteristic in cache: " |
| << characteristic->GetUUID().canonical_value(); |
| if (characteristic->GetUUID() == characteristic_uuid) { |
| characteristics.push_back(characteristic); |
| } |
| } |
| return characteristics; |
| } |
| |
| // TODO(ortuno): This should really be a BluetoothDevice method. |
| // Replace when implemented. http://crbug.com/552022 |
| std::vector<device::BluetoothRemoteGattService*> GetPrimaryServicesByUUID( |
| device::BluetoothDevice* device, |
| const BluetoothUUID& service_uuid) { |
| std::vector<device::BluetoothRemoteGattService*> services; |
| VLOG(1) << "Looking for service: " << service_uuid.canonical_value(); |
| for (device::BluetoothRemoteGattService* service : |
| device->GetGattServices()) { |
| VLOG(1) << "Service in cache: " << service->GetUUID().canonical_value(); |
| if (service->GetUUID() == service_uuid && service->IsPrimary()) { |
| services.push_back(service); |
| } |
| } |
| return services; |
| } |
| |
| // TODO(ortuno): This should really be a BluetoothDevice method. |
| // Replace when implemented. http://crbug.com/552022 |
| std::vector<device::BluetoothRemoteGattService*> GetPrimaryServices( |
| device::BluetoothDevice* device) { |
| std::vector<device::BluetoothRemoteGattService*> services; |
| VLOG(1) << "Looking for services."; |
| for (device::BluetoothRemoteGattService* service : |
| device->GetGattServices()) { |
| VLOG(1) << "Service in cache: " << service->GetUUID().canonical_value(); |
| if (service->IsPrimary()) { |
| services.push_back(service); |
| } |
| } |
| return services; |
| } |
| |
| } // namespace |
| |
| WebBluetoothServiceImpl::WebBluetoothServiceImpl( |
| RenderFrameHost* render_frame_host, |
| blink::mojom::WebBluetoothServiceRequest request) |
| : WebContentsObserver(WebContents::FromRenderFrameHost(render_frame_host)), |
| connected_devices_(new FrameConnectedBluetoothDevices(render_frame_host)), |
| render_frame_host_(render_frame_host), |
| binding_(this, std::move(request)), |
| weak_ptr_factory_(this) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CHECK(web_contents()); |
| } |
| |
| WebBluetoothServiceImpl::~WebBluetoothServiceImpl() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| ClearState(); |
| } |
| |
| void WebBluetoothServiceImpl::SetClientConnectionErrorHandler( |
| base::Closure closure) { |
| binding_.set_connection_error_handler(closure); |
| } |
| |
| void WebBluetoothServiceImpl::DidFinishNavigation( |
| NavigationHandle* navigation_handle) { |
| if (navigation_handle->HasCommitted() && |
| navigation_handle->GetRenderFrameHost() == render_frame_host_ && |
| !navigation_handle->IsSamePage()) { |
| ClearState(); |
| } |
| } |
| |
| void WebBluetoothServiceImpl::AdapterPoweredChanged( |
| device::BluetoothAdapter* adapter, |
| bool powered) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (device_chooser_controller_.get()) { |
| device_chooser_controller_->AdapterPoweredChanged(powered); |
| } |
| } |
| |
| void WebBluetoothServiceImpl::DeviceAdded(device::BluetoothAdapter* adapter, |
| device::BluetoothDevice* device) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (device_chooser_controller_.get()) { |
| VLOG(1) << "Adding device to device chooser controller: " |
| << device->GetAddress(); |
| device_chooser_controller_->AddFilteredDevice(*device); |
| } |
| } |
| |
| void WebBluetoothServiceImpl::DeviceChanged(device::BluetoothAdapter* adapter, |
| device::BluetoothDevice* device) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!device->IsGattConnected()) { |
| std::string device_id = |
| connected_devices_->CloseConnectionToDeviceWithAddress( |
| device->GetAddress()); |
| if (!device_id.empty()) { |
| if (client_) { |
| client_->GattServerDisconnected(device_id); |
| } |
| } |
| } |
| } |
| |
| void WebBluetoothServiceImpl::GattServicesDiscovered( |
| device::BluetoothAdapter* adapter, |
| device::BluetoothDevice* device) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| const std::string& device_address = device->GetAddress(); |
| VLOG(1) << "Services discovered for device: " << device_address; |
| |
| auto iter = pending_primary_services_requests_.find(device_address); |
| if (iter == pending_primary_services_requests_.end()) { |
| return; |
| } |
| std::vector<PrimaryServicesRequestCallback> requests = |
| std::move(iter->second); |
| pending_primary_services_requests_.erase(iter); |
| |
| for (const PrimaryServicesRequestCallback& request : requests) { |
| request.Run(device); |
| } |
| |
| // Sending get-service responses unexpectedly queued another request. |
| DCHECK(!ContainsKey(pending_primary_services_requests_, device_address)); |
| } |
| |
| void WebBluetoothServiceImpl::GattCharacteristicValueChanged( |
| device::BluetoothAdapter* adapter, |
| device::BluetoothRemoteGattCharacteristic* characteristic, |
| const std::vector<uint8_t>& value) { |
| // Don't notify of characteristics that we haven't returned. |
| if (!ContainsKey(characteristic_id_to_service_id_, |
| characteristic->GetIdentifier())) { |
| return; |
| } |
| |
| // On Chrome OS and Linux, GattCharacteristicValueChanged is called before the |
| // success callback for ReadRemoteCharacteristic is called, which could result |
| // in an event being fired before the readValue promise is resolved. |
| if (!base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::Bind(&WebBluetoothServiceImpl::NotifyCharacteristicValueChanged, |
| weak_ptr_factory_.GetWeakPtr(), |
| characteristic->GetIdentifier(), value))) { |
| LOG(WARNING) << "No TaskRunner."; |
| } |
| } |
| |
| void WebBluetoothServiceImpl::NotifyCharacteristicValueChanged( |
| const std::string& characteristic_instance_id, |
| std::vector<uint8_t> value) { |
| if (client_) { |
| client_->RemoteCharacteristicValueChanged( |
| characteristic_instance_id, mojo::Array<uint8_t>(std::move(value))); |
| } |
| } |
| |
| void WebBluetoothServiceImpl::SetClient( |
| blink::mojom::WebBluetoothServiceClientAssociatedPtrInfo client) { |
| DCHECK(!client_.get()); |
| client_.Bind(std::move(client)); |
| } |
| |
| void WebBluetoothServiceImpl::RequestDevice( |
| blink::mojom::WebBluetoothRequestDeviceOptionsPtr options, |
| const RequestDeviceCallback& callback) { |
| RecordWebBluetoothFunctionCall(UMAWebBluetoothFunction::REQUEST_DEVICE); |
| RecordRequestDeviceOptions(options); |
| |
| if (!GetAdapter()) { |
| if (GetBluetoothAdapterFactoryWrapper()->IsBluetoothAdapterAvailable()) { |
| GetBluetoothAdapterFactoryWrapper()->AcquireAdapter( |
| this, base::Bind(&WebBluetoothServiceImpl::RequestDeviceImpl, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Passed(std::move(options)), callback)); |
| return; |
| } |
| RecordRequestDeviceOutcome(UMARequestDeviceOutcome::NO_BLUETOOTH_ADAPTER); |
| callback.Run(blink::mojom::WebBluetoothError::NO_BLUETOOTH_ADAPTER, |
| nullptr /* device */); |
| return; |
| } |
| RequestDeviceImpl(std::move(options), callback, GetAdapter()); |
| } |
| |
| void WebBluetoothServiceImpl::RemoteServerConnect( |
| const mojo::String& device_id, |
| const RemoteServerConnectCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordWebBluetoothFunctionCall(UMAWebBluetoothFunction::CONNECT_GATT); |
| |
| const CacheQueryResult query_result = QueryCacheForDevice(device_id); |
| |
| if (query_result.outcome != CacheQueryOutcome::SUCCESS) { |
| RecordConnectGATTOutcome(query_result.outcome); |
| callback.Run(query_result.GetWebError()); |
| return; |
| } |
| |
| if (connected_devices_->IsConnectedToDeviceWithId(device_id)) { |
| VLOG(1) << "Already connected."; |
| callback.Run(blink::mojom::WebBluetoothError::SUCCESS); |
| return; |
| } |
| |
| // It's possible for WebBluetoothServiceImpl to issue two successive |
| // connection requests for which it would get two successive responses |
| // and consequently try to insert two BluetoothGattConnections for the |
| // same device. WebBluetoothServiceImpl should reject or queue connection |
| // requests if there is a pending connection already, but the platform |
| // abstraction doesn't currently support checking for pending connections. |
| // TODO(ortuno): CHECK that this never happens once the platform |
| // abstraction allows to check for pending connections. |
| // http://crbug.com/583544 |
| const base::TimeTicks start_time = base::TimeTicks::Now(); |
| query_result.device->CreateGattConnection( |
| base::Bind(&WebBluetoothServiceImpl::OnCreateGATTConnectionSuccess, |
| weak_ptr_factory_.GetWeakPtr(), device_id, start_time, |
| callback), |
| base::Bind(&WebBluetoothServiceImpl::OnCreateGATTConnectionFailed, |
| weak_ptr_factory_.GetWeakPtr(), device_id, start_time, |
| callback)); |
| } |
| |
| void WebBluetoothServiceImpl::RemoteServerDisconnect( |
| const mojo::String& device_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordWebBluetoothFunctionCall( |
| UMAWebBluetoothFunction::REMOTE_GATT_SERVER_DISCONNECT); |
| |
| if (connected_devices_->IsConnectedToDeviceWithId(device_id)) { |
| VLOG(1) << "Disconnecting device: " << device_id; |
| connected_devices_->CloseConnectionToDeviceWithId(device_id); |
| } |
| } |
| |
| void WebBluetoothServiceImpl::RemoteServerGetPrimaryServices( |
| const mojo::String& device_id, |
| blink::mojom::WebBluetoothGATTQueryQuantity quantity, |
| const base::Optional<BluetoothUUID>& services_uuid, |
| const RemoteServerGetPrimaryServicesCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordWebBluetoothFunctionCall( |
| quantity == blink::mojom::WebBluetoothGATTQueryQuantity::SINGLE |
| ? UMAWebBluetoothFunction::GET_PRIMARY_SERVICE |
| : UMAWebBluetoothFunction::GET_PRIMARY_SERVICES); |
| RecordGetPrimaryServicesServices(quantity, services_uuid); |
| |
| if (services_uuid && |
| !allowed_devices_map_.IsOriginAllowedToAccessService( |
| GetOrigin(), device_id, services_uuid.value())) { |
| callback.Run(blink::mojom::WebBluetoothError::NOT_ALLOWED_TO_ACCESS_SERVICE, |
| nullptr /* service */); |
| return; |
| } |
| |
| const CacheQueryResult query_result = QueryCacheForDevice(device_id); |
| |
| if (query_result.outcome == CacheQueryOutcome::BAD_RENDERER) { |
| return; |
| } |
| |
| if (query_result.outcome != CacheQueryOutcome::SUCCESS) { |
| RecordGetPrimaryServicesOutcome(quantity, query_result.outcome); |
| callback.Run(query_result.GetWebError(), nullptr /* service */); |
| return; |
| } |
| |
| const std::string& device_address = query_result.device->GetAddress(); |
| |
| // We can't know if a service is present or not until GATT service discovery |
| // is complete for the device. |
| if (query_result.device->IsGattServicesDiscoveryComplete()) { |
| RemoteServerGetPrimaryServicesImpl(device_id, quantity, services_uuid, |
| callback, query_result.device); |
| return; |
| } |
| |
| VLOG(1) << "Services not yet discovered."; |
| pending_primary_services_requests_[device_address].push_back(base::Bind( |
| &WebBluetoothServiceImpl::RemoteServerGetPrimaryServicesImpl, |
| base::Unretained(this), device_id, quantity, services_uuid, callback)); |
| } |
| |
| void WebBluetoothServiceImpl::RemoteServiceGetCharacteristics( |
| const mojo::String& service_instance_id, |
| blink::mojom::WebBluetoothGATTQueryQuantity quantity, |
| const base::Optional<BluetoothUUID>& characteristics_uuid, |
| const RemoteServiceGetCharacteristicsCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| RecordWebBluetoothFunctionCall( |
| quantity == blink::mojom::WebBluetoothGATTQueryQuantity::SINGLE |
| ? UMAWebBluetoothFunction::SERVICE_GET_CHARACTERISTIC |
| : UMAWebBluetoothFunction::SERVICE_GET_CHARACTERISTICS); |
| RecordGetCharacteristicsCharacteristic(quantity, characteristics_uuid); |
| |
| if (characteristics_uuid && |
| BluetoothBlacklist::Get().IsExcluded(characteristics_uuid.value())) { |
| RecordGetCharacteristicsOutcome(quantity, |
| UMAGetCharacteristicOutcome::BLACKLISTED); |
| callback.Run( |
| blink::mojom::WebBluetoothError::BLACKLISTED_CHARACTERISTIC_UUID, |
| nullptr /* characteristics */); |
| return; |
| } |
| |
| const CacheQueryResult query_result = |
| QueryCacheForService(service_instance_id); |
| |
| if (query_result.outcome == CacheQueryOutcome::BAD_RENDERER) { |
| return; |
| } |
| |
| if (query_result.outcome != CacheQueryOutcome::SUCCESS) { |
| RecordGetCharacteristicsOutcome(quantity, query_result.outcome); |
| callback.Run(query_result.GetWebError(), nullptr /* characteristics */); |
| return; |
| } |
| |
| std::vector<device::BluetoothRemoteGattCharacteristic*> characteristics = |
| characteristics_uuid |
| ? GetCharacteristicsByUUID(query_result.service, |
| characteristics_uuid.value()) |
| : query_result.service->GetCharacteristics(); |
| |
| mojo::Array<blink::mojom::WebBluetoothRemoteGATTCharacteristicPtr> |
| response_characteristics; |
| for (device::BluetoothRemoteGattCharacteristic* characteristic : |
| characteristics) { |
| if (BluetoothBlacklist::Get().IsExcluded(characteristic->GetUUID())) { |
| continue; |
| } |
| std::string characteristic_instance_id = characteristic->GetIdentifier(); |
| auto insert_result = characteristic_id_to_service_id_.insert( |
| std::make_pair(characteristic_instance_id, service_instance_id)); |
| // If value is already in map, DCHECK it's valid. |
| if (!insert_result.second) |
| DCHECK(insert_result.first->second == service_instance_id); |
| |
| blink::mojom::WebBluetoothRemoteGATTCharacteristicPtr characteristic_ptr = |
| blink::mojom::WebBluetoothRemoteGATTCharacteristic::New(); |
| characteristic_ptr->instance_id = characteristic_instance_id; |
| characteristic_ptr->uuid = characteristic->GetUUID().canonical_value(); |
| characteristic_ptr->properties = |
| static_cast<uint32_t>(characteristic->GetProperties()); |
| response_characteristics.push_back(std::move(characteristic_ptr)); |
| |
| if (quantity == blink::mojom::WebBluetoothGATTQueryQuantity::SINGLE) { |
| break; |
| } |
| } |
| |
| if (!response_characteristics.empty()) { |
| RecordGetCharacteristicsOutcome(quantity, |
| UMAGetCharacteristicOutcome::SUCCESS); |
| callback.Run(blink::mojom::WebBluetoothError::SUCCESS, |
| std::move(response_characteristics)); |
| return; |
| } |
| |
| RecordGetCharacteristicsOutcome( |
| quantity, characteristics_uuid |
| ? UMAGetCharacteristicOutcome::NOT_FOUND |
| : UMAGetCharacteristicOutcome::NO_CHARACTERISTICS); |
| callback.Run(characteristics_uuid |
| ? blink::mojom::WebBluetoothError::CHARACTERISTIC_NOT_FOUND |
| : blink::mojom::WebBluetoothError::NO_CHARACTERISTICS_FOUND, |
| nullptr /* characteristics */); |
| } |
| |
| void WebBluetoothServiceImpl::RemoteCharacteristicReadValue( |
| const mojo::String& characteristic_instance_id, |
| const RemoteCharacteristicReadValueCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordWebBluetoothFunctionCall( |
| UMAWebBluetoothFunction::CHARACTERISTIC_READ_VALUE); |
| |
| const CacheQueryResult query_result = |
| QueryCacheForCharacteristic(characteristic_instance_id); |
| |
| if (query_result.outcome == CacheQueryOutcome::BAD_RENDERER) { |
| return; |
| } |
| |
| if (query_result.outcome != CacheQueryOutcome::SUCCESS) { |
| RecordCharacteristicReadValueOutcome(query_result.outcome); |
| callback.Run(query_result.GetWebError(), nullptr /* value */); |
| return; |
| } |
| |
| if (BluetoothBlacklist::Get().IsExcludedFromReads( |
| query_result.characteristic->GetUUID())) { |
| RecordCharacteristicReadValueOutcome(UMAGATTOperationOutcome::BLACKLISTED); |
| callback.Run(blink::mojom::WebBluetoothError::BLACKLISTED_READ, |
| nullptr /* value */); |
| return; |
| } |
| |
| query_result.characteristic->ReadRemoteCharacteristic( |
| base::Bind(&WebBluetoothServiceImpl::OnReadValueSuccess, |
| weak_ptr_factory_.GetWeakPtr(), callback), |
| base::Bind(&WebBluetoothServiceImpl::OnReadValueFailed, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void WebBluetoothServiceImpl::RemoteCharacteristicWriteValue( |
| const mojo::String& characteristic_instance_id, |
| mojo::Array<uint8_t> value, |
| const RemoteCharacteristicWriteValueCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordWebBluetoothFunctionCall( |
| UMAWebBluetoothFunction::CHARACTERISTIC_WRITE_VALUE); |
| |
| // We perform the length check on the renderer side. So if we |
| // get a value with length > 512, we can assume it's a hostile |
| // renderer and kill it. |
| if (value.size() > 512) { |
| CrashRendererAndClosePipe(bad_message::BDH_INVALID_WRITE_VALUE_LENGTH); |
| return; |
| } |
| |
| const CacheQueryResult query_result = |
| QueryCacheForCharacteristic(characteristic_instance_id); |
| |
| if (query_result.outcome == CacheQueryOutcome::BAD_RENDERER) { |
| return; |
| } |
| |
| if (query_result.outcome != CacheQueryOutcome::SUCCESS) { |
| RecordCharacteristicWriteValueOutcome(query_result.outcome); |
| callback.Run(query_result.GetWebError()); |
| return; |
| } |
| |
| if (BluetoothBlacklist::Get().IsExcludedFromWrites( |
| query_result.characteristic->GetUUID())) { |
| RecordCharacteristicWriteValueOutcome(UMAGATTOperationOutcome::BLACKLISTED); |
| callback.Run(blink::mojom::WebBluetoothError::BLACKLISTED_WRITE); |
| return; |
| } |
| |
| query_result.characteristic->WriteRemoteCharacteristic( |
| value.To<std::vector<uint8_t>>(), |
| base::Bind(&WebBluetoothServiceImpl::OnWriteValueSuccess, |
| weak_ptr_factory_.GetWeakPtr(), callback), |
| base::Bind(&WebBluetoothServiceImpl::OnWriteValueFailed, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void WebBluetoothServiceImpl::RemoteCharacteristicStartNotifications( |
| const mojo::String& characteristic_instance_id, |
| const RemoteCharacteristicStartNotificationsCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordWebBluetoothFunctionCall( |
| UMAWebBluetoothFunction::CHARACTERISTIC_START_NOTIFICATIONS); |
| |
| auto iter = |
| characteristic_id_to_notify_session_.find(characteristic_instance_id); |
| if (iter != characteristic_id_to_notify_session_.end() && |
| iter->second->IsActive()) { |
| // If the frame has already started notifications and the notifications |
| // are active we return SUCCESS. |
| callback.Run(blink::mojom::WebBluetoothError::SUCCESS); |
| return; |
| } |
| |
| const CacheQueryResult query_result = |
| QueryCacheForCharacteristic(characteristic_instance_id); |
| |
| if (query_result.outcome == CacheQueryOutcome::BAD_RENDERER) { |
| return; |
| } |
| |
| if (query_result.outcome != CacheQueryOutcome::SUCCESS) { |
| RecordStartNotificationsOutcome(query_result.outcome); |
| callback.Run(query_result.GetWebError()); |
| return; |
| } |
| |
| device::BluetoothRemoteGattCharacteristic::Properties notify_or_indicate = |
| query_result.characteristic->GetProperties() & |
| (device::BluetoothRemoteGattCharacteristic::PROPERTY_NOTIFY | |
| device::BluetoothRemoteGattCharacteristic::PROPERTY_INDICATE); |
| if (!notify_or_indicate) { |
| callback.Run(blink::mojom::WebBluetoothError::GATT_NOT_SUPPORTED); |
| return; |
| } |
| |
| query_result.characteristic->StartNotifySession( |
| base::Bind(&WebBluetoothServiceImpl::OnStartNotifySessionSuccess, |
| weak_ptr_factory_.GetWeakPtr(), callback), |
| base::Bind(&WebBluetoothServiceImpl::OnStartNotifySessionFailed, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void WebBluetoothServiceImpl::RemoteCharacteristicStopNotifications( |
| const mojo::String& characteristic_instance_id, |
| const RemoteCharacteristicStopNotificationsCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordWebBluetoothFunctionCall( |
| UMAWebBluetoothFunction::CHARACTERISTIC_STOP_NOTIFICATIONS); |
| |
| const CacheQueryResult query_result = |
| QueryCacheForCharacteristic(characteristic_instance_id); |
| |
| if (query_result.outcome == CacheQueryOutcome::BAD_RENDERER) { |
| return; |
| } |
| |
| auto notify_session_iter = |
| characteristic_id_to_notify_session_.find(characteristic_instance_id); |
| if (notify_session_iter == characteristic_id_to_notify_session_.end()) { |
| // If the frame hasn't subscribed to notifications before we just |
| // run the callback. |
| callback.Run(); |
| return; |
| } |
| notify_session_iter->second->Stop(base::Bind( |
| &WebBluetoothServiceImpl::OnStopNotifySessionComplete, |
| weak_ptr_factory_.GetWeakPtr(), characteristic_instance_id, callback)); |
| } |
| |
| void WebBluetoothServiceImpl::RequestDeviceImpl( |
| blink::mojom::WebBluetoothRequestDeviceOptionsPtr options, |
| const RequestDeviceCallback& callback, |
| device::BluetoothAdapter* adapter) { |
| // requestDevice() can only be called when processing a user-gesture and any |
| // user gesture outside of a chooser should close the chooser. This does |
| // not happen on all platforms so we don't DCHECK that the old one is closed. |
| // We destroy the old chooser before constructing the new one to make sure |
| // they can't conflict. |
| device_chooser_controller_.reset(); |
| |
| device_chooser_controller_.reset(new BluetoothDeviceChooserController( |
| this, render_frame_host_, adapter, |
| GetBluetoothAdapterFactoryWrapper()->GetScanDuration())); |
| |
| device_chooser_controller_->GetDevice( |
| std::move(options), |
| base::Bind(&WebBluetoothServiceImpl::OnGetDeviceSuccess, |
| weak_ptr_factory_.GetWeakPtr(), callback), |
| base::Bind(&WebBluetoothServiceImpl::OnGetDeviceFailed, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void WebBluetoothServiceImpl::RemoteServerGetPrimaryServicesImpl( |
| const mojo::String& device_id, |
| blink::mojom::WebBluetoothGATTQueryQuantity quantity, |
| const base::Optional<BluetoothUUID>& services_uuid, |
| const RemoteServerGetPrimaryServicesCallback& callback, |
| device::BluetoothDevice* device) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(device->IsGattServicesDiscoveryComplete()); |
| |
| std::vector<device::BluetoothRemoteGattService*> services = |
| services_uuid ? GetPrimaryServicesByUUID(device, services_uuid.value()) |
| : GetPrimaryServices(device); |
| |
| mojo::Array<blink::mojom::WebBluetoothRemoteGATTServicePtr> response_services; |
| for (device::BluetoothRemoteGattService* service : services) { |
| if (!allowed_devices_map_.IsOriginAllowedToAccessService( |
| GetOrigin(), device_id, service->GetUUID())) { |
| continue; |
| } |
| std::string service_instance_id = service->GetIdentifier(); |
| const std::string& device_address = device->GetAddress(); |
| auto insert_result = service_id_to_device_address_.insert( |
| make_pair(service_instance_id, device_address)); |
| // If value is already in map, DCHECK it's valid. |
| if (!insert_result.second) |
| DCHECK_EQ(insert_result.first->second, device_address); |
| |
| blink::mojom::WebBluetoothRemoteGATTServicePtr service_ptr = |
| blink::mojom::WebBluetoothRemoteGATTService::New(); |
| service_ptr->instance_id = service_instance_id; |
| service_ptr->uuid = service->GetUUID().canonical_value(); |
| response_services.push_back(std::move(service_ptr)); |
| |
| if (quantity == blink::mojom::WebBluetoothGATTQueryQuantity::SINGLE) { |
| break; |
| } |
| } |
| |
| if (!response_services.empty()) { |
| VLOG(1) << "Services found in device."; |
| RecordGetPrimaryServicesOutcome(quantity, |
| UMAGetPrimaryServiceOutcome::SUCCESS); |
| callback.Run(blink::mojom::WebBluetoothError::SUCCESS, |
| std::move(response_services)); |
| return; |
| } |
| |
| VLOG(1) << "Services not found in device."; |
| RecordGetPrimaryServicesOutcome( |
| quantity, services_uuid ? UMAGetPrimaryServiceOutcome::NOT_FOUND |
| : UMAGetPrimaryServiceOutcome::NO_SERVICES); |
| callback.Run(services_uuid |
| ? blink::mojom::WebBluetoothError::SERVICE_NOT_FOUND |
| : blink::mojom::WebBluetoothError::NO_SERVICES_FOUND, |
| nullptr /* services */); |
| } |
| |
| void WebBluetoothServiceImpl::OnGetDeviceSuccess( |
| const RequestDeviceCallback& callback, |
| blink::mojom::WebBluetoothRequestDeviceOptionsPtr options, |
| const std::string& device_address) { |
| device_chooser_controller_.reset(); |
| |
| const device::BluetoothDevice* const device = |
| GetAdapter()->GetDevice(device_address); |
| if (device == nullptr) { |
| VLOG(1) << "Device " << device_address << " no longer in adapter"; |
| RecordRequestDeviceOutcome(UMARequestDeviceOutcome::CHOSEN_DEVICE_VANISHED); |
| callback.Run(blink::mojom::WebBluetoothError::CHOSEN_DEVICE_VANISHED, |
| nullptr /* device */); |
| return; |
| } |
| |
| const std::string device_id_for_origin = |
| allowed_devices_map_.AddDevice(GetOrigin(), device_address, options); |
| |
| VLOG(1) << "Device: " << device->GetNameForDisplay(); |
| VLOG(1) << "UUIDs: "; |
| |
| mojo::Array<mojo::String> filtered_uuids; |
| for (const BluetoothUUID& uuid : device->GetUUIDs()) { |
| if (allowed_devices_map_.IsOriginAllowedToAccessService( |
| GetOrigin(), device_id_for_origin, uuid)) { |
| VLOG(1) << "\t Allowed: " << uuid.canonical_value(); |
| filtered_uuids.push_back(uuid.canonical_value()); |
| } else { |
| VLOG(1) << "\t Not Allowed: " << uuid.canonical_value(); |
| } |
| } |
| |
| blink::mojom::WebBluetoothDevicePtr device_ptr = |
| blink::mojom::WebBluetoothDevice::New(); |
| device_ptr->id = device_id_for_origin; |
| // TODO(615720): Use the upcoming GetName (was GetDeviceName). |
| device_ptr->name = base::UTF16ToUTF8(device->GetNameForDisplay()); |
| device_ptr->uuids = std::move(filtered_uuids); |
| |
| RecordRequestDeviceOutcome(UMARequestDeviceOutcome::SUCCESS); |
| callback.Run(blink::mojom::WebBluetoothError::SUCCESS, std::move(device_ptr)); |
| } |
| |
| void WebBluetoothServiceImpl::OnGetDeviceFailed( |
| const RequestDeviceCallback& callback, |
| blink::mojom::WebBluetoothError error) { |
| // Errors are recorded by the *device_chooser_controller_. |
| callback.Run(error, nullptr /* device */); |
| device_chooser_controller_.reset(); |
| } |
| |
| void WebBluetoothServiceImpl::OnCreateGATTConnectionSuccess( |
| const std::string& device_id, |
| base::TimeTicks start_time, |
| const RemoteServerConnectCallback& callback, |
| std::unique_ptr<device::BluetoothGattConnection> connection) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordConnectGATTTimeSuccess(base::TimeTicks::Now() - start_time); |
| RecordConnectGATTOutcome(UMAConnectGATTOutcome::SUCCESS); |
| |
| connected_devices_->Insert(device_id, std::move(connection)); |
| callback.Run(blink::mojom::WebBluetoothError::SUCCESS); |
| } |
| |
| void WebBluetoothServiceImpl::OnCreateGATTConnectionFailed( |
| const std::string& device_id, |
| base::TimeTicks start_time, |
| const RemoteServerConnectCallback& callback, |
| device::BluetoothDevice::ConnectErrorCode error_code) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordConnectGATTTimeFailed(base::TimeTicks::Now() - start_time); |
| callback.Run(TranslateConnectErrorAndRecord(error_code)); |
| } |
| |
| void WebBluetoothServiceImpl::OnReadValueSuccess( |
| const RemoteCharacteristicReadValueCallback& callback, |
| const std::vector<uint8_t>& value) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordCharacteristicReadValueOutcome(UMAGATTOperationOutcome::SUCCESS); |
| callback.Run(blink::mojom::WebBluetoothError::SUCCESS, |
| mojo::Array<uint8_t>::From(value)); |
| } |
| |
| void WebBluetoothServiceImpl::OnReadValueFailed( |
| const RemoteCharacteristicReadValueCallback& callback, |
| device::BluetoothRemoteGattService::GattErrorCode error_code) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| callback.Run(TranslateGATTErrorAndRecord( |
| error_code, UMAGATTOperation::CHARACTERISTIC_READ), |
| nullptr /* value */); |
| } |
| |
| void WebBluetoothServiceImpl::OnWriteValueSuccess( |
| const RemoteCharacteristicWriteValueCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RecordCharacteristicWriteValueOutcome(UMAGATTOperationOutcome::SUCCESS); |
| callback.Run(blink::mojom::WebBluetoothError::SUCCESS); |
| } |
| |
| void WebBluetoothServiceImpl::OnWriteValueFailed( |
| const RemoteCharacteristicWriteValueCallback& callback, |
| device::BluetoothRemoteGattService::GattErrorCode error_code) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| callback.Run(TranslateGATTErrorAndRecord( |
| error_code, UMAGATTOperation::CHARACTERISTIC_WRITE)); |
| } |
| |
| void WebBluetoothServiceImpl::OnStartNotifySessionSuccess( |
| const RemoteCharacteristicStartNotificationsCallback& callback, |
| std::unique_ptr<device::BluetoothGattNotifySession> notify_session) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| // Copy Characteristic Instance ID before passing a unique pointer because |
| // compilers may evaluate arguments in any order. |
| std::string characteristic_instance_id = |
| notify_session->GetCharacteristicIdentifier(); |
| // Saving the BluetoothGattNotifySession keeps notifications active. |
| characteristic_id_to_notify_session_[characteristic_instance_id] = |
| std::move(notify_session); |
| callback.Run(blink::mojom::WebBluetoothError::SUCCESS); |
| } |
| |
| void WebBluetoothServiceImpl::OnStartNotifySessionFailed( |
| const RemoteCharacteristicStartNotificationsCallback& callback, |
| device::BluetoothRemoteGattService::GattErrorCode error_code) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| callback.Run(TranslateGATTErrorAndRecord( |
| error_code, UMAGATTOperation::START_NOTIFICATIONS)); |
| } |
| |
| void WebBluetoothServiceImpl::OnStopNotifySessionComplete( |
| const std::string& characteristic_instance_id, |
| const RemoteCharacteristicStopNotificationsCallback& callback) { |
| characteristic_id_to_notify_session_.erase(characteristic_instance_id); |
| callback.Run(); |
| } |
| |
| CacheQueryResult WebBluetoothServiceImpl::QueryCacheForDevice( |
| const std::string& device_id) { |
| const std::string& device_address = |
| allowed_devices_map_.GetDeviceAddress(GetOrigin(), device_id); |
| if (device_address.empty()) { |
| CrashRendererAndClosePipe(bad_message::BDH_DEVICE_NOT_ALLOWED_FOR_ORIGIN); |
| return CacheQueryResult(CacheQueryOutcome::BAD_RENDERER); |
| } |
| |
| CacheQueryResult result; |
| result.device = GetAdapter()->GetDevice(device_address); |
| |
| // When a device can't be found in the BluetoothAdapter, that generally |
| // indicates that it's gone out of range. We reject with a NetworkError in |
| // that case. |
| if (result.device == nullptr) { |
| result.outcome = CacheQueryOutcome::NO_DEVICE; |
| } |
| return result; |
| } |
| |
| CacheQueryResult WebBluetoothServiceImpl::QueryCacheForService( |
| const std::string& service_instance_id) { |
| auto device_iter = service_id_to_device_address_.find(service_instance_id); |
| |
| // Kill the render, see "ID Not in Map Note" above. |
| if (device_iter == service_id_to_device_address_.end()) { |
| CrashRendererAndClosePipe(bad_message::BDH_INVALID_SERVICE_ID); |
| return CacheQueryResult(CacheQueryOutcome::BAD_RENDERER); |
| } |
| |
| const std::string& device_id = |
| allowed_devices_map_.GetDeviceId(GetOrigin(), device_iter->second); |
| // Kill the renderer if origin is not allowed to access the device. |
| if (device_id.empty()) { |
| CrashRendererAndClosePipe(bad_message::BDH_DEVICE_NOT_ALLOWED_FOR_ORIGIN); |
| return CacheQueryResult(CacheQueryOutcome::BAD_RENDERER); |
| } |
| |
| CacheQueryResult result = QueryCacheForDevice(device_id); |
| if (result.outcome != CacheQueryOutcome::SUCCESS) { |
| return result; |
| } |
| |
| result.service = result.device->GetGattService(service_instance_id); |
| if (result.service == nullptr) { |
| result.outcome = CacheQueryOutcome::NO_SERVICE; |
| } else if (!allowed_devices_map_.IsOriginAllowedToAccessService( |
| GetOrigin(), device_id, result.service->GetUUID())) { |
| CrashRendererAndClosePipe(bad_message::BDH_SERVICE_NOT_ALLOWED_FOR_ORIGIN); |
| return CacheQueryResult(CacheQueryOutcome::BAD_RENDERER); |
| } |
| return result; |
| } |
| |
| CacheQueryResult WebBluetoothServiceImpl::QueryCacheForCharacteristic( |
| const std::string& characteristic_instance_id) { |
| auto characteristic_iter = |
| characteristic_id_to_service_id_.find(characteristic_instance_id); |
| |
| // Kill the render, see "ID Not in Map Note" above. |
| if (characteristic_iter == characteristic_id_to_service_id_.end()) { |
| CrashRendererAndClosePipe(bad_message::BDH_INVALID_CHARACTERISTIC_ID); |
| return CacheQueryResult(CacheQueryOutcome::BAD_RENDERER); |
| } |
| |
| CacheQueryResult result = QueryCacheForService(characteristic_iter->second); |
| |
| if (result.outcome != CacheQueryOutcome::SUCCESS) { |
| return result; |
| } |
| |
| result.characteristic = |
| result.service->GetCharacteristic(characteristic_instance_id); |
| |
| if (result.characteristic == nullptr) { |
| result.outcome = CacheQueryOutcome::NO_CHARACTERISTIC; |
| } |
| |
| return result; |
| } |
| |
| RenderProcessHost* WebBluetoothServiceImpl::GetRenderProcessHost() { |
| return render_frame_host_->GetProcess(); |
| } |
| |
| BluetoothAdapterFactoryWrapper* |
| WebBluetoothServiceImpl::GetBluetoothAdapterFactoryWrapper() { |
| RenderProcessHostImpl* render_process_host_impl = |
| static_cast<RenderProcessHostImpl*>(GetRenderProcessHost()); |
| return render_process_host_impl->GetBluetoothAdapterFactoryWrapper(); |
| } |
| |
| device::BluetoothAdapter* WebBluetoothServiceImpl::GetAdapter() { |
| return GetBluetoothAdapterFactoryWrapper()->GetAdapter(this); |
| } |
| |
| void WebBluetoothServiceImpl::CrashRendererAndClosePipe( |
| bad_message::BadMessageReason reason) { |
| bad_message::ReceivedBadMessage(GetRenderProcessHost(), reason); |
| binding_.Close(); |
| } |
| |
| url::Origin WebBluetoothServiceImpl::GetOrigin() { |
| return render_frame_host_->GetLastCommittedOrigin(); |
| } |
| |
| void WebBluetoothServiceImpl::ClearState() { |
| characteristic_id_to_notify_session_.clear(); |
| pending_primary_services_requests_.clear(); |
| characteristic_id_to_service_id_.clear(); |
| service_id_to_device_address_.clear(); |
| connected_devices_.reset( |
| new FrameConnectedBluetoothDevices(render_frame_host_)); |
| allowed_devices_map_ = BluetoothAllowedDevicesMap(); |
| device_chooser_controller_.reset(); |
| GetBluetoothAdapterFactoryWrapper()->ReleaseAdapter(this); |
| } |
| |
| } // namespace content |