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
btsdpinquiry.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
8#include "btutility_p.h"
9
10#include <QtCore/qoperatingsystemversion.h>
11#include <QtCore/qvariant.h>
12#include <QtCore/qstring.h>
13#include <QtCore/qtimer.h>
14
15#include <memory>
16
17QT_BEGIN_NAMESPACE
18
19namespace DarwinBluetooth {
20
21namespace {
22
23const int basebandConnectTimeoutMS = 20000;
24
25QBluetoothUuid sdp_element_to_uuid(IOBluetoothSDPDataElement *element)
26{
28
29 if (!element || [element getTypeDescriptor] != kBluetoothSDPDataElementTypeUUID)
30 return {};
31
32 return qt_uuid([[element getUUIDValue] getUUIDWithLength:16]);
33}
34
35QBluetoothUuid extract_service_ID(IOBluetoothSDPServiceRecord *record)
36{
37 Q_ASSERT(record);
38
40
41 return sdp_element_to_uuid([record getAttributeDataElement:kBluetoothSDPAttributeIdentifierServiceID]);
42}
43
44QList<QBluetoothUuid> extract_service_class_ID_list(IOBluetoothSDPServiceRecord *record)
45{
46 Q_ASSERT(record);
47
49
50 IOBluetoothSDPDataElement *const idList = [record getAttributeDataElement:kBluetoothSDPAttributeIdentifierServiceClassIDList];
51
52 QList<QBluetoothUuid> uuids;
53 if (!idList)
54 return uuids;
55
56 NSArray *arr = nil;
57 if ([idList getTypeDescriptor] == kBluetoothSDPDataElementTypeDataElementSequence)
58 arr = [idList getArrayValue];
59 else if ([idList getTypeDescriptor] == kBluetoothSDPDataElementTypeUUID)
60 arr = @[idList];
61
62 if (!arr)
63 return uuids;
64
65 for (IOBluetoothSDPDataElement *dataElement in arr) {
66 const auto qtUuid = sdp_element_to_uuid(dataElement);
67 if (!qtUuid.isNull())
68 uuids.push_back(qtUuid);
69 }
70
71 return uuids;
72}
73
74QBluetoothServiceInfo::Sequence service_class_ID_list_to_sequence(const QList<QBluetoothUuid> &uuids)
75{
76 if (uuids.isEmpty())
77 return {};
78
79 QBluetoothServiceInfo::Sequence sequence;
80 for (const auto &uuid : uuids) {
81 Q_ASSERT(!uuid.isNull());
82 sequence.append(QVariant::fromValue(uuid));
83 }
84
85 return sequence;
86}
87
88} // unnamed namespace
89
90QVariant extract_attribute_value(IOBluetoothSDPDataElement *dataElement)
91{
92 Q_ASSERT_X(dataElement, Q_FUNC_INFO, "invalid data element (nil)");
93
94 // TODO: error handling and diagnostic messages.
95
96 // All "temporary" obj-c objects are autoreleased.
98
99 const BluetoothSDPDataElementTypeDescriptor typeDescriptor = [dataElement getTypeDescriptor];
100
101 switch (typeDescriptor) {
102 case kBluetoothSDPDataElementTypeNil:
103 break;
104 case kBluetoothSDPDataElementTypeUnsignedInt:
105 return [[dataElement getNumberValue] unsignedIntValue];
106 case kBluetoothSDPDataElementTypeSignedInt:
107 return [[dataElement getNumberValue] intValue];
108 case kBluetoothSDPDataElementTypeUUID:
109 return QVariant::fromValue(sdp_element_to_uuid(dataElement));
110 case kBluetoothSDPDataElementTypeString:
111 case kBluetoothSDPDataElementTypeURL:
112 return QString::fromNSString([dataElement getStringValue]);
113 case kBluetoothSDPDataElementTypeBoolean:
114 return [[dataElement getNumberValue] boolValue];
115 case kBluetoothSDPDataElementTypeDataElementSequence:
116 case kBluetoothSDPDataElementTypeDataElementAlternative: // TODO: check this!
117 {
118 QBluetoothServiceInfo::Sequence sequence;
119 NSArray *const arr = [dataElement getArrayValue];
120 for (IOBluetoothSDPDataElement *element in arr)
121 sequence.append(extract_attribute_value(element));
122
123 return QVariant::fromValue(sequence);
124 }
125 break;// Coding style.
126 default:;
127 }
128
129 return QVariant();
130}
131
132void extract_service_record(IOBluetoothSDPServiceRecord *record, QBluetoothServiceInfo &serviceInfo)
133{
135
136 if (!record)
137 return;
138
139 NSDictionary *const attributes = record.attributes;
140 NSEnumerator *const keys = attributes.keyEnumerator;
141 for (NSNumber *key in keys) {
142 const quint16 attributeID = [key unsignedShortValue];
143 IOBluetoothSDPDataElement *const element = [attributes objectForKey:key];
144 const QVariant attributeValue = DarwinBluetooth::extract_attribute_value(element);
145 serviceInfo.setAttribute(attributeID, attributeValue);
146 }
147
148 const QBluetoothUuid serviceUuid = extract_service_ID(record);
149 if (!serviceUuid.isNull())
150 serviceInfo.setServiceUuid(serviceUuid);
151
152 const QList<QBluetoothUuid> uuids(extract_service_class_ID_list(record));
153 const auto sequence = service_class_ID_list_to_sequence(uuids);
154 if (!sequence.isEmpty())
155 serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, sequence);
156}
157
158QList<QBluetoothUuid> extract_services_uuids(IOBluetoothDevice *device)
159{
160 QList<QBluetoothUuid> uuids;
161
162 // All "temporary" obj-c objects are autoreleased.
164
165 if (!device || !device.services)
166 return uuids;
167
168 NSArray * const records = device.services;
169 for (IOBluetoothSDPServiceRecord *record in records) {
170 const QBluetoothUuid serviceID = extract_service_ID(record);
171 if (!serviceID.isNull())
172 uuids.push_back(serviceID);
173
174 const QList<QBluetoothUuid> idList(extract_service_class_ID_list(record));
175 if (idList.size())
176 uuids.append(idList);
177 }
178
179 return uuids;
180}
181
182} // namespace DarwinBluetooth
183
184QT_END_NAMESPACE
185
186QT_USE_NAMESPACE
187
188using namespace DarwinBluetooth;
189
190@implementation DarwinBTSDPInquiry
191{
192 QT_PREPEND_NAMESPACE(DarwinBluetooth::SDPInquiryDelegate) *delegate;
193 ObjCScopedPointer<IOBluetoothDevice> device;
194 bool isActive;
195
196 // Needed to workaround a broken SDP on Monterey:
197 std::unique_ptr<QTimer> connectionWatchdog;
198}
199
200- (id)initWithDelegate:(DarwinBluetooth::SDPInquiryDelegate *)aDelegate
201{
202 Q_ASSERT_X(aDelegate, Q_FUNC_INFO, "invalid delegate (null)");
203
204 if (self = [super init]) {
205 delegate = aDelegate;
206 isActive = false;
207 }
208
209 return self;
210}
211
212- (void)dealloc
213{
214 //[device closeConnection]; //??? - synchronous, "In the future this API will be changed to allow asynchronous operation."
215 [super dealloc];
216}
217
218- (IOReturn)performSDPQueryWithDevice:(const QBluetoothAddress &)address
219{
220 Q_ASSERT_X(!isActive, Q_FUNC_INFO, "SDP query in progress");
221
222 QList<QBluetoothUuid> emptyFilter;
223 return [self performSDPQueryWithDevice:address filters:emptyFilter];
224}
225
226- (void)interruptSDPQuery
227{
228 // To be only executed on timer.
229 Q_ASSERT(connectionWatchdog.get());
230 // If device was reset, so the timer should be, we can never be here then.
231 Q_ASSERT(device.get());
232
233 Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)");
234 qCDebug(QT_BT_DARWIN) << "couldn't connect to device" << [device nameOrAddress]
235 << ", ending SDP inquiry.";
236
237 // Stop the watchdog and close the connection as otherwise there could be
238 // later "connectionComplete" callbacks
239 connectionWatchdog->stop();
240 [device closeConnection];
241
242 delegate->SDPInquiryError(device, kIOReturnTimeout);
243 device.reset();
244 isActive = false;
245}
246
247- (IOReturn)performSDPQueryWithDevice:(const QBluetoothAddress &)address
248 filters:(const QList<QBluetoothUuid> &)qtFilters
249{
250 Q_ASSERT_X(!isActive, Q_FUNC_INFO, "SDP query in progress");
251 Q_ASSERT_X(!address.isNull(), Q_FUNC_INFO, "invalid target device address");
252 qCDebug(QT_BT_DARWIN) << "Starting and SDP inquiry for address:" << address;
253
255
256 // We first try to allocate "filters":
257 ObjCScopedPointer<NSMutableArray> array;
258 if (QOperatingSystemVersion::current() <= QOperatingSystemVersion::MacOSBigSur
259 && qtFilters.size()) { // See the comment about filters on Monterey below.
260 array.reset([[NSMutableArray alloc] init], RetainPolicy::noInitialRetain);
261 if (!array) {
262 qCCritical(QT_BT_DARWIN) << "failed to allocate an uuid filter";
263 return kIOReturnError;
264 }
265
266 for (const QBluetoothUuid &qUuid : qtFilters) {
267 ObjCStrongReference<IOBluetoothSDPUUID> uuid(iobluetooth_uuid(qUuid));
268 if (uuid)
269 [array addObject:uuid];
270 }
271
272 if (qsizetype([array count]) != qtFilters.size()) {
273 qCCritical(QT_BT_DARWIN) << "failed to create an uuid filter";
274 return kIOReturnError;
275 }
276 }
277
278 const BluetoothDeviceAddress iobtAddress(iobluetooth_address(address));
279 device.reset([IOBluetoothDevice deviceWithAddress:&iobtAddress], RetainPolicy::doInitialRetain);
280 if (!device) {
281 qCCritical(QT_BT_DARWIN) << "failed to create an IOBluetoothDevice object";
282 return kIOReturnError;
283 }
284 qCDebug(QT_BT_DARWIN) << "Device" << [device nameOrAddress] << "connected:"
285 << bool([device isConnected]) << "paired:" << bool([device isPaired]);
286
287 IOReturn result = kIOReturnSuccess;
288
289 if (QOperatingSystemVersion::current() > QOperatingSystemVersion::MacOSBigSur) {
290 // SDP query on Monterey does not follow its own documented/expected behavior:
291 // - a simple performSDPQuery was previously ensuring baseband connection
292 // to be opened, now it does not do so, instead logs a warning and returns
293 // immediately.
294 // - a version with UUID filters simply does nothing except it immediately
295 // returns kIOReturnSuccess.
296
297 // If the device was not yet connected, connect it first
298 if (![device isConnected]) {
299 qCDebug(QT_BT_DARWIN) << "Device" << [device nameOrAddress]
300 << "is not connected, connecting it first";
301 result = [device openConnection:self];
302 // The connection may succeed immediately. But if it didn't, start a connection timer
303 // which has two guardian roles:
304 // 1. Guard against connect attempt taking too long time
305 // 2. Sometimes on Monterey the callback indicating "connection completion" is
306 // not received even though the connection has in fact succeeded
307 if (![device isConnected]) {
308 qCDebug(QT_BT_DARWIN) << "Starting connection monitor for device"
309 << [device nameOrAddress] << "with timeout limit of"
310 << basebandConnectTimeoutMS/1000 << "seconds.";
311 connectionWatchdog.reset(new QTimer);
312 connectionWatchdog->setSingleShot(false);
313 QObject::connect(connectionWatchdog.get(), &QTimer::timeout,
314 connectionWatchdog.get(),
315 [self] () {
316 qCDebug(QT_BT_DARWIN) << "Connection monitor timeout for device:"
317 << [device nameOrAddress]
318 << ", connected:" << bool([device isConnected]);
319 // Device can sometimes get properly connected without IOBluetooth
320 // calling the connectionComplete callback, so we check the status here
321 if ([device isConnected])
322 [self connectionComplete:device status:kIOReturnSuccess];
323 else
324 [self interruptSDPQuery];
325 });
326 connectionWatchdog->start(basebandConnectTimeoutMS);
327 }
328 }
329
330 if ([device isConnected])
331 result = [device performSDPQuery:self];
332
333 if (result != kIOReturnSuccess) {
334 qCCritical(QT_BT_DARWIN, "failed to start an SDP query");
335 device.reset();
336 } else {
337 isActive = true;
338 }
339
340 return result;
341 } // Monterey's code path.
342
343 if (qtFilters.size())
344 result = [device performSDPQuery:self uuids:array];
345 else
346 result = [device performSDPQuery:self];
347
348 if (result != kIOReturnSuccess) {
349 qCCritical(QT_BT_DARWIN) << "failed to start an SDP query";
350 device.reset();
351 } else {
352 isActive = true;
353 }
354
355 return result;
356}
357
358- (void)connectionComplete:(IOBluetoothDevice *)aDevice status:(IOReturn)status
359{
360 qCDebug(QT_BT_DARWIN) << "connectionComplete for device" << [aDevice nameOrAddress]
361 << "with status:" << status;
362 if (aDevice != device) {
363 // Connection was previously cancelled, probably, due to the timeout.
364 return;
365 }
366
367 // The connectionComplete may be invoked by either the IOBluetooth callback or our
368 // connection watchdog. In either case stop the watchdog if it exists
369 if (connectionWatchdog)
370 connectionWatchdog->stop();
371
372 if (status == kIOReturnSuccess)
373 status = [aDevice performSDPQuery:self];
374
375 if (status != kIOReturnSuccess) {
376 isActive = false;
377 qCWarning(QT_BT_DARWIN, "failed to open connection or start an SDP query");
378 Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)");
379 delegate->SDPInquiryError(aDevice, status);
380 }
381}
382
383- (void)stopSDPQuery
384{
385 // There is no API to stop it SDP on device, but there is a 'stop'
386 // member-function in Qt and after it's called sdpQueryComplete
387 // must be somehow ignored (device != aDevice in a callback).
388 device.reset();
389 isActive = false;
390 connectionWatchdog.reset();
391}
392
393- (void)sdpQueryComplete:(IOBluetoothDevice *)aDevice status:(IOReturn)status
394{
395 qCDebug(QT_BT_DARWIN) << "sdpQueryComplete for device:" << [aDevice nameOrAddress]
396 << "with status:" << status;
397 // Can happen - there is no legal way to cancel an SDP query,
398 // after the 'reset' device can never be
399 // the same as the cancelled one.
400 if (device != aDevice)
401 return;
402
403 Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)");
404
405 isActive = false;
406
407 // If we used the manual connection establishment, close the
408 // connection here. Otherwise the IOBluetooth may call stray
409 // connectionComplete or sdpQueryCompletes
410 if (connectionWatchdog) {
411 qCDebug(QT_BT_DARWIN) << "Closing the connection established for SDP inquiry.";
412 connectionWatchdog.reset();
413 [device closeConnection];
414 }
415
416 if (status != kIOReturnSuccess)
417 delegate->SDPInquiryError(aDevice, status);
418 else
419 delegate->SDPInquiryFinished(aDevice);
420}
421
422@end
#define QT_BT_MAC_AUTORELEASEPOOL
Definition btutility_p.h:78
QList< QBluetoothUuid > extract_services_uuids(IOBluetoothDevice *device)
void extract_service_record(IOBluetoothSDPServiceRecord *record, QBluetoothServiceInfo &serviceInfo)
QVariant extract_attribute_value(IOBluetoothSDPDataElement *dataElement)