summaryrefslogtreecommitdiffstats
path: root/src/bluetooth/osx/osxbtledeviceinquiry.mm
diff options
context:
space:
mode:
authorTimur Pocheptsov <timur.pocheptsov@theqtcompany.com>2015-11-24 10:28:01 +0100
committerTimur Pocheptsov <timur.pocheptsov@theqtcompany.com>2015-11-27 14:19:42 +0000
commite117459e0d3d0670aa2cc60dfa3eafe81bbf11a9 (patch)
tree14a495bd997005b9f76e85919d4792adf5a084cf /src/bluetooth/osx/osxbtledeviceinquiry.mm
parentdab0c9a010cf7fcd521707168cc8107b7ed6af24 (diff)
Bluetooth LE scan - move to its own dispatch queue (iOS/OS X)
Move CBCentralManager's delegate to its own dispatch queue, not the main one, so that even if somebody _blocks_ the main thread somehow, waiting for discovery to finish, the discovery can, indeed, finish. Also, make DDA aware of the fact that now actual scan and 'this' are working on different threads, thus we must be thread-safe. Task-number: QTBUG-49476 Change-Id: I9bcc74131f51389c8a014577c65e0943bbc8da42 Reviewed-by: Morten Johan Sørvig <morten.sorvig@theqtcompany.com> Reviewed-by: Alex Blasche <alexander.blasche@theqtcompany.com>
Diffstat (limited to 'src/bluetooth/osx/osxbtledeviceinquiry.mm')
-rw-r--r--src/bluetooth/osx/osxbtledeviceinquiry.mm325
1 files changed, 153 insertions, 172 deletions
diff --git a/src/bluetooth/osx/osxbtledeviceinquiry.mm b/src/bluetooth/osx/osxbtledeviceinquiry.mm
index 28bfd1bc..f3a95820 100644
--- a/src/bluetooth/osx/osxbtledeviceinquiry.mm
+++ b/src/bluetooth/osx/osxbtledeviceinquiry.mm
@@ -46,10 +46,6 @@ QT_BEGIN_NAMESPACE
namespace OSXBluetooth {
-LEDeviceInquiryDelegate::~LEDeviceInquiryDelegate()
-{
-}
-
#if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_6_0)
QBluetoothUuid qt_uuid(NSUUID *nsUuid)
@@ -107,32 +103,19 @@ using namespace QT_NAMESPACE;
#endif
-@interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) (PrivateAPI) <CBCentralManagerDelegate, CBPeripheralDelegate>
-// "Timeout" callback to stop a scan.
+@interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) (PrivateAPI) <CBCentralManagerDelegate>
- (void)stopScan;
-- (void)handlePoweredOffAfterDelay;
+- (void)handlePoweredOff;
@end
@implementation QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry)
-+ (int)inquiryLength
-{
- // There is no default timeout,
- // scan does not stop if not asked.
- // Return in milliseconds
- return 10 * 1000;
-}
-
-- (id)initWithDelegate:(OSXBluetooth::LEDeviceInquiryDelegate *)aDelegate
+- (id)init
{
- Q_ASSERT_X(aDelegate, Q_FUNC_INFO, "invalid delegate (null)");
-
if (self = [super init]) {
- delegate = aDelegate;
- peripherals = [[NSMutableDictionary alloc] init];
- manager = nil;
- scanPhase = noActivity;
- cancelled = false;
+ uuids.reset([[NSMutableSet alloc] init]);
+ internalState = InquiryStarting;
+ state.store(int(internalState));
}
return self;
@@ -140,150 +123,137 @@ using namespace QT_NAMESPACE;
- (void)dealloc
{
- [NSObject cancelPreviousPerformRequestsWithTarget:self];
-
if (manager) {
[manager setDelegate:nil];
- if (scanPhase == activeScan)
+ if (internalState == InquiryActive)
[manager stopScan];
- [manager release];
}
- [peripherals release];
[super dealloc];
}
- (void)stopScan
{
- // Scan's timeout.
- Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)");
- Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid central (nil)");
- Q_ASSERT_X(scanPhase == activeScan, Q_FUNC_INFO, "invalid state");
- Q_ASSERT_X(!cancelled, Q_FUNC_INFO, "invalid state");
-
- [manager setDelegate:nil];
- [manager stopScan];
- scanPhase = noActivity;
-
- delegate->LEdeviceInquiryFinished();
-}
+ // Scan's "timeout" - we consider LE device
+ // discovery finished.
+ using namespace OSXBluetooth;
-- (void)handlePoweredOffAfterDelay
-{
- // If we are here, this means:
- // we received 'PoweredOff' while scanPhase == startingScan
- // and no 'PoweredOn' after this.
-
- Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)");
- Q_ASSERT_X(scanPhase == startingScan, Q_FUNC_INFO, "invalid state");
-
- scanPhase = noActivity;
- if (cancelled) {
- // Timeout happened before
- // the second status update, but after 'stop'.
- delegate->LEdeviceInquiryFinished();
- } else {
- // Timeout and no 'stop' between 'start'
- // and 'centralManagerDidUpdateStatus':
- delegate->LEnotSupported();
+ if (internalState == InquiryActive) {
+ if (scanTimer.elapsed() >= qt_LE_deviceInquiryLength() * 1000) {
+ // We indeed stop now:
+ [manager stopScan];
+ [manager setDelegate:nil];
+ internalState = InquiryFinished;
+ state.store(int(internalState));
+ } else {
+ dispatch_queue_t leQueue(qt_LE_queue());
+ Q_ASSERT(leQueue);
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
+ int64_t(qt_LE_deviceInquiryLength() / 100. * NSEC_PER_SEC)),
+ leQueue,
+ ^{
+ [self stopScan];
+ });
+ }
}
}
-- (bool)start
+- (void)handlePoweredOff
{
- Q_ASSERT_X(![self isActive], Q_FUNC_INFO, "LE device scan is already active");
- Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)");
-
- if (!peripherals) {
- qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "internal error";
- return false;
- }
-
- cancelled = false;
- [peripherals removeAllObjects];
+ // This is interesting on iOS only, where
+ // the system shows an alert asking to enable
+ // Bluetooth in the 'Settings' app. If not done yet (after 30
+ // seconds) - we consider it an error.
+ if (internalState == InquiryStarting) {
+ if (errorTimer.elapsed() >= 30000) {
+ [manager setDelegate:nil];
+ internalState = ErrorPoweredOff;
+ state.store(int(internalState));
+ } else {
+ dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue());
+ Q_ASSERT(leQueue);
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
+ (int64_t)(30 / 100. * NSEC_PER_SEC)),
+ leQueue,
+ ^{
+ [self handlePoweredOff];
+ });
- if (manager) {
- // We can never be here, if status was not updated yet.
- [manager setDelegate:nil];
- [manager release];
+ }
}
+}
- startTime = QTime();
- scanPhase = startingScan;
- manager = [CBCentralManager alloc];
- manager = [manager initWithDelegate:self queue:nil];
- if (!manager) {
- qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to create a central manager";
- return false;
- }
+- (void)start
+{
+ dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue());
- return true;
+ Q_ASSERT(leQueue);
+ manager.reset([[CBCentralManager alloc] initWithDelegate:self queue:leQueue]);
}
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
- Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)");
-
- const CBCentralManagerState state = central.state;
-
- if (scanPhase == startingScan && (state == CBCentralManagerStatePoweredOn
- || state == CBCentralManagerStateUnsupported
- || state == CBCentralManagerStateUnauthorized
- || state == CBCentralManagerStatePoweredOff)) {
- // We probably had 'PoweredOff' before,
- // cancel the previous handlePoweredOffAfterDelay.
- [NSObject cancelPreviousPerformRequestsWithTarget:self];
- }
+ if (central != manager)
+ return;
- if (cancelled) {
- Q_ASSERT_X(scanPhase != activeScan, Q_FUNC_INFO, "in 'activeScan' phase");
- scanPhase = noActivity;
- delegate->LEdeviceInquiryFinished();
+ if (internalState != InquiryActive && internalState != InquiryStarting)
return;
- }
- if (state == CBCentralManagerStatePoweredOn) {
- if (scanPhase == startingScan) {
- scanPhase = activeScan;
-#ifndef Q_OS_OSX
- const NSTimeInterval timeout([QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) inquiryLength] / 1000);
- Q_ASSERT_X(timeout > 0., Q_FUNC_INFO, "invalid scan timeout");
- [self performSelector:@selector(stopScan) withObject:nil afterDelay:timeout];
-#endif
- startTime = QTime::currentTime();
+ using namespace OSXBluetooth;
+
+ dispatch_queue_t leQueue(qt_LE_queue());
+ Q_ASSERT(leQueue);
+
+ const CBCentralManagerState cbState(central.state);
+ if (cbState == CBCentralManagerStatePoweredOn) {
+ if (internalState == InquiryStarting) {
+ internalState = InquiryActive;
+ // Scan time is actually 10 seconds. Having a block with such delay can prevent
+ // 'self' from being deleted in time, which is not good. So we split this
+ // 10 s. timeout into smaller 'chunks'.
+ scanTimer.start();
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
+ int64_t(qt_LE_deviceInquiryLength() / 100. * NSEC_PER_SEC)),
+ leQueue,
+ ^{
+ [self stopScan];
+ });
[manager scanForPeripheralsWithServices:nil options:nil];
} // Else we ignore.
} else if (state == CBCentralManagerStateUnsupported || state == CBCentralManagerStateUnauthorized) {
- if (scanPhase == startingScan) {
- scanPhase = noActivity;
- delegate->LEnotSupported();
- } else if (scanPhase == activeScan) {
- // Cancel stopScan:
- [NSObject cancelPreviousPerformRequestsWithTarget:self];
-
- scanPhase = noActivity;
+ if (internalState == InquiryActive) {
[manager stopScan];
- delegate->LEdeviceInquiryError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
+ // Not sure how this is possible at all, probably, can never happen.
+ internalState = ErrorPoweredOff;
+ } else {
+ internalState = ErrorLENotSupported;
}
- } else if (state == CBCentralManagerStatePoweredOff) {
- if (scanPhase == startingScan) {
+
+ [manager setDelegate:nil];
+ } else if (cbState == CBCentralManagerStatePoweredOff) {
+ if (internalState == InquiryStarting) {
#ifndef Q_OS_OSX
// On iOS a user can see at this point an alert asking to enable
// Bluetooth in the "Settings" app. If a user does,
// we'll receive 'PoweredOn' state update later.
- [self performSelector:@selector(handlePoweredOffAfterDelay) withObject:nil afterDelay:30.];
+ // No change in state. Wait for 30 seconds (we split it into 'chunks' not
+ // to retain 'self' for too long ) ...
+ errorTimer.start();
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
+ (int64_t)(30 / 100. * NSEC_PER_SEC)),
+ leQueue,
+ ^{
+ [self handlePoweredOff];
+ });
return;
#endif
- scanPhase = noActivity;
- delegate->LEnotSupported();
- } else if (scanPhase == activeScan) {
- // Cancel stopScan:
- [NSObject cancelPreviousPerformRequestsWithTarget:self];
-
- scanPhase = noActivity;
+ internalState = ErrorPoweredOff;
+ } else {
+ internalState = ErrorPoweredOff;
[manager stopScan];
- delegate->LEdeviceInquiryError(QBluetoothDeviceDiscoveryAgent::PoweredOffError);
- } // Else we ignore.
+ }
+
+ [manager setDelegate:nil];
} else {
// The following two states we ignore (from Apple's docs):
//"
@@ -294,44 +264,36 @@ using namespace QT_NAMESPACE;
// -CBCentralManagerStateResetting
// The connection with the system service was momentarily
// lost; an update is imminent. "
+ // Wait for this imminent update.
}
+
+ state.store(int(internalState));
}
- (void)stop
{
- if (scanPhase != startingScan) {
- // startingScan means either no selector at all,
- // or handlePoweredOffAfterDelay and we do not want to cancel it yet,
- // waiting for DidUpdateState or handlePoweredOffAfterDelay, whoever
- // fires first ...
- [NSObject cancelPreviousPerformRequestsWithTarget:self];
- }
-
- if (scanPhase == startingScan || cancelled) {
- // We have to wait for a status update or handlePoweredOffAfterDelay.
- cancelled = true;
- return;
- }
-
- if (scanPhase == activeScan) {
+ if (internalState == InquiryActive)
[manager stopScan];
- scanPhase = noActivity;
- delegate->LEdeviceInquiryFinished();
- }
+
+ [manager setDelegate:nil];
+ internalState = InquiryCancelled;
+ state.store(int(internalState));
}
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
- Q_UNUSED(central)
- Q_UNUSED(advertisementData)
+ Q_UNUSED(advertisementData);
using namespace OSXBluetooth;
- if (scanPhase != activeScan)
+ if (central != manager)
return;
- Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)");
+ if (internalState != InquiryActive)
+ return;
+
+ QBluetoothUuid deviceUuid;
#if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_7_0)
if (QSysInfo::MacintoshVersion >= qt_OS_limit(QSysInfo::MV_10_9, QSysInfo::MV_IOS_7_0)) {
@@ -340,45 +302,64 @@ using namespace QT_NAMESPACE;
return;
}
- if (![peripherals objectForKey:peripheral.identifier]) {
- [peripherals setObject:peripheral forKey:peripheral.identifier];
- const QBluetoothUuid deviceUuid(OSXBluetooth::qt_uuid(peripheral.identifier));
- delegate->LEdeviceFound(peripheral, deviceUuid, advertisementData, RSSI);
+ if ([uuids containsObject:peripheral.identifier]) {
+ // We already know this peripheral ...
+ return;
}
- return;
+
+ [uuids addObject:peripheral.identifier];
+ deviceUuid = OSXBluetooth::qt_uuid(peripheral.identifier);
}
#endif
// Either SDK or the target is below 10.9/7.0:
// The property UUID was finally removed in iOS 9, we have
// to avoid compilation errors ...
- CFUUIDRef cfUUID = Q_NULLPTR;
+ if (deviceUuid.isNull()) {
+ CFUUIDRef cfUUID = Q_NULLPTR;
+
+ if ([peripheral respondsToSelector:@selector(UUID)]) {
+ // This will require a bridged cast if we switch to ARC ...
+ cfUUID = reinterpret_cast<CFUUIDRef>([peripheral performSelector:@selector(UUID)]);
+ }
- if ([peripheral respondsToSelector:@selector(UUID)]) {
- // This will require a bridged cast if we switch to ARC ...
- cfUUID = reinterpret_cast<CFUUIDRef>([peripheral performSelector:@selector(UUID)]);
+ if (!cfUUID) {
+ qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "peripheral without CFUUID";
+ return;
+ }
+
+ StringStrongReference key(uuid_as_nsstring(cfUUID));
+ if ([uuids containsObject:key.data()])
+ return; // We've seen this peripheral before ...
+ [uuids addObject:key.data()];
+ deviceUuid = OSXBluetooth::qt_uuid(cfUUID);
}
- if (!cfUUID) {
- qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "peripheral without CFUUID";
+ if (deviceUuid.isNull()) {
+ qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no way to address peripheral, QBluetoothUuid is null";
return;
}
- StringStrongReference key(uuid_as_nsstring(cfUUID));
- if (![peripherals objectForKey:key.data()]) {
- [peripherals setObject:peripheral forKey:key.data()];
- const QBluetoothUuid deviceUuid(OSXBluetooth::qt_uuid(cfUUID));
- delegate->LEdeviceFound(peripheral, deviceUuid, advertisementData, RSSI);
- }
+ QString name;
+ if (peripheral.name)
+ name = QString::fromNSString(peripheral.name);
+
+ // TODO: fix 'classOfDevice' (0 for now).
+ QBluetoothDeviceInfo newDeviceInfo(deviceUuid, name, 0);
+ if (RSSI)
+ newDeviceInfo.setRssi([RSSI shortValue]);
+ // CoreBluetooth scans only for LE devices.
+ newDeviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration);
+ devices.append(newDeviceInfo);
}
-- (bool)isActive
+- (LEInquiryState) inquiryState
{
- return scanPhase == startingScan || scanPhase == activeScan;
+ return LEInquiryState(state.load());
}
-- (const QTime&)startTime
+- (const QList<QBluetoothDeviceInfo> &)discoveredDevices
{
- return startTime;
+ return devices;
}
@end