OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "content/browser/bluetooth/bluetooth_device_provider.h" | |
6 | |
7 #include <set> | |
8 #include <string> | |
9 #include <unordered_set> | |
10 | |
11 #include "base/bind.h" | |
12 #include "base/bind_helpers.h" | |
13 #include "base/strings/string_util.h" | |
14 #include "base/strings/utf_string_conversions.h" | |
15 #include "content/browser/bluetooth/bluetooth_blacklist.h" | |
16 #include "content/browser/bluetooth/bluetooth_metrics.h" | |
17 #include "content/browser/bluetooth/first_device_bluetooth_chooser.h" | |
18 #include "content/public/browser/browser_thread.h" | |
19 #include "content/public/browser/content_browser_client.h" | |
20 #include "content/public/browser/render_frame_host.h" | |
21 #include "content/public/browser/web_contents.h" | |
22 #include "content/public/browser/web_contents_delegate.h" | |
23 #include "device/bluetooth/bluetooth_adapter.h" | |
24 #include "device/bluetooth/bluetooth_discovery_session.h" | |
25 | |
26 namespace content { | |
27 | |
28 namespace { | |
29 constexpr size_t kMaxLengthForDeviceName = | |
30 29; // max length of device name in filter. | |
31 | |
32 void LogRequestDeviceOptions( | |
33 const blink::mojom::WebBluetoothRequestDeviceOptionsPtr& options) { | |
34 VLOG(1) << "requestDevice called with the following filters: "; | |
35 int i = 0; | |
36 for (const auto& filter : options->filters) { | |
37 VLOG(1) << "Filter #" << ++i; | |
38 if (!filter->name.is_null()) | |
39 VLOG(1) << "Name: " << filter->name; | |
40 | |
41 if (!filter->name_prefix.is_null()) | |
42 VLOG(1) << "Name Prefix: " << filter->name_prefix; | |
43 | |
44 if (!filter->services.is_null()) { | |
45 VLOG(1) << "Services: "; | |
46 VLOG(1) << "\t["; | |
47 for (const auto& service : filter->services) | |
48 VLOG(1) << "\t\t" << service; | |
49 VLOG(1) << "\t]"; | |
50 } | |
51 } | |
52 } | |
53 | |
54 bool IsEmptyOrInvalidFilter( | |
55 const blink::mojom::WebBluetoothScanFilterPtr& filter) { | |
56 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.
| |
57 filter->services.is_null()) || | |
58 (!filter->name.is_null() && | |
59 filter->name.size() > kMaxLengthForDeviceName) || | |
60 (!filter->name_prefix.is_null() && | |
61 filter->name_prefix.size() > kMaxLengthForDeviceName); | |
62 } | |
63 | |
64 bool HasEmptyOrInvalidFilter( | |
65 const mojo::Array<blink::mojom::WebBluetoothScanFilterPtr>& filters) { | |
66 return filters.empty() | |
67 ? true | |
68 : filters.end() != std::find_if(filters.begin(), filters.end(), | |
69 IsEmptyOrInvalidFilter); | |
70 } | |
71 | |
72 bool MatchesFilter(const device::BluetoothDevice& device, | |
73 const blink::mojom::WebBluetoothScanFilterPtr& filter) { | |
74 DCHECK(!IsEmptyOrInvalidFilter(filter)); | |
75 | |
76 const std::string device_name = base::UTF16ToUTF8(device.GetName()); | |
77 | |
78 if (!filter->name.is_null() && (device_name != filter->name)) { | |
79 return false; | |
80 } | |
81 | |
82 if (!filter->name_prefix.is_null() && | |
83 (!base::StartsWith(device_name, filter->name_prefix.get(), | |
84 base::CompareCase::SENSITIVE))) { | |
85 return false; | |
86 } | |
87 | |
88 if (!filter->services.is_null()) { | |
89 const auto& device_uuid_list = device.GetUUIDs(); | |
90 const std::set<device::BluetoothUUID> device_uuids(device_uuid_list.begin(), | |
91 device_uuid_list.end()); | |
92 for (const auto& service : filter->services) { | |
93 if (!ContainsKey(device_uuids, device::BluetoothUUID(service))) { | |
94 return false; | |
95 } | |
96 } | |
97 } | |
98 | |
99 return true; | |
100 } | |
101 | |
102 bool MatchesFilters( | |
103 const device::BluetoothDevice& device, | |
104 const mojo::Array<blink::mojom::WebBluetoothScanFilterPtr>& filters) { | |
105 DCHECK(!HasEmptyOrInvalidFilter(filters)); | |
106 for (const auto& filter : filters) { | |
107 if (MatchesFilter(device, filter)) { | |
108 return true; | |
109 } | |
110 } | |
111 return false; | |
112 } | |
113 | |
114 std::unique_ptr<device::BluetoothDiscoveryFilter> ComputeScanFilter( | |
115 const mojo::Array<blink::mojom::WebBluetoothScanFilterPtr>& filters) { | |
116 std::unordered_set<std::string> services; | |
117 for (const auto& filter : filters) { | |
118 for (const std::string& service : filter->services) { | |
119 services.insert(service); | |
120 } | |
121 } | |
122 std::unique_ptr<device::BluetoothDiscoveryFilter> discovery_filter( | |
123 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.
| |
124 device::BluetoothDiscoveryFilter::TRANSPORT_DUAL)); | |
125 for (const std::string& service : services) { | |
126 discovery_filter->AddUUID(device::BluetoothUUID(service)); | |
127 } | |
128 return discovery_filter; | |
129 } | |
130 | |
131 void StopDiscoverySession( | |
132 std::unique_ptr<device::BluetoothDiscoverySession> discovery_session) { | |
133 // Nothing goes wrong if the discovery session fails to stop, and we don't | |
134 // need to wait for it before letting the user's script proceed, so we ignore | |
135 // the results here. | |
136 discovery_session->Stop(base::Bind(&base::DoNothing), | |
137 base::Bind(&base::DoNothing)); | |
138 } | |
139 | |
140 UMARequestDeviceOutcome OutcomeFromChooserEvent(BluetoothChooser::Event event) { | |
141 switch (event) { | |
142 case BluetoothChooser::Event::DENIED_PERMISSION: | |
143 return UMARequestDeviceOutcome::BLUETOOTH_CHOOSER_DENIED_PERMISSION; | |
144 case BluetoothChooser::Event::CANCELLED: | |
145 return UMARequestDeviceOutcome::BLUETOOTH_CHOOSER_CANCELLED; | |
146 case BluetoothChooser::Event::SHOW_OVERVIEW_HELP: | |
147 return UMARequestDeviceOutcome::BLUETOOTH_OVERVIEW_HELP_LINK_PRESSED; | |
148 case BluetoothChooser::Event::SHOW_ADAPTER_OFF_HELP: | |
149 return UMARequestDeviceOutcome::ADAPTER_OFF_HELP_LINK_PRESSED; | |
150 case BluetoothChooser::Event::SHOW_NEED_LOCATION_HELP: | |
151 return UMARequestDeviceOutcome::NEED_LOCATION_HELP_LINK_PRESSED; | |
152 case BluetoothChooser::Event::SELECTED: | |
153 // We can't know if we are going to send a success message yet because | |
154 // the device could have vanished. This event should be histogramed | |
155 // manually after checking if the device is still around. | |
156 NOTREACHED(); | |
157 return UMARequestDeviceOutcome::SUCCESS; | |
158 case BluetoothChooser::Event::RESCAN: | |
159 // Rescanning doesn't result in a IPC message for the request being sent | |
160 // so no need to histogram it. | |
161 NOTREACHED(); | |
162 return UMARequestDeviceOutcome::SUCCESS; | |
163 } | |
164 NOTREACHED(); | |
165 return UMARequestDeviceOutcome::SUCCESS; | |
166 } | |
167 | |
168 } // namespace | |
169 | |
170 BluetoothDeviceProvider::BluetoothDeviceProvider( | |
171 RenderFrameHost* render_frame_host, | |
172 device::BluetoothAdapter* adapter, | |
173 int scan_duration) | |
174 : adapter_(adapter), | |
175 render_frame_host_(render_frame_host), | |
176 web_contents_(WebContents::FromRenderFrameHost(render_frame_host_)), | |
177 discovery_session_timer_( | |
178 FROM_HERE, | |
179 // TODO(jyasskin): Add a way for tests to control the dialog | |
180 // directly, and change this to a reasonable discovery timeout. | |
181 base::TimeDelta::FromSecondsD(scan_duration), | |
182 base::Bind(&BluetoothDeviceProvider::StopDeviceDiscovery, | |
183 // base::Timer guarantees it won't call back after its | |
184 // destructor starts. | |
185 base::Unretained(this)), | |
186 /*is_repeating=*/false), | |
187 weak_ptr_factory_(this) { | |
188 CHECK(adapter_); | |
189 } | |
190 | |
191 BluetoothDeviceProvider::~BluetoothDeviceProvider() {} | |
192 | |
193 void BluetoothDeviceProvider::GetDevice( | |
194 blink::mojom::WebBluetoothRequestDeviceOptionsPtr options, | |
195 const SuccessCallback& success_callback, | |
196 const ErrorCallback& error_callback) { | |
197 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
198 | |
199 // GetDevice should only be called once. | |
200 DCHECK(success_callback_.is_null()); | |
201 DCHECK(error_callback_.is_null()); | |
202 | |
203 success_callback_ = success_callback; | |
204 error_callback_ = error_callback; | |
205 | |
206 // The renderer should never send empty filters. | |
207 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
| |
208 options_ = std::move(options); | |
209 LogRequestDeviceOptions(options_); | |
210 | |
211 // Check blacklist to reject invalid filters and adjust optional_services. | |
212 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
| |
213 RecordRequestDeviceOutcome( | |
214 UMARequestDeviceOutcome::BLACKLISTED_SERVICE_IN_FILTER); | |
215 PostErrorCallback( | |
216 blink::mojom::WebBluetoothError::REQUEST_DEVICE_WITH_BLACKLISTED_UUID); | |
217 return; | |
218 } | |
219 BluetoothBlacklist::Get().RemoveExcludedUUIDs(options_.get()); | |
220 | |
221 const url::Origin requesting_origin = | |
222 render_frame_host_->GetLastCommittedOrigin(); | |
223 const url::Origin embedding_origin = | |
224 web_contents_->GetMainFrame()->GetLastCommittedOrigin(); | |
225 | |
226 // TODO(crbug.com/518042): Enforce correctly-delegated permissions instead of | |
227 // matching origins. When relaxing this, take care to handle non-sandboxed | |
228 // unique origins. | |
229 if (!embedding_origin.IsSameOriginWith(requesting_origin)) { | |
230 PostErrorCallback(blink::mojom::WebBluetoothError:: | |
231 REQUEST_DEVICE_FROM_CROSS_ORIGIN_IFRAME); | |
232 return; | |
233 } | |
234 // The above also excludes unique origins, which are not even same-origin with | |
235 // themselves. | |
236 DCHECK(!requesting_origin.unique()); | |
237 | |
238 if (!adapter_->IsPresent()) { | |
239 VLOG(1) << "Bluetooth Adapter not present. Can't serve requestDevice."; | |
240 RecordRequestDeviceOutcome( | |
241 UMARequestDeviceOutcome::BLUETOOTH_ADAPTER_NOT_PRESENT); | |
242 PostErrorCallback(blink::mojom::WebBluetoothError::NO_BLUETOOTH_ADAPTER); | |
243 return; | |
244 } | |
245 | |
246 switch (GetContentClient()->browser()->AllowWebBluetooth( | |
247 web_contents_->GetBrowserContext(), requesting_origin, | |
248 embedding_origin)) { | |
249 case ContentBrowserClient::AllowWebBluetoothResult::BLOCK_POLICY: { | |
250 RecordRequestDeviceOutcome( | |
251 UMARequestDeviceOutcome::BLUETOOTH_CHOOSER_POLICY_DISABLED); | |
252 PostErrorCallback(blink::mojom::WebBluetoothError:: | |
253 CHOOSER_NOT_SHOWN_API_LOCALLY_DISABLED); | |
254 return; | |
255 } | |
256 case ContentBrowserClient::AllowWebBluetoothResult:: | |
257 BLOCK_GLOBALLY_DISABLED: { | |
258 // Log to the developer console. | |
259 web_contents_->GetMainFrame()->AddMessageToConsole( | |
260 content::CONSOLE_MESSAGE_LEVEL_LOG, | |
261 "Bluetooth permission has been blocked."); | |
262 // Block requests. | |
263 RecordRequestDeviceOutcome( | |
264 UMARequestDeviceOutcome::BLUETOOTH_GLOBALLY_DISABLED); | |
265 PostErrorCallback(blink::mojom::WebBluetoothError:: | |
266 CHOOSER_NOT_SHOWN_API_GLOBALLY_DISABLED); | |
267 return; | |
268 } | |
269 case ContentBrowserClient::AllowWebBluetoothResult::ALLOW: | |
270 break; | |
271 } | |
272 | |
273 BluetoothChooser::EventHandler chooser_event_handler = | |
274 base::Bind(&BluetoothDeviceProvider::OnBluetoothChooserEvent, | |
275 base::Unretained(this)); | |
276 | |
277 if (WebContentsDelegate* delegate = web_contents_->GetDelegate()) { | |
278 chooser_ = delegate->RunBluetoothChooser(render_frame_host_, | |
279 chooser_event_handler); | |
280 } | |
281 | |
282 if (!chooser_.get()) { | |
283 LOG(WARNING) | |
284 << "No Bluetooth chooser implementation; falling back to first device."; | |
285 chooser_.reset(new FirstDeviceBluetoothChooser(chooser_event_handler)); | |
286 } | |
287 | |
288 if (!chooser_->CanAskForScanningPermission()) { | |
289 VLOG(1) << "Closing immediately because Chooser cannot obtain permission."; | |
290 OnBluetoothChooserEvent(BluetoothChooser::Event::DENIED_PERMISSION, | |
291 "" /* device_address */); | |
292 return; | |
293 } | |
294 | |
295 // Populate the initial list of devices. | |
296 VLOG(1) << "Populating " << adapter_->GetDevices().size() | |
297 << " devices in chooser."; | |
298 for (const device::BluetoothDevice* device : adapter_->GetDevices()) { | |
299 AddFilteredDevice(*device); | |
300 } | |
301 | |
302 if (!chooser_.get()) { | |
303 // If the dialog's closing, no need to do any of the rest of this. | |
304 return; | |
305 } | |
306 | |
307 if (!adapter_->IsPowered()) { | |
308 chooser_->SetAdapterPresence( | |
309 BluetoothChooser::AdapterPresence::POWERED_OFF); | |
310 return; | |
311 } | |
312 | |
313 StartDeviceDiscovery(); | |
314 } | |
315 | |
316 void BluetoothDeviceProvider::AddFilteredDevice( | |
317 const device::BluetoothDevice& device) { | |
318 if (chooser_.get() && MatchesFilters(device, options_->filters)) { | |
319 VLOG(1) << "Adding device to chooser: " << device.GetAddress(); | |
320 chooser_->AddDevice(device.GetAddress(), device.GetName()); | |
321 } | |
322 } | |
323 | |
324 void BluetoothDeviceProvider::AdapterPoweredChanged(bool powered) { | |
325 if (!powered && discovery_session_.get()) { | |
326 StopDiscoverySession(std::move(discovery_session_)); | |
327 } | |
328 | |
329 if (chooser_.get()) { | |
330 chooser_->SetAdapterPresence( | |
331 powered ? BluetoothChooser::AdapterPresence::POWERED_ON | |
332 : BluetoothChooser::AdapterPresence::POWERED_OFF); | |
333 } | |
334 | |
335 if (!powered) { | |
336 discovery_session_timer_.Stop(); | |
337 } | |
338 } | |
339 | |
340 void BluetoothDeviceProvider::StartDeviceDiscovery() { | |
341 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
342 | |
343 if (discovery_session_.get() && discovery_session_->IsActive()) { | |
344 // Already running; just increase the timeout. | |
345 discovery_session_timer_.Reset(); | |
346 return; | |
347 } | |
348 | |
349 chooser_->ShowDiscoveryState(BluetoothChooser::DiscoveryState::DISCOVERING); | |
350 adapter_->StartDiscoverySessionWithFilter( | |
351 ComputeScanFilter(options_->filters), | |
352 base::Bind(&BluetoothDeviceProvider::OnStartDiscoverySessionSuccess, | |
353 weak_ptr_factory_.GetWeakPtr()), | |
354 base::Bind(&BluetoothDeviceProvider::OnStartDiscoverySessionFailed, | |
355 weak_ptr_factory_.GetWeakPtr())); | |
356 } | |
357 | |
358 void BluetoothDeviceProvider::StopDeviceDiscovery() { | |
359 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
360 StopDiscoverySession(std::move(discovery_session_)); | |
361 if (chooser_) { | |
362 chooser_->ShowDiscoveryState(BluetoothChooser::DiscoveryState::IDLE); | |
363 } | |
364 } | |
365 | |
366 void BluetoothDeviceProvider::OnStartDiscoverySessionSuccess( | |
367 std::unique_ptr<device::BluetoothDiscoverySession> discovery_session) { | |
368 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
369 VLOG(1) << "Started discovery session."; | |
370 if (chooser_.get()) { | |
371 discovery_session_ = std::move(discovery_session); | |
372 discovery_session_timer_.Reset(); | |
373 } else { | |
374 StopDiscoverySession(std::move(discovery_session)); | |
375 } | |
376 } | |
377 | |
378 void BluetoothDeviceProvider::OnStartDiscoverySessionFailed() { | |
379 if (chooser_.get()) { | |
380 chooser_->ShowDiscoveryState( | |
381 BluetoothChooser::DiscoveryState::FAILED_TO_START); | |
382 } | |
383 } | |
384 | |
385 void BluetoothDeviceProvider::OnBluetoothChooserEvent( | |
386 BluetoothChooser::Event event, | |
387 const std::string& device_address) { | |
388 DCHECK_CURRENTLY_ON(BrowserThread::UI); | |
389 // Shouldn't recieve an event from a closed chooser. | |
390 DCHECK(chooser_.get()); | |
391 | |
392 switch (event) { | |
393 case BluetoothChooser::Event::RESCAN: | |
394 StartDeviceDiscovery(); | |
395 // No need to close the chooser so we return. | |
396 return; | |
397 case BluetoothChooser::Event::DENIED_PERMISSION: | |
398 RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event)); | |
399 PostErrorCallback(blink::mojom::WebBluetoothError:: | |
400 CHOOSER_NOT_SHOWN_USER_DENIED_PERMISSION_TO_SCAN); | |
401 break; | |
402 case BluetoothChooser::Event::CANCELLED: | |
403 RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event)); | |
404 PostErrorCallback(blink::mojom::WebBluetoothError::CHOOSER_CANCELLED); | |
405 break; | |
406 case BluetoothChooser::Event::SHOW_OVERVIEW_HELP: | |
407 VLOG(1) << "Overview Help link pressed."; | |
408 RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event)); | |
409 PostErrorCallback(blink::mojom::WebBluetoothError::CHOOSER_CANCELLED); | |
410 break; | |
411 case BluetoothChooser::Event::SHOW_ADAPTER_OFF_HELP: | |
412 VLOG(1) << "Adapter Off Help link pressed."; | |
413 RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event)); | |
414 PostErrorCallback(blink::mojom::WebBluetoothError::CHOOSER_CANCELLED); | |
415 break; | |
416 case BluetoothChooser::Event::SHOW_NEED_LOCATION_HELP: | |
417 VLOG(1) << "Need Location Help link pressed."; | |
418 RecordRequestDeviceOutcome(OutcomeFromChooserEvent(event)); | |
419 PostErrorCallback(blink::mojom::WebBluetoothError::CHOOSER_CANCELLED); | |
420 break; | |
421 case BluetoothChooser::Event::SELECTED: | |
422 PostSuccessCallback(device_address); | |
423 break; | |
424 } | |
425 // Close chooser. | |
426 chooser_.reset(); | |
427 } | |
428 | |
429 void BluetoothDeviceProvider::PostSuccessCallback( | |
430 const std::string& device_address) { | |
431 if (!base::ThreadTaskRunnerHandle::Get()->PostTask( | |
432 FROM_HERE, base::Bind(success_callback_, device_address))) { | |
433 LOG(WARNING) << "No TaskRunner."; | |
434 } | |
435 } | |
436 | |
437 void BluetoothDeviceProvider::PostErrorCallback( | |
438 blink::mojom::WebBluetoothError error) { | |
439 if (!base::ThreadTaskRunnerHandle::Get()->PostTask( | |
440 FROM_HERE, base::Bind(error_callback_, error))) { | |
441 LOG(WARNING) << "No TaskRunner."; | |
442 } | |
443 } | |
444 | |
445 } // namespace content | |
OLD | NEW |