diff options
author | Timur Pocheptsov <Timur.Pocheptsov@digia.com> | 2014-09-01 15:32:15 +0200 |
---|---|---|
committer | Timur Pocheptsov <Timur.Pocheptsov@digia.com> | 2014-09-16 09:12:06 +0200 |
commit | 982eeb3547f85dc76e5864559ee56db74a7dd86f (patch) | |
tree | 721e0d1a6e36ffb876e9c38adab780e56bd29779 /src/bluetooth/qbluetoothlocaldevice_osx.mm | |
parent | bacabbcd91f018328eb0cec468c522599f614f6f (diff) |
Port QBluetoothLocalDevice and QBluetoothDeviceDiscoveryAgent to OS X.
QBluetoothLocalDevice and QBluetoothDeviceDiscoveryAgent for OS X -
Bluetooth Classic (the implementation based on IOBluetooth).
+ a very simple non-gui test (requires QApplication to work though).
Update 0: style issues reported by Qt-Bot + fix a test.
Update 1: QBluetoothLocalDevice - display confirmation
Update 2: Device discovery agent - follow the expected logic
and apply suggested fixes.
Update 3: started/finished delegate methods seems to be synchronous and immediately follow
start/stop calls on an inquiry.
Update 4: remove unused function and redundant error message.
Update 5: the first attempt to fix pairingStatus/requestPairing on a local device.
Update 6: on OS X it's impossible (with a given public API) to request 'Unpaired'.
I was only able to find some quite terrible hacks with private APIs or
even worse - playing with SystemConfiguration frameworks and changing
System Preferencies programmatically (requires authorization and looks like a
total hack, since it has nothing to do with Bluetooth framework).
Update 7: A very limited support for deviceConnected and connectedDevices.
Update 8: Fix an invalid invokeMethod's argument.
Update 9: Subject changed.
Update 10: fixes in a documentation.
Update 11: asserts in a coding convetion/style.
Update 12: "fix" asserts + emit errors if a start/stop failed.
Update 13: deviceDisconnected implemented.
Update 14: use not only paired && connected devices (QBluetoothLocalDevice::connectedDevices),
but also devices discovered by the connection monitor.
Update 15: remove a test, not required (there are 'auto' tests).
Update 16: fix private headers - they MUST have _p suffix :(
Update 17: tests are known to fail (at the moment) - IOBluetooth
requires adjustment (QApplication instead of QCoreApplication,
lack of ability to power on/off a device (not possible on Mac) +
other things).
Change-Id: Iea1c8a98f1fd719f4560ec8920d00cc07eaa8146
Reviewed-by: Alex Blasche <alexander.blasche@digia.com>
Reviewed-by: Timur Pocheptsov <Timur.Pocheptsov@digia.com>
Diffstat (limited to 'src/bluetooth/qbluetoothlocaldevice_osx.mm')
-rw-r--r-- | src/bluetooth/qbluetoothlocaldevice_osx.mm | 499 |
1 files changed, 499 insertions, 0 deletions
diff --git a/src/bluetooth/qbluetoothlocaldevice_osx.mm b/src/bluetooth/qbluetoothlocaldevice_osx.mm new file mode 100644 index 00000000..6a222ae6 --- /dev/null +++ b/src/bluetooth/qbluetoothlocaldevice_osx.mm @@ -0,0 +1,499 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "osx/osxbtconnectionmonitor_p.h" +#include "qbluetoothlocaldevice_p.h" +#include "qbluetoothlocaldevice.h" +#include "osx/osxbtdevicepair_p.h" +#include "osx/osxbtutility_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qstring.h> +#include <QtCore/qdebug.h> +#include <QtCore/qmap.h> + +// We have to import, not include. Obj-C headers are not protected +// against a multiple inclusion. +#import <IOBluetooth/objc/IOBluetoothHostController.h> +#import <IOBluetooth/objc/IOBluetoothDevice.h> + +#include <algorithm> + +// TODO: check how all these things work with threads. + +QT_BEGIN_NAMESPACE + +class QBluetoothLocalDevicePrivate : public OSXBluetooth::PairingDelegate, + public OSXBluetooth::ConnectionMonitor +{ + friend class QBluetoothLocalDevice; +public: + typedef QBluetoothLocalDevice::Pairing Pairing; + + QBluetoothLocalDevicePrivate(QBluetoothLocalDevice *, const QBluetoothAddress & = + QBluetoothAddress()); + + bool isValid() const; + void requestPairing(const QBluetoothAddress &address, Pairing pairing); + Pairing pairingStatus(const QBluetoothAddress &address) const; + +private: + + // PairingDelegate: + void connecting(ObjCPairingRequest *pair) Q_DECL_OVERRIDE; + void requestPIN(ObjCPairingRequest *pair) Q_DECL_OVERRIDE; + void requestUserConfirmation(ObjCPairingRequest *pair, + BluetoothNumericValue) Q_DECL_OVERRIDE; + void passkeyNotification(ObjCPairingRequest *pair, + BluetoothPasskey passkey) Q_DECL_OVERRIDE; + void error(ObjCPairingRequest *pair, IOReturn errorCode) Q_DECL_OVERRIDE; + void pairingFinished(ObjCPairingRequest *pair) Q_DECL_OVERRIDE; + + // ConnectionMonitor + void deviceConnected(const QBluetoothAddress &deviceAddress) Q_DECL_OVERRIDE; + void deviceDisconnected(const QBluetoothAddress &deviceAddress) Q_DECL_OVERRIDE; + + void emitPairingFinished(const QBluetoothAddress &deviceAddress, Pairing pairing, bool queued); + void emitError(QBluetoothLocalDevice::Error error, bool queued); + + void unpair(const QBluetoothAddress &deviceAddress); + + QBluetoothLocalDevice *q_ptr; + + typedef OSXBluetooth::ObjCScopedPointer<IOBluetoothHostController> HostController; + HostController hostController; + + typedef OSXBluetooth::ObjCStrongReference<ObjCPairingRequest> PairingRequest; + typedef QMap<QBluetoothAddress, PairingRequest> RequestMap; + + RequestMap pairingRequests; + + OSXBluetooth::ObjCScopedPointer<ObjCConnectionMonitor> connectionMonitor; + QList<QBluetoothAddress> discoveredDevices; +}; + +QBluetoothLocalDevicePrivate::QBluetoothLocalDevicePrivate(QBluetoothLocalDevice *q, + const QBluetoothAddress &address) : + q_ptr(q) +{ + Q_ASSERT_X(q, "QBluetoothLocalDevicePrivate", "invalid q_ptr (null)"); + + QT_BT_MAC_AUTORELEASEPOOL; + + HostController defaultController([[IOBluetoothHostController defaultController] retain]); + if (!defaultController) { + qCCritical(QT_BT_OSX) << "QBluetoothLocalDevicePrivate(), failed to " + "init a host controller object"; + return; + } + + if (!address.isNull()) { + NSString *const hciAddress = [defaultController addressAsString]; + if (!hciAddress) { + qCCritical(QT_BT_OSX) << "QBluetoothLocalDevicePrivate(), " + "failed to obtain an address"; + return; + } + + if (address.toString() != QString::fromNSString(hciAddress)) { + qCCritical(QT_BT_OSX) << "QBluetoothLocalDevicePrivate(), " + "invalid local device's address"; + return; + } + } + + hostController.reset(defaultController.take()); + + // This one is optional, if it fails to initialize, we do not care at all. + connectionMonitor.reset([[ObjCConnectionMonitor alloc] initWithMonitor:this]); +} + +bool QBluetoothLocalDevicePrivate::isValid() const +{ + return hostController; +} + +void QBluetoothLocalDevicePrivate::requestPairing(const QBluetoothAddress &address, Pairing pairing) +{ + Q_ASSERT_X(isValid(), "requestPairing()", "invalid local device"); + Q_ASSERT_X(!address.isNull(), "requestPairing()", "invalid device address"); + + using OSXBluetooth::device_with_address; + using OSXBluetooth::ObjCStrongReference; + + // That's a really special case on OS X. + if (pairing == QBluetoothLocalDevice::Unpaired) + return unpair(address); + + QT_BT_MAC_AUTORELEASEPOOL; + + if (pairing == QBluetoothLocalDevice::AuthorizedPaired) + pairing = QBluetoothLocalDevice::Paired; + + RequestMap::iterator pos = pairingRequests.find(address); + if (pos != pairingRequests.end()) { + if ([pos.value() isActive]) // Still trying to pair, continue. + return; + + // 'device' is autoreleased: + IOBluetoothDevice *const device = [pos.value() targetDevice]; + if ([device isPaired]) { + emitPairingFinished(address, pairing, true); + } else if ([pos.value() start] != kIOReturnSuccess) { + qCCritical(QT_BT_OSX) << "QBluetoothLocalDevicePrivate::requestPairing(), " + "failed to start a new pairing request"; + emitError(QBluetoothLocalDevice::PairingError, true); + } + return; + } + + // That's a totally new request ('Paired', since we are here). + // Even if this device is paired (not by our local device), I still create a pairing request, + // it'll just finish with success (skipping any intermediate steps). + PairingRequest newRequest([[ObjCPairingRequest alloc] initWithTarget:address delegate:this], false); + if (!newRequest) { + qCCritical(QT_BT_OSX) << "QBluetoothLocalDevicePrivate::requestPairing(), " + "failed to allocate a new pairing request"; + emitError(QBluetoothLocalDevice::PairingError, true); + return; + } + + pos = pairingRequests.insert(address, newRequest); + const IOReturn result = [newRequest start]; + if (result != kIOReturnSuccess) { + pairingRequests.erase(pos); + qCCritical(QT_BT_OSX) << "QBluetoothLocalDevicePrivate::requestPairing(), " + "failed to start a new pairing request"; + emitError(QBluetoothLocalDevice::PairingError, true); + } +} + +QBluetoothLocalDevice::Pairing QBluetoothLocalDevicePrivate::pairingStatus(const QBluetoothAddress &address)const +{ + Q_ASSERT_X(isValid(), "pairingStatus", "invalid local device"); + Q_ASSERT_X(!address.isNull(), "pairingStatus", "invalid address"); + + using OSXBluetooth::device_with_address; + using OSXBluetooth::ObjCStrongReference; + + QT_BT_MAC_AUTORELEASEPOOL; + + RequestMap::const_iterator it = pairingRequests.find(address); + if (it != pairingRequests.end()) { + // All Obj-C objects are autoreleased. + IOBluetoothDevice *const device = [it.value() targetDevice]; + if (device && [device isPaired]) + return QBluetoothLocalDevice::Paired; + } else { + // Try even if device was not paired by this local device ... + const ObjCStrongReference<IOBluetoothDevice> device(device_with_address(address)); + if (device && [device isPaired]) + return QBluetoothLocalDevice::Paired; + } + + return QBluetoothLocalDevice::Unpaired; +} + +void QBluetoothLocalDevicePrivate::connecting(ObjCPairingRequest *pair) +{ + Q_UNUSED(pair) +} + +void QBluetoothLocalDevicePrivate::requestPIN(ObjCPairingRequest *pair) +{ + Q_UNUSED(pair) +} + +void QBluetoothLocalDevicePrivate::requestUserConfirmation(ObjCPairingRequest *pair, BluetoothNumericValue intPin) +{ + Q_UNUSED(pair) + Q_UNUSED(intPin) +} + +void QBluetoothLocalDevicePrivate::passkeyNotification(ObjCPairingRequest *pair, + BluetoothPasskey passkey) +{ + Q_UNUSED(pair) + Q_UNUSED(passkey) +} + +void QBluetoothLocalDevicePrivate::error(ObjCPairingRequest *pair, IOReturn errorCode) +{ + Q_UNUSED(pair) + Q_UNUSED(errorCode) + // TODO: map from IOReturn to QBluetoothLocalDevice::Error. + // TODO: emit or invokeMethod??? + emitError(QBluetoothLocalDevice::PairingError, false); +} + +void QBluetoothLocalDevicePrivate::pairingFinished(ObjCPairingRequest *pair) +{ + Q_ASSERT_X(pair, "QBluetoothLocalDevicePrivate::pairingFinished()", + "invalid pairing request (nil)"); + + const QBluetoothAddress &deviceAddress = [pair targetAddress]; + Q_ASSERT_X(!deviceAddress.isNull(), "pairingFinished()", + "invalid target address"); + + emitPairingFinished(deviceAddress, QBluetoothLocalDevice::Paired, false); +} + +void QBluetoothLocalDevicePrivate::deviceConnected(const QBluetoothAddress &deviceAddress) +{ + Q_ASSERT_X(q_ptr, "deviceConnected()", "invalid q_ptr (null)"); + + if (!discoveredDevices.contains(deviceAddress)) + discoveredDevices.append(deviceAddress); + + QMetaObject::invokeMethod(q_ptr, "deviceConnected", Qt::QueuedConnection, + Q_ARG(QBluetoothAddress, deviceAddress)); +} + +void QBluetoothLocalDevicePrivate::deviceDisconnected(const QBluetoothAddress &deviceAddress) +{ + Q_ASSERT_X(q_ptr, "deviceDisconnected()", "invalid q_ptr (null)"); + + QList<QBluetoothAddress>::iterator devicePos =std::find(discoveredDevices.begin(), + discoveredDevices.end(), + deviceAddress); + + if (devicePos != discoveredDevices.end()) + discoveredDevices.erase(devicePos); + + QMetaObject::invokeMethod(q_ptr, "deviceDisconnected", Qt::QueuedConnection, + Q_ARG(QBluetoothAddress, deviceAddress)); +} + +void QBluetoothLocalDevicePrivate::emitError(QBluetoothLocalDevice::Error error, bool queued) +{ + Q_ASSERT_X(q_ptr, "QBluetoothLocalDevicePrivate::error()", + "invalid q_ptr (null)"); + + if (queued) { + QMetaObject::invokeMethod(q_ptr, "error", Qt::QueuedConnection, + Q_ARG(QBluetoothLocalDevice::Error, error)); + } else { + emit q_ptr->error(QBluetoothLocalDevice::PairingError); + } +} + +void QBluetoothLocalDevicePrivate::emitPairingFinished(const QBluetoothAddress &deviceAddress, + Pairing pairing, bool queued) +{ + Q_ASSERT_X(!deviceAddress.isNull(), "pairingFinished()", "invalid target device address"); + Q_ASSERT_X(q_ptr, "pairingFinished()", "invalid q_ptr (null)"); + + if (queued) { + QMetaObject::invokeMethod(q_ptr, "pairingFinished", Qt::QueuedConnection, + Q_ARG(QBluetoothAddress, deviceAddress), + Q_ARG(QBluetoothLocalDevice::Pairing, pairing)); + } else { + emit q_ptr->pairingFinished(deviceAddress, pairing); + } +} + +void QBluetoothLocalDevicePrivate::unpair(const QBluetoothAddress &deviceAddress) +{ + Q_ASSERT_X(!deviceAddress.isNull(), "unpair()", + "invalid target address"); + + emitPairingFinished(deviceAddress, QBluetoothLocalDevice::Unpaired, true); +} + +QBluetoothLocalDevice::QBluetoothLocalDevice(QObject *parent) : + QObject(parent), + d_ptr(new QBluetoothLocalDevicePrivate(this)) +{ +} + +QBluetoothLocalDevice::QBluetoothLocalDevice(const QBluetoothAddress &address, QObject *parent) : + QObject(parent), + d_ptr(new QBluetoothLocalDevicePrivate(this, address)) +{ +} + +QBluetoothLocalDevice::~QBluetoothLocalDevice() +{ + delete d_ptr; +} + +bool QBluetoothLocalDevice::isValid() const +{ + return d_ptr->isValid(); +} + + +QString QBluetoothLocalDevice::name() const +{ + QT_BT_MAC_AUTORELEASEPOOL; + + if (isValid()) { + if (NSString *const nsn = [d_ptr->hostController nameAsString]) + return QString::fromNSString(nsn); + qCCritical(QT_BT_OSX) << "QBluetoothLocalDevice::name(), " + "failed to obtain a name"; + } + + return QString(); +} + +QBluetoothAddress QBluetoothLocalDevice::address() const +{ + QT_BT_MAC_AUTORELEASEPOOL; + + if (isValid()) { + if (NSString *const nsa = [d_ptr->hostController addressAsString]) + return QBluetoothAddress(OSXBluetooth::qt_bt_address(nsa)); + + qCCritical(QT_BT_OSX) << "QBluetoothLocalDevice::address(), " + "failed to obtain an address"; + } else { + qCWarning(QT_BT_OSX) << "QBluetoothLocalDevice::address(), " + "invalid local device"; + } + + return QBluetoothAddress(); +} + +void QBluetoothLocalDevice::powerOn() +{ + if (!isValid()) { + qCWarning(QT_BT_OSX) << "QBluetoothLocalDevice::powerOn() " + "invalid local device"; + } +} + +void QBluetoothLocalDevice::setHostMode(QBluetoothLocalDevice::HostMode mode) +{ + Q_UNUSED(mode) + + if (!isValid()) { + qCWarning(QT_BT_OSX) << "QBluetoothLovalDevice::setHostMode() " + "invalid local device"; + } +} + +QBluetoothLocalDevice::HostMode QBluetoothLocalDevice::hostMode() const +{ + if (!isValid() || ![d_ptr->hostController powerState]) + return HostPoweredOff; + + return HostConnectable; +} + +QList<QBluetoothAddress> QBluetoothLocalDevice::connectedDevices() const +{ + QT_BT_MAC_AUTORELEASEPOOL; + + QList<QBluetoothAddress> connectedDevices; + + // Take the devices known to IOBluetooth to be paired and connected first: + NSArray *const pairedDevices = [IOBluetoothDevice pairedDevices]; + for (IOBluetoothDevice *device in pairedDevices) { + if ([device isConnected]) { + const QBluetoothAddress address(OSXBluetooth::qt_bt_address([device getAddress])); + if (!address.isNull()) + connectedDevices.append(address); + } + } + + // Add devices, discovered by the connection monitor: + connectedDevices += d_ptr->discoveredDevices; + // Find something more elegant? :) + // But after all, addresses are integers. + std::sort(connectedDevices.begin(), connectedDevices.end()); + connectedDevices.erase(std::unique(connectedDevices.begin(), + connectedDevices.end()), + connectedDevices.end()); + + return connectedDevices; +} + +QList<QBluetoothHostInfo> QBluetoothLocalDevice::allDevices() +{ + QList<QBluetoothHostInfo> localDevices; + + QBluetoothLocalDevice defaultAdapter; + if (!defaultAdapter.isValid() || defaultAdapter.address().isNull()) { + qCCritical(QT_BT_OSX) << "QBluetoothLocalDevice::allDevices(), no valid device found"; + return localDevices; + } + + QBluetoothHostInfo deviceInfo; + deviceInfo.setName(defaultAdapter.name()); + deviceInfo.setAddress(defaultAdapter.address()); + + localDevices.append(deviceInfo); + + return localDevices; +} + +void QBluetoothLocalDevice::pairingConfirmation(bool confirmation) +{ + Q_UNUSED(confirmation) +} + + +void QBluetoothLocalDevice::requestPairing(const QBluetoothAddress &address, Pairing pairing) +{ + if (!isValid()) + qCWarning(QT_BT_OSX) << "QBluetoothLocalDevice::requestPairing(), invalid local device"; + + if (!isValid() || address.isNull()) { + d_ptr->emitError(PairingError, true); + return; + } + + return d_ptr->requestPairing(address, pairing); +} + +QBluetoothLocalDevice::Pairing QBluetoothLocalDevice::pairingStatus(const QBluetoothAddress &address) const +{ + if (!isValid()) + qCWarning(QT_BT_OSX) << "QBluetoothLocalDevice::pairingStatus(), invalid local device"; + + if (!isValid() || address.isNull()) + return Unpaired; + + return d_ptr->pairingStatus(address); +} + +QT_END_NAMESPACE |