Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qbluetoothdevicediscoveryagent_darwin.mm
Go to the documentation of this file.
1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
6
7#include "darwin/btledeviceinquiry_p.h"
8
9#ifdef Q_OS_MACOS
10#include "darwin/btdeviceinquiry_p.h"
11#include "darwin/btsdpinquiry_p.h"
12
13#include <IOBluetooth/IOBluetooth.h>
14#endif // Q_OS_MACOS
15
17#include "darwin/btnotifier_p.h"
18#include "darwin/btutility_p.h"
19#include "darwin/uistrings_p.h"
21#include "darwin/uistrings_p.h"
23#include "darwin/btraii_p.h"
24#include "qbluetoothuuid.h"
25
26#include <QtCore/qloggingcategory.h>
27#include <QtCore/qcoreapplication.h>
28#include <QtCore/qpermissions.h>
29#include <QtCore/qvector.h>
30#include <QtCore/qglobal.h>
31#include <QtCore/qstring.h>
32#include <QtCore/qdebug.h>
33
34#include <Foundation/Foundation.h>
35
36#include <CoreBluetooth/CoreBluetooth.h>
37
38QT_BEGIN_NAMESPACE
39
40namespace
41{
42
43void registerQDeviceDiscoveryMetaType()
44{
45 static bool initDone = false;
46 if (!initDone) {
47 qRegisterMetaType<QBluetoothDeviceInfo>();
48 qRegisterMetaType<QBluetoothDeviceDiscoveryAgent::Error>();
49 initDone = true;
50 }
51}
52
53} //namespace
54
55QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress &adapter,
56 QBluetoothDeviceDiscoveryAgent *q) :
57 adapterAddress(adapter),
58 agentState(NonActive),
59 lowEnergySearchTimeout(DarwinBluetooth::defaultLEScanTimeoutMS),
60#ifdef Q_OS_MACOS
61 requestedMethods(QBluetoothDeviceDiscoveryAgent::ClassicMethod | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod),
62#else
63 requestedMethods(QBluetoothDeviceDiscoveryAgent::ClassicMethod),
64#endif // Q_OS_MACOS
65 q_ptr(q)
66{
67 registerQDeviceDiscoveryMetaType();
68
69 Q_ASSERT_X(q != nullptr, Q_FUNC_INFO, "invalid q_ptr (null)");
70}
71
72QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate()
73{
74 if (inquiryLE && agentState != NonActive) {
75 // We want the LE scan to stop as soon as possible.
76 if (dispatch_queue_t leQueue = DarwinBluetooth::qt_LE_queue()) {
77 // Local variable to be retained ...
78 DarwinBTLEDeviceInquiry *inq = inquiryLE.getAs<DarwinBTLEDeviceInquiry>();
79 dispatch_sync(leQueue, ^{
80 [inq stop];
81 });
82 }
83 }
84}
85
86bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const
87{
88 if (startPending)
89 return true;
90
91 if (stopPending)
92 return false;
93
94 return agentState != NonActive;
95}
96
97void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
98{
99 using namespace DarwinBluetooth;
100
101 Q_ASSERT(!isActive());
102 Q_ASSERT(methods & (QBluetoothDeviceDiscoveryAgent::ClassicMethod
103 | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod));
104
105#ifdef Q_OS_MACOS
106 if (!controller) {
107 IOBluetoothHostController *hostController = [IOBluetoothHostController defaultController];
108 if (!hostController) {
109 qCWarning(QT_BT_DARWIN) << "No default Bluetooth controller found";
110 setError(QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError);
111 emit q_ptr->errorOccurred(lastError);
112 return;
113 } else if ([hostController powerState] != kBluetoothHCIPowerStateON) {
114 qCWarning(QT_BT_DARWIN) << "Default Bluetooth controller is OFF";
115 setError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
116 emit q_ptr->errorOccurred(lastError);
117 return;
118 } else if (!adapterAddress.isNull()) {
119 // If user has provided the local address, check that it matches with the actual
120 NSString *const hciAddress = [hostController addressAsString];
121 if (adapterAddress != QBluetoothAddress(QString::fromNSString(hciAddress))) {
122 qCWarning(QT_BT_DARWIN) << "Provided address" << adapterAddress
123 << "does not match with adapter:" << hciAddress;
124 setError(QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError);
125 emit q_ptr->errorOccurred(lastError);
126 return;
127 }
128 }
129 controller.reset(hostController, DarwinBluetooth::RetainPolicy::doInitialRetain);
130 }
131#endif // Q_OS_MACOS
132
133 // To be able to scan for devices, iOS requires Info.plist containing
134 // NSBluetoothAlwaysUsageDescription entry with a string, explaining
135 // the usage of Bluetooth interface. macOS also requires this description,
136 // starting from Monterey.
137
138 // No Classic on iOS, and Classic does not require a description on macOS:
139 if (methods.testFlag(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)) {
140 const auto permissionStatus = qApp->checkPermission(QBluetoothPermission{});
141 if (permissionStatus != Qt::PermissionStatus::Granted) {
142 qCWarning(QT_BT_DARWIN,
143 "Use of Bluetooth LE requires explicitly requested permissions.");
144 setError(QBluetoothDeviceDiscoveryAgent::MissingPermissionsError);
145 emit q_ptr->errorOccurred(lastError);
146 // Arguably, Classic scan is still possible, but let's keep the logic
147 // simple.
148 return;
149 }
150 }
151
152 requestedMethods = methods;
153
154 if (stopPending) {
155 startPending = true;
156 return;
157 }
158
159 // This function (re)starts the scan(s) from the scratch;
160 // starting from Classic if it's in 'methods' (or LE scan if not).
161
162 agentState = NonActive;
163 discoveredDevices.clear();
164 setError(QBluetoothDeviceDiscoveryAgent::NoError);
165#ifdef Q_OS_MACOS
166 if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod)
167 return startClassic();
168#endif // Q_OS_MACOS
169
170 startLE();
171}
172
173#ifdef Q_OS_MACOS
174
175void QBluetoothDeviceDiscoveryAgentPrivate::startClassic()
176{
177 Q_ASSERT(!isActive());
178 Q_ASSERT(lastError == QBluetoothDeviceDiscoveryAgent::NoError);
179 Q_ASSERT(requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod);
180 Q_ASSERT(agentState == NonActive);
181
182 DarwinBluetooth::qt_test_iobluetooth_runloop();
183
184 if (!inquiry) {
185 // The first Classic scan for this DDA.
186 inquiry.reset([[DarwinBTClassicDeviceInquiry alloc] initWithDelegate:this],
187 DarwinBluetooth::RetainPolicy::noInitialRetain);
188
189 if (!inquiry) {
190 qCCritical(QT_BT_DARWIN) << "failed to initialize an Classic device inquiry";
191 setError(QBluetoothDeviceDiscoveryAgent::UnknownError,
192 QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED));
193 emit q_ptr->errorOccurred(lastError);
194 return;
195 }
196 }
197
198 agentState = ClassicScan;
199
200 const IOReturn res = [inquiry.getAs<DarwinBTClassicDeviceInquiry>() start];
201 if (res != kIOReturnSuccess) {
202 setError(res, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED));
203 agentState = NonActive;
204 emit q_ptr->errorOccurred(lastError);
205 }
206}
207
208#endif // Q_OS_MACOS
209
210void QBluetoothDeviceDiscoveryAgentPrivate::startLE()
211{
212 Q_ASSERT(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError);
213 Q_ASSERT(requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
214
215 using namespace DarwinBluetooth;
216
217 std::unique_ptr<LECBManagerNotifier> notifier = std::make_unique<LECBManagerNotifier>();
218 // Connections:
219 using ErrMemFunPtr = void (LECBManagerNotifier::*)(QBluetoothDeviceDiscoveryAgent::Error);
220 notifier->connect(notifier.get(), ErrMemFunPtr(&LECBManagerNotifier::CBManagerError),
221 this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError);
222 notifier->connect(notifier.get(), &LECBManagerNotifier::LEnotSupported,
223 this, &QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported);
224 notifier->connect(notifier.get(), &LECBManagerNotifier::discoveryFinished,
225 this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished);
226 using DeviceMemFunPtr = void (QBluetoothDeviceDiscoveryAgentPrivate::*)(const QBluetoothDeviceInfo &);
227 notifier->connect(notifier.get(), &LECBManagerNotifier::deviceDiscovered,
228 this, DeviceMemFunPtr(&QBluetoothDeviceDiscoveryAgentPrivate::deviceFound));
229
230 // Check queue and create scanner:
231 inquiryLE.reset([[DarwinBTLEDeviceInquiry alloc] initWithNotifier:notifier.get()],
232 DarwinBluetooth::RetainPolicy::noInitialRetain);
233 if (inquiryLE)
234 notifier.release(); // Whatever happens next, inquiryLE is already the owner ...
235
236 dispatch_queue_t leQueue(qt_LE_queue());
237 if (!leQueue || !inquiryLE) {
238 setError(QBluetoothDeviceDiscoveryAgent::UnknownError,
239 QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED_LE));
240 agentState = NonActive;
241 emit q_ptr->errorOccurred(lastError);
242 return;
243 }
244
245 // Now start in on LE queue:
246 agentState = LEScan;
247 // We need the local variable so that it's retained ...
248 DarwinBTLEDeviceInquiry *inq = inquiryLE.getAs<DarwinBTLEDeviceInquiry>();
249 dispatch_async(leQueue, ^{
250 [inq startWithTimeout:lowEnergySearchTimeout];
251 });
252}
253
254void QBluetoothDeviceDiscoveryAgentPrivate::stop()
255{
256 Q_ASSERT_X(isActive(), Q_FUNC_INFO, "called whithout active inquiry");
257 Q_ASSERT_X(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError,
258 Q_FUNC_INFO, "called with invalid bluetooth adapter");
259
260 using namespace DarwinBluetooth;
261
262 const bool prevStart = startPending;
263 startPending = false;
264 stopPending = true;
265
266 setError(QBluetoothDeviceDiscoveryAgent::NoError);
267
268#ifdef Q_OS_MACOS
269 if (agentState == ClassicScan) {
270 const IOReturn res = [inquiry.getAs<DarwinBTClassicDeviceInquiry>() stop];
271 if (res != kIOReturnSuccess) {
272 qCWarning(QT_BT_DARWIN) << "failed to stop";
273 startPending = prevStart;
274 stopPending = false;
275 setError(res, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STOPPED));
276 emit q_ptr->errorOccurred(lastError);
277 }
278 } else {
279#else
280 {
281 Q_UNUSED(prevStart);
282#endif // Q_OS_MACOS
283 dispatch_queue_t leQueue(qt_LE_queue());
284 Q_ASSERT(leQueue);
285 // We need the local variable so that it's retained ...
286 DarwinBTLEDeviceInquiry *inq = inquiryLE.getAs<DarwinBTLEDeviceInquiry>();
287 dispatch_sync(leQueue, ^{
288 [inq stop];
289 });
290 // We consider LE scan to be stopped immediately and
291 // do not care about this LEDeviceInquiry object anymore.
292 LEinquiryFinished();
293 }
294}
295
296#ifdef Q_OS_MACOS
297
298void QBluetoothDeviceDiscoveryAgentPrivate::inquiryFinished()
299{
300 // The subsequent start(LE) function (if any)
301 // will (re)set the correct state.
302 agentState = NonActive;
303
304 if (stopPending && !startPending) {
305 stopPending = false;
306 emit q_ptr->canceled();
307 } else if (startPending) {
308 startPending = false;
309 stopPending = false;
310 start(requestedMethods);
311 } else {
312 // We can be here _only_ if a classic scan
313 // finished in a normal way (not cancelled)
314 // and requestedMethods includes LowEnergyMethod.
315 // startLE() will take care of old devices
316 // not supporting Bluetooth 4.0.
317 if (requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)
318 startLE();
319 else
320 emit q_ptr->finished();
321 }
322}
323
324void QBluetoothDeviceDiscoveryAgentPrivate::error(IOReturn error)
325{
326 startPending = false;
327 stopPending = false;
328
329 setError(error);
330
331 emit q_ptr->errorOccurred(lastError);
332}
333
334void QBluetoothDeviceDiscoveryAgentPrivate::classicDeviceFound(void *obj)
335{
336 auto device = static_cast<IOBluetoothDevice *>(obj);
337 Q_ASSERT_X(device, Q_FUNC_INFO, "invalid IOBluetoothDevice (nil)");
338
339 Q_ASSERT_X(agentState == ClassicScan, Q_FUNC_INFO,
340 "invalid agent state (expected classic scan)");
341
342 QT_BT_MAC_AUTORELEASEPOOL;
343
344 // Let's collect some info about this device:
345 const QBluetoothAddress deviceAddress(DarwinBluetooth::qt_address([device getAddress]));
346 if (deviceAddress.isNull()) {
347 qCWarning(QT_BT_DARWIN) << "invalid Bluetooth address";
348 return;
349 }
350
351 QString deviceName;
352 if (device.name)
353 deviceName = QString::fromNSString(device.name);
354
355 const auto classOfDevice = qint32(device.classOfDevice);
356
357 QBluetoothDeviceInfo deviceInfo(deviceAddress, deviceName, classOfDevice);
358 deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration);
359 deviceInfo.setRssi(device.RSSI);
360
361 const QList<QBluetoothUuid> uuids(DarwinBluetooth::extract_services_uuids(device));
362 deviceInfo.setServiceUuids(uuids);
363
364 deviceFound(deviceInfo);
365}
366
367void QBluetoothDeviceDiscoveryAgentPrivate::setError(IOReturn error, const QString &text)
368{
369 if (error == kIOReturnSuccess)
370 setError(QBluetoothDeviceDiscoveryAgent::NoError, text);
371 else if (error == kIOReturnNoPower)
372 setError(QBluetoothDeviceDiscoveryAgent::PoweredOffError, text);
373 else
374 setError(QBluetoothDeviceDiscoveryAgent::UnknownError, text);
375}
376
377#endif // Q_OS_MACOS
378
379void QBluetoothDeviceDiscoveryAgentPrivate::setError(QBluetoothDeviceDiscoveryAgent::Error error, const QString &text)
380{
381 lastError = error;
382
383 if (!text.isEmpty()) {
384 errorString = text;
385 } else {
386 switch (lastError) {
387 case QBluetoothDeviceDiscoveryAgent::NoError:
388 errorString = QString();
389 break;
390 case QBluetoothDeviceDiscoveryAgent::PoweredOffError:
391 errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_POWERED_OFF);
392 break;
393 case QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError:
394 errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_INVALID_ADAPTER);
395 break;
396 case QBluetoothDeviceDiscoveryAgent::InputOutputError:
397 errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_IO);
398 break;
399 case QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError:
400 errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_NOTSUPPORTED);
401 break;
402 case QBluetoothDeviceDiscoveryAgent::MissingPermissionsError:
403 errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_MISSING_PERMISSION);
404 break;
405 case QBluetoothDeviceDiscoveryAgent::UnknownError:
406 default:
407 errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_UNKNOWN_ERROR);
408 }
409 }
410}
411
412void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error)
413{
414 Q_ASSERT(error == QBluetoothDeviceDiscoveryAgent::PoweredOffError
415 || error == QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod
416 || error == QBluetoothDeviceDiscoveryAgent::MissingPermissionsError);
417
418 inquiryLE.reset();
419
420 startPending = false;
421 stopPending = false;
422 agentState = NonActive;
423 setError(error);
424 emit q_ptr->errorOccurred(lastError);
425}
426
427void QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported()
428{
429 qCDebug(QT_BT_DARWIN) << "no Bluetooth LE support";
430
431#ifdef Q_OS_MACOS
432 if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) {
433 // Having both Classic | LE means this is not an error.
434 LEinquiryFinished();
435 } else {
436 // In the past this was never an error, that's why we have
437 // LEnotSupported as a special method. But now, since
438 // we can have separate Classic/LE scans, we have to report it
439 // as UnsupportedDiscoveryMethod.
440 LEinquiryError(QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod);
441 }
442#else
443 inquiryLE.reset();
444 startPending = false;
445 stopPending = false;
446 setError(QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError);
447 emit q_ptr->errorOccurred(lastError);
448#endif
449}
450
451void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished()
452{
453 // The same logic as in inquiryFinished, but does not start LE scan.
454 agentState = NonActive;
455 inquiryLE.reset();
456
457 if (stopPending && !startPending) {
458 stopPending = false;
459 emit q_ptr->canceled();
460 } else if (startPending) {
461 startPending = false;
462 stopPending = false;
463 start(requestedMethods); //Start again.
464 } else {
465 emit q_ptr->finished();
466 }
467}
468
469void QBluetoothDeviceDiscoveryAgentPrivate::deviceFound(const QBluetoothDeviceInfo &newDeviceInfo)
470{
471 // Core Bluetooth does not allow us to access addresses, we have to use uuid instead.
472 // This uuid has nothing to do with uuids in Bluetooth in general (it's generated by
473 // Apple's framework using some algorithm), but it's a 128-bit uuid after all.
474 const bool isLE =
475#ifdef Q_OS_MACOS
476 newDeviceInfo.coreConfigurations() == QBluetoothDeviceInfo::LowEnergyCoreConfiguration;
477#else
478 true;
479#endif // Q_OS_MACOS
480 for (qsizetype i = 0, e = discoveredDevices.size(); i < e; ++i) {
481 if (isLE) {
482 if (discoveredDevices[i].deviceUuid() == newDeviceInfo.deviceUuid()) {
483 QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None;
484 if (discoveredDevices[i].rssi() != newDeviceInfo.rssi()) {
485 qCDebug(QT_BT_DARWIN) << "Updating RSSI for" << newDeviceInfo.address()
486 << newDeviceInfo.rssi();
487 discoveredDevices[i].setRssi(newDeviceInfo.rssi());
488 updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI);
489 }
490
491 if (discoveredDevices[i].manufacturerData() != newDeviceInfo.manufacturerData()) {
492 qCDebug(QT_BT_DARWIN) << "Updating manufacturer data for" << newDeviceInfo.address();
493 const QList<quint16> keys = newDeviceInfo.manufacturerIds();
494 for (auto key: keys)
495 discoveredDevices[i].setManufacturerData(key, newDeviceInfo.manufacturerData(key));
496 updatedFields.setFlag(QBluetoothDeviceInfo::Field::ManufacturerData);
497 }
498
499 if (discoveredDevices[i].serviceData() != newDeviceInfo.serviceData()) {
500 qCDebug(QT_BT_DARWIN) << "Updating service data for" << newDeviceInfo.address();
501 const QList<QBluetoothUuid> keys = newDeviceInfo.serviceIds();
502 for (auto key : keys)
503 discoveredDevices[i].setServiceData(key, newDeviceInfo.serviceData(key));
504 updatedFields.setFlag(QBluetoothDeviceInfo::Field::ServiceData);
505 }
506
507 if (lowEnergySearchTimeout > 0) {
508 if (discoveredDevices[i] != newDeviceInfo) {
509 discoveredDevices.replace(i, newDeviceInfo);
510 emit q_ptr->deviceDiscovered(newDeviceInfo);
511 } else {
512 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
513 emit q_ptr->deviceUpdated(discoveredDevices[i], updatedFields);
514 }
515
516 return;
517 }
518
519 discoveredDevices.replace(i, newDeviceInfo);
520 emit q_ptr->deviceDiscovered(newDeviceInfo);
521
522 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
523 emit q_ptr->deviceUpdated(discoveredDevices[i], updatedFields);
524
525 return;
526 }
527 } else {
528#ifdef Q_OS_MACOS
529 if (discoveredDevices[i].address() == newDeviceInfo.address()) {
530 if (discoveredDevices[i] == newDeviceInfo)
531 return;
532
533 discoveredDevices.replace(i, newDeviceInfo);
534 emit q_ptr->deviceDiscovered(newDeviceInfo);
535 return;
536 }
537#else
538 Q_UNREACHABLE();
539#endif // Q_OS_MACOS
540 }
541 }
542
543 discoveredDevices.append(newDeviceInfo);
544 emit q_ptr->deviceDiscovered(newDeviceInfo);
545}
546
547QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods()
548{
549#ifdef Q_OS_MACOS
550 return ClassicMethod | LowEnergyMethod;
551#else
552 return LowEnergyMethod;
553#endif // Q_OS_MACOS
554}
555
556QT_END_NAMESPACE