diff options
author | Timur Pocheptsov <Timur.Pocheptsov@digia.com> | 2014-10-03 17:05:00 +0200 |
---|---|---|
committer | Alex Blasche <alexander.blasche@digia.com> | 2014-10-16 16:59:15 +0200 |
commit | 038e0e38f96246848a6b7976690376cedb48140a (patch) | |
tree | 3f05f7bb2b506bffaea4b7214405ab08b27d53d4 | |
parent | e417b7036e35744daa1978cd236798e3ee1693c0 (diff) |
Port QBluetoothTransferReply to OS X.
Implement file transfer using IOBluetoothOBEXSession.
- Add OBEX session class and session delegate.
- Implement OBEX connect operation.
- Aux. function to convert OBEX data into Qt's data types.
- Start implementing OBEX put.
- Extract a connection ID from response headers (if any?).
- OBEX Put (without response/event handler yet).
- OBEX Put completed - send the remaining chunks of data (if any).
Change the unicode string encoding - byte order.
- OBEX change error handling - there can be real errors (OBEX event type == error_type)
but also response code can be not something good - handle them both.
- Emit all signals and make Qt' btfiletransfer really working now.
- Protect a service discovery against the early stop (can be a result of emit serviceDiscovered).
- After a file transfer finished (either success or a failure) - disconnect (if connected)
OBEX session - to make it possible to send more files.
- Implement OBEXPutError + isFinished/isRunning and signals on error.
- Add proper 'abort' and 'start' slots. Also emit finished on errors.
- If we do not have a file, but just an input stream, generate
a _really_ random and unique name, not 'XXXXX' template.
Change-Id: Ib6fb35d8e0eb07d71ccd2d7b565bba226bef119c
Reviewed-by: Alex Blasche <alexander.blasche@digia.com>
-rw-r--r-- | src/bluetooth/bluetooth.pro | 6 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbt.pri | 6 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtobexsession.mm | 840 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtobexsession_p.h | 147 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtutility_p.h | 7 | ||||
-rw-r--r-- | src/bluetooth/qbluetoothservicediscoveryagent_osx.mm | 4 | ||||
-rw-r--r-- | src/bluetooth/qbluetoothtransfermanager.cpp | 7 | ||||
-rw-r--r-- | src/bluetooth/qbluetoothtransferreply_osx.mm | 473 | ||||
-rw-r--r-- | src/bluetooth/qbluetoothtransferreply_osx_p.h | 84 |
9 files changed, 1569 insertions, 5 deletions
diff --git a/src/bluetooth/bluetooth.pro b/src/bluetooth/bluetooth.pro index 16ae481f..e71adeae 100644 --- a/src/bluetooth/bluetooth.pro +++ b/src/bluetooth/bluetooth.pro @@ -153,13 +153,15 @@ config_bluez:qtHaveModule(dbus) { qbluetoothserviceinfo_osx.mm \ qbluetoothservicediscoveryagent_osx.mm \ qbluetoothsocket_osx.mm \ - qbluetoothserver_osx.mm + qbluetoothserver_osx.mm \ + qbluetoothtransferreply_osx.mm SOURCES += \ qlowenergycontroller_p.cpp PRIVATE_HEADERS += qbluetoothsocket_osx_p.h \ - qbluetoothserver_osx_p.h + qbluetoothserver_osx_p.h \ + qbluetoothtransferreply_osx_p.h SOURCES -= qbluetoothdevicediscoveryagent.cpp SOURCES -= qbluetoothserviceinfo.cpp diff --git a/src/bluetooth/osx/osxbt.pri b/src/bluetooth/osx/osxbt.pri index c1382484..cdcf0659 100644 --- a/src/bluetooth/osx/osxbt.pri +++ b/src/bluetooth/osx/osxbt.pri @@ -7,7 +7,8 @@ PRIVATE_HEADERS += osx/osxbtutility_p.h \ osx/osxbtl2capchannel_p.h \ osx/osxbtchanneldelegate_p.h \ osx/osxbtservicerecord_p.h \ - osx/osxbtsocketlistener_p.h + osx/osxbtsocketlistener_p.h \ + osx/osxbtobexsession_p.h OBJECTIVE_SOURCES += osx/osxbtutility.mm \ osx/osxbtdevicepair.mm \ @@ -18,4 +19,5 @@ OBJECTIVE_SOURCES += osx/osxbtutility.mm \ osx/osxbtl2capchannel.mm \ osx/osxbtchanneldelegate.mm \ osx/osxbtservicerecord.mm \ - osx/osxbtsocketlistener.mm + osx/osxbtsocketlistener.mm \ + osx/osxbtobexsession.mm diff --git a/src/bluetooth/osx/osxbtobexsession.mm b/src/bluetooth/osx/osxbtobexsession.mm new file mode 100644 index 00000000..8a5feb32 --- /dev/null +++ b/src/bluetooth/osx/osxbtobexsession.mm @@ -0,0 +1,840 @@ +/**************************************************************************** +** +** 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 "osxbtobexsession_p.h" +#include "qbluetoothaddress.h" +#include "osxbtutility_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qvector.h> +#include <QtCore/qdebug.h> +#include <QtCore/qlist.h> + +// Import, since it's Obj-C headers: +#import <IOBluetooth/objc/IOBluetoothOBEXSession.h> +#import <IOBluetooth/objc/IOBluetoothDevice.h> + +#include <algorithm> +#include <cstddef> +#include <limits> + +QT_BEGIN_NAMESPACE + +namespace OSXBluetooth +{ + +OBEXSessionDelegate::~OBEXSessionDelegate() +{ +} + +namespace { + +struct OBEXHeader +{ + OBEXHeader() : headerID(0) + { + } + + quint8 headerID; + QVariant value; +}; + +enum { + // Bits 7 and 8 == header's format. + OBEXHeaderFormatMask = 0xc0, + // + OBEXHeaderFormatUnicode = 0, // 87 + OBEXHeaderFormatByteSequence = 0x40, // 0100 0000 + OBEXHeaderFormat1Byte = 0x80, // 1000 0000 + OBEXHeaderFormat4Byte = 0xc0, // 1100 0000 + +}; + +quint32 extract_uint32(const uint8_t *bytes) +{ + // Four byte value, high byte first. + Q_ASSERT_X(bytes, "extract_uint32", "invalid input data (null)"); + + uint32_t value = uint32_t(); + std::copy(bytes, bytes + sizeof value, reinterpret_cast<uint8_t *>(&value)); + + return NSSwapBigIntToHost(value); +} + +quint16 extract_uint16(const uint8_t *bytes) +{ + // Two byte value, high byte first. + Q_ASSERT_X(bytes, "extract_uint16", "invalid input data (null)"); + + uint16_t value = uint16_t(); + std::copy(bytes, bytes + sizeof value, reinterpret_cast<uint8_t *>(&value)); + + return NSSwapBigShortToHost(value); +} + +QString extract_qstring(const uint8_t *bytes, quint16 stringLength) +{ + if (bytes && stringLength) { + NSString * const nsString = [[NSString alloc] initWithBytes:bytes + length:stringLength + encoding:NSUnicodeStringEncoding]; + if (nsString) + return QString::fromNSString(nsString); + } + + // Empty string is an error, "valid" empty strings are + // handled separately. + return QString(); +} + +QList<OBEXHeader> qt_bluetooth_headers(const uint8_t *data, std::size_t length) +{ + // Convert a data from IOBluetooth into something, Qt understands. + // Possible formats (bits 7 and 8): + // 1. 00 Two bytes of length folowed by a null-terminated + // Unicode text string (length is unsigned integer; + // it covers the header ID and the whole of the header + // value, including the length bytes and the two bytes + // of null terminator. + // 2. 01 Two bytes of length followed by a byte sequence (length + // is an unsigned integer sent high byte first; it covers + // the header ID and the whole of the header value). + // 3. 10 A single byte value. + // 4. 11 A four byte value, sent high byte first. + + Q_ASSERT_X(data, "qt_bluetooth_headers", "invalid data (null)"); + Q_ASSERT_X(length >= 2, "qt_bluetooth_headers", "invalid data length"); + + Q_UNUSED(data) + Q_UNUSED(length) + + QList<OBEXHeader> empty; + QList<OBEXHeader> qtHeaders; + + for (std::size_t i = 0; i < length;) { + std::size_t headerLength = 0; + OBEXHeader header; + header.headerID = data[i]; + + switch (data[i] & OBEXHeaderFormatMask) { + case OBEXHeaderFormatUnicode: + { + if (i + 3 > length) + return empty; + headerLength = extract_uint16(data + i + 1); + // Invalid length or input data: + if (headerLength < 3 || i + headerLength > length) + return empty; + if (headerLength == 3 || headerLength == 5) { // Can 5 ever happen? + header.value.fromValue<QString>(QString()); + } else if (headerLength > 5) {// TODO: We do not check now, that the string actually valid. + const QString value(extract_qstring(data + i + 3, headerLength - 5)); + if (!value.length()) // Some error? + return empty; + header.value.setValue<QString>(value); + } else // Still something weird. + return empty; + break; + } + case OBEXHeaderFormatByteSequence: + { + if (i + 3 > length) + return empty; + headerLength = extract_uint16(data + i + 1); + // Something is wrong: + if (headerLength < 3 || i + headerLength > length) + return empty; + QVector<unsigned char> value; + if (headerLength > 3) { + value.resize(headerLength - 3); + std::copy(data, data + headerLength, value.begin()); + } + header.value.setValue<QVector<unsigned char> >(value); + break; + } + case OBEXHeaderFormat1Byte: + { + // 1 byte integer + 1 byte headerID == 2 + if (i + 2 > length) + return empty; + headerLength = 2; + header.value.setValue<quint8>(data[i + 1]); + break; + } + case OBEXHeaderFormat4Byte: + { + // 4 byte integer + 1 byte headerID == 5 + if (i + 5 > length) + return empty; + headerLength = 5; + header.value.setValue<quint32>(extract_uint32(data + i + 1)); + break; + } + default: + qCWarning(QT_BT_OSX) << "qt_bluetooth_headers(), invalid header format"; + return empty; + } + + i += headerLength; + qtHeaders.push_back(header); + } + + return qtHeaders; +} + +bool append_uint16(ObjCStrongReference<NSMutableData> headers, uint16_t value) +{ + if (!headers) + return false; + + const NSUInteger length = [headers length]; + const uint16_t valueSwapped = NSSwapHostShortToBig(value); + [headers appendBytes:&valueSwapped length:sizeof valueSwapped]; + + return [headers length] - length == 2; +} + + +bool append_four_byte_header(ObjCStrongReference<NSMutableData> headers, uint8_t headerID, + uint32_t headerValue) +{ + if (!headers) + return false; + + const NSUInteger length = [headers length]; + // Header ID (1 byte) + [headers appendBytes:&headerID length:1]; + // Header value (4 bytes) + const uint32_t valueSwapped(NSSwapHostIntToBig(headerValue)); + [headers appendBytes:&valueSwapped length:sizeof valueSwapped]; + + return [headers length] - length == 5; +} + +bool append_unicode_header(ObjCStrongReference<NSMutableData> headers, uint8_t headerID, + const QString &string) +{ + // Two bytes of length followed by a null-terminated + // Unicode text string. Length is unsigned integer, + // it covers the header ID and the whole of the header + // value, including the length bytes and the two bytes + // of null terminator. + // All Obj-C objects are autoreleased. + + if (!headers) + return false; + + QT_BT_MAC_AUTORELEASEPOOL; + + const NSUInteger initialLength = [headers length]; + [headers appendBytes:&headerID length:1]; + + if (!string.length()) { + // Empty string. The length is 3 + // (header ID + length value itself). + return append_uint16(headers, 3); + } + + NSString *const nsString = string.toNSString(); + if (!nsString) + return false; + + // TODO: check if the encodings is right. It was NSUnicodeStringEncoding but + // byte order was wrong. Also, I do not need BOM check anymore? + NSData *const data = [nsString dataUsingEncoding:NSUTF16BigEndianStringEncoding]; + if (!data) + return false; + + // This data can include byte-order marker (BOM) and does not include + // a null terminator. Anyway, the length must be >= 2. + NSUInteger length = [data length]; + if (length < 2) + return false; + + const uint8_t *dataPtr = static_cast<const uint8_t *>([data bytes]); + if ((dataPtr[0] == 0xff && dataPtr[1] == 0xfe) + || (dataPtr[0] == 0xfe && dataPtr[1] == 0xff)) { + if (length == 2) //Something weird? + return false; + // Skip a BOM. + dataPtr += 2; + length -= 2; + } + + // headerID + length == 3, string's length + 2 + // bytes for a null terminator. + if (!append_uint16(headers, length + 3 + 2)) + return false; + + [headers appendBytes:dataPtr length:length]; + const uint8_t nullTerminator[2] = {}; + [headers appendBytes:nullTerminator length:2]; + + return [headers length] - initialLength == length + 3 + 2; +} + +ObjCStrongReference<NSMutableData> next_data_chunk(QIODevice &inputStream, IOBluetoothOBEXSession *session, + NSUInteger headersLength, bool &isLast) +{ + // Work only for OBEX put (we request a specific payload length). + Q_ASSERT_X(session, "next_data_chunk", "invalid OBEX session (nil)"); + + const OBEXMaxPacketLength packetSize = [session getAvailableCommandPayloadLength:kOBEXOpCodePut]; + if (!packetSize || headersLength >= packetSize) + return ObjCStrongReference<NSMutableData>(); + + const OBEXMaxPacketLength maxBodySize = packetSize - headersLength; + + QVector<char> block(maxBodySize); + const int realSize = inputStream.read(block.data(), block.size()); + if (realSize <= 0) { + // Well, either the last or an error. + isLast = true; + return ObjCStrongReference<NSMutableData>(); + } + + ObjCStrongReference<NSMutableData> chunk([NSMutableData dataWithBytes:block.data() + length:realSize], true); + if (chunk && [chunk length]) { + // If it actually was the last chunk + // of a length == maxBodySize, we'll + // send one more packet (empty though)? + isLast = [chunk length] < maxBodySize; + } + + return chunk; +} + +bool check_connect_event(const OBEXSessionEvent *e, OBEXError &error, OBEXOpCode &response) +{ + Q_ASSERT_X(e, "check_connect_event", "invalid event (null)"); + + // This function tries to extract either an error code or a + // server response code. "Good" event has type connect command respond + // and reponse code 0XA0. Everything else is a "bad" event and + // means connect failed. + + // If it's an error event - return the error. + // If it's connect response - extract the response code. + // If it's something else (is it possible?) - set general error. + + if (e->type == kOBEXSessionEventTypeError) { + error = e->u.errorData.error; + return false; + } if (e->type == kOBEXSessionEventTypeConnectCommandResponseReceived) { + // We can read response code only for such an event. + response = e->u.connectCommandResponseData.serverResponseOpCode; + return response == kOBEXResponseCodeSuccessWithFinalBit; + } else { + qCWarning(QT_BT_OSX) << "check_connect_event, unexpected event type"; + error = kOBEXGeneralError; + return false; + } +} + +bool check_put_event(const OBEXSessionEvent *e, OBEXError &error, OBEXOpCode &response) +{ + Q_ASSERT_X(e, "check_put_event", "invalid event (null)"); + + // See the comments above. + + if (e->type == kOBEXSessionEventTypeError) { + error = e->u.errorData.error; + return false; + } else if (e->type == kOBEXSessionEventTypePutCommandResponseReceived) { + response = e->u.putCommandResponseData.serverResponseOpCode; + return response == kOBEXResponseCodeContinueWithFinalBit || + response == kOBEXResponseCodeSuccessWithFinalBit; + } else { + qCWarning(QT_BT_OSX) << "check_put_event, unexpected event type"; + error = kOBEXGeneralError; + return false; + } +} + +bool check_abort_event(const OBEXSessionEvent *e, OBEXError &error, OBEXOpCode &response) +{ + Q_ASSERT_X(e, "check_abort_event", "invalid event (null)"); + + if (e->type == kOBEXSessionEventTypeError) { + error = e->u.errorData.error; + return false; + } else if (e->type == kOBEXSessionEventTypeAbortCommandResponseReceived) { + response = e->u.abortCommandResponseData.serverResponseOpCode; + return response == kOBEXResponseCodeSuccessWithFinalBit; + } else { + qCWarning(QT_BT_OSX) << "check_abort_event, unexpected event type"; + return false; + } +} + +} // Unnamed namespace. +} // OSXBluetooth. + +QT_END_NAMESPACE + +#ifdef QT_NAMESPACE + +using namespace QT_NAMESPACE; + +#endif + +@interface QT_MANGLE_NAMESPACE(OSXBTOBEXSession) (PrivateAPI) + +// OBEXDisconnect returns void - it's considered to be always +// successful. These methods are "private API" - no need to expose them, +// for internal use only. +- (void)OBEXDisconnect; +- (void)OBEXDisconnectHandler:(const OBEXSessionEvent*)event; + +@end + +@implementation QT_MANGLE_NAMESPACE(OSXBTOBEXSession) + ++ (OBEXMaxPacketLength) maxPacketLength +{ + // Some arbitrary number, we'll adjust it as soon as + // we connected, asking a session about packet size for + // a particular command. + return 0x1000; +} + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(OSXBluetooth::OBEXSessionDelegate) *)aDelegate + remoteDevice:(const QBluetoothAddress &)deviceAddress channelID:(quint16)port +{ + Q_ASSERT_X(aDelegate, "-initWithDelegate:remoteDevice:channelID:", + "invalid delegate (null)"); + Q_ASSERT_X(!deviceAddress.isNull(), "-initWithDelegate:remoteDevice:channelID:", + "invalid remote device address"); + Q_ASSERT_X(port, "-initWithDelegate:remoteDevice:channelID:", + "invalid port (0)"); + + if (self = [super init]) { + connected = false; + currentRequest = OSXBluetooth::OBEXNoop; + connectionID = 0; + connectionIDFound = false; + + const BluetoothDeviceAddress addr(OSXBluetooth::iobluetooth_address(deviceAddress)); + device = [[IOBluetoothDevice deviceWithAddress:&addr] retain]; + if (!device) { + qCWarning(QT_BT_OSX) << "-initWithDelegate:remoteDevice:channelID:, " + "failed to create an IOBluetoothDevice"; + return self; + } + + session = [[IOBluetoothOBEXSession alloc] initWithDevice:device channelID:port]; + if (!session) { + qCWarning(QT_BT_OSX) << "-initWithDelegate:remoteDevice:channelID:, " + "failed to create an OBEX session"; + return self; + } + + delegate = aDelegate; + channelID = port; + } + + return self; +} + +- (void)dealloc +{ + [device release]; + [session release]; + + [headersData release]; + [bodyData release]; + + [super dealloc]; +} + +- (OBEXError)OBEXConnect +{ + if (!session) { + qCWarning(QT_BT_OSX) << "-OBEXConnect, invalid session (nil)"; + return kOBEXGeneralError; + } + + // That's a "single-shot" operation: + Q_ASSERT_X(currentRequest == OSXBluetooth::OBEXNoop, "-connect", + "can not connect in this state (another request is active)"); + + connected = false; + connectionIDFound = false; + connectionID = 0; + currentRequest = OSXBluetooth::OBEXConnect; + + const OBEXError status = [session OBEXConnect:kOBEXConnectFlagNone + maxPacketLength:[QT_MANGLE_NAMESPACE(OSXBTOBEXSession) maxPacketLength] + optionalHeaders:Q_NULLPTR + optionalHeadersLength:0 + eventSelector:@selector(OBEXConnectHandler:) + selectorTarget:self + refCon:Q_NULLPTR]; + + if (status != kOBEXSuccess) { + currentRequest = OSXBluetooth::OBEXNoop; + // Already connected is still ok for us? + connected = status == kOBEXSessionAlreadyConnectedError; + } + + return status; +} + +- (void)OBEXConnectHandler:(const OBEXSessionEvent*)event +{ + using namespace OSXBluetooth; + + Q_ASSERT_X(session, "-OBEXConnectHandler:", "invalid session (nil)"); + + if (pendingAbort) { + currentRequest = OBEXNoop; + [self OBEXAbort]; + return; + } + + if (currentRequest != OBEXConnect) { + qCWarning(QT_BT_OSX) << "-OBEXConnectHandler:, called while there is no" + "active connect request"; + return; + } + + currentRequest = OBEXNoop; + + OBEXError errorCode = kOBEXSuccess; + OBEXOpCode responseCode = kOBEXResponseCodeSuccessWithFinalBit; + + if (!check_connect_event(event, errorCode, responseCode)) { + // OBEX connect failed. + if (delegate) + delegate->OBEXConnectError(errorCode, responseCode); + return; + } + + const OBEXConnectCommandResponseData *const response = &event->u.connectCommandResponseData; + if (response->headerDataPtr && response->headerDataLength >= 2) { + // 2 == 1 byte headerID + at least 1 byte headerValue ... + QList<OBEXHeader> headers(qt_bluetooth_headers(static_cast<const uint8_t *>(response->headerDataPtr), + response->headerDataLength)); + // ConnectionID is used when multiplexing OBEX connections + // to identify which particular connection this object is + // being sent on. When used, this _must_ be the first + // header sent. + + foreach (const OBEXHeader &header, headers) { + if (header.headerID == kOBEXHeaderIDConnectionID) { + connectionID = header.value.value<quint32>(); + connectionIDFound = true; + break; + } + } + } + + connected = true; + + if (delegate) + delegate->OBEXConnectSuccess(); +} + +- (OBEXError)OBEXAbort +{ + using namespace OSXBluetooth; + + Q_ASSERT_X(session, "-OBEXAbort", "invalid OBEX session (nil)"); + + if (currentRequest == OBEXNoop) { + pendingAbort = false; + + if (![self isConnected]) + return kOBEXSessionNotConnectedError; + + currentRequest = OBEXAbort; + const OBEXError status = [session OBEXAbort:Q_NULLPTR + optionalHeadersLength:0 + eventSelector:@selector(OBEXAbortHandler:) + selectorTarget:self + refCon:Q_NULLPTR]; + if (status != kOBEXSuccess) + currentRequest = OBEXNoop; + + return status; + } else { + // We're in the middle of some request, wait + // for any handler to be called first. + pendingAbort = true; + return kOBEXSuccess; + } +} + +- (void)OBEXAbortHandler:(const OBEXSessionEvent*)event +{ + using namespace OSXBluetooth; + + Q_ASSERT_X(session, "-OBEXAbortHandler:", "invalid OBEX session (nil)"); + + if (currentRequest != OBEXAbort) { + qCWarning(QT_BT_OSX) << "-OBEXAbortHandler:, called while there " + "is no ABORT request"; + return; + } + + pendingAbort = false; + currentRequest = OBEXNoop; + + if (delegate) { + OBEXError error = kOBEXSuccess; + OBEXOpCode response = kOBEXResponseCodeSuccessWithFinalBit; + if (check_abort_event(event, error, response)) + delegate->OBEXAbortSuccess(); + } +} + +- (OBEXError)OBEXPutFile:(QT_PREPEND_NAMESPACE(QIODevice) *)input withName:(const QString &)name +{ + using namespace OSXBluetooth; + + if (!session || ![self isConnected]) + return kOBEXSessionNotConnectedError; + + Q_ASSERT_X(currentRequest == OBEXNoop, "-OBEXPutFile:withName:", + "the current session has an active request already"); + Q_ASSERT_X(input, "-OBEXPutFile:withName:", "invalid input stream (null)"); + Q_ASSERT_X(input->isReadable(), "-OBEXPutFile:withName:", + "invalid input stream (not readable)"); + + // We send a put request with a couple of headers (size/file name/may be connection ID) + + // a payload. + const qint64 fileSize = input->size(); + if (fileSize <= 0 || fileSize >= std::numeric_limits<uint32_t>::max()) { + qCWarning(QT_BT_OSX) << "-OBEXPutFile:withName:, invalid input file size"; + return kOBEXBadArgumentError; + } + + ObjCStrongReference<NSMutableData> headers([[NSMutableData alloc] init], false); + if (!headers) { + qCWarning(QT_BT_OSX) << "-OBEXPutFile:withName:, failed to allocate headers"; + return kOBEXNoResourcesError; + } + + // Now we append headers with: Connection ID (if any), + // file name, file size, the first (and probably the only) chunk of data + // from the input stream and send a put request. + + if (connectionIDFound) { + if (!append_four_byte_header(headers, kOBEXHeaderIDConnectionID, connectionID)) { + qCWarning(QT_BT_OSX) << "-OBEXPutFile:withName:, " + "failed to append connection ID header"; + return kOBEXNoResourcesError; + } + } + + if (name.length()) { + if (!append_unicode_header(headers, kOBEXHeaderIDName, name)) { + qCWarning(QT_BT_OSX) << "-OBEXPutFile:withName:, " + "failed to append a unicode string"; + return kOBEXNoResourcesError; + } + } + + if (fileSize && !input->isSequential()) + append_four_byte_header(headers, kOBEXHeaderIDLength, uint32_t(fileSize)); + + bool lastChunk = false; + ObjCStrongReference<NSMutableData> chunk(next_data_chunk(*input, session, [headers length], lastChunk)); + if (!chunk || ![chunk length]) { + // We do not support PUT-DELETE (?) + // At least the first chunk is expected to be non-empty. + qCWarning(QT_BT_OSX) << "-OBEXPutFile:withName:, invalid input stream"; + return kOBEXBadArgumentError; + } + + currentRequest = OBEXPut; + + const OBEXError status = [session OBEXPut:lastChunk + headersData:[headers mutableBytes] + headersDataLength:[headers length] + bodyData:[chunk mutableBytes] + bodyDataLength:[chunk length] + eventSelector:@selector(OBEXPutHandler:) + selectorTarget:self + refCon:Q_NULLPTR]; + + if (status == kOBEXSuccess) { + if (delegate && fileSize && !input->isSequential()) + delegate->OBEXPutDataSent([chunk length], fileSize); + + bytesSent = [chunk length]; + headersData = headers.take(); + bodyData = chunk.take(); + inputStream = input; + } else { + // PUT request failed and we now + // want to close a connection/session. + currentRequest = OBEXNoop; + // Try to cleanup (disconnect). + [self OBEXDisconnect]; + } + + return status; +} + +- (void)OBEXPutHandler:(const OBEXSessionEvent*)event +{ + using namespace OSXBluetooth; + + Q_ASSERT_X(session, "-OBEXPutHandler:", "invalid OBEX session (nil)"); + + if (pendingAbort) { + currentRequest = OBEXNoop; + [self OBEXAbort]; + return; + } + + if (currentRequest != OBEXPut) { + qCWarning(QT_BT_OSX) << "-OBEXPutHandler:, called while the current " + "request is not a put request"; + return; + } + + OBEXError error = kOBEXSuccess; + OBEXOpCode responseCode = kOBEXResponseCodeSuccessWithFinalBit; + if (!check_put_event(event, error, responseCode)) { + currentRequest = OBEXNoop; + if (delegate) + delegate->OBEXPutError(error, responseCode); + [self OBEXDisconnect]; + return; + } + + // Now try to send more data if we have any. + if (responseCode == kOBEXResponseCodeContinueWithFinalBit) { + // Send more data. + bool lastChunk = false; + // 0 for the headers length, no more headers. + ObjCStrongReference<NSMutableData> chunk(next_data_chunk(*inputStream, session, 0, lastChunk)); + if (!chunk && !lastChunk) { + qCWarning(QT_BT_OSX) << "-OBEXPutHandler:, failed to " + "allocate the next memory chunk"; + return; + } + + void *dataPtr = chunk ? [chunk mutableBytes] : Q_NULLPTR; + const NSUInteger dataSize = chunk ? [chunk length] : 0; + const OBEXError status = [session OBEXPut:lastChunk + headersData:Q_NULLPTR + headersDataLength:0 + bodyData:dataPtr + bodyDataLength:dataSize + eventSelector:@selector(OBEXPutHandler:) + selectorTarget:self + refCon:Q_NULLPTR]; + + if (status != kOBEXSuccess) { + qCWarning(QT_BT_OSX) << "-OBEXPutHandler:, failed to " + "send the next memory chunk"; + currentRequest = OBEXNoop; + if (delegate) // Response code is not important here. + delegate->OBEXPutError(kOBEXNoResourcesError, 0); + + [self OBEXDisconnect]; + } else { + [bodyData release]; + bytesSent += [chunk length]; + bodyData = chunk.take();//retained already. + + if (delegate && !inputStream->isSequential()) + delegate->OBEXPutDataSent(bytesSent, inputStream->size()); + } + } else if (responseCode == kOBEXResponseCodeSuccessWithFinalBit) { + currentRequest = OBEXNoop; + if (delegate) + delegate->OBEXPutSuccess(); + + [self OBEXDisconnect]; + } +} + +- (void)OBEXDisconnect +{ + Q_ASSERT_X(session, "-OBEXDisconnect", "invalid session (nil)"); + + currentRequest = OSXBluetooth::OBEXDisconnect; + + [session OBEXDisconnect:Q_NULLPTR + optionalHeadersLength:0 + eventSelector:@selector(OBEXDisconnectHandler:) + selectorTarget:self + refCon:Q_NULLPTR]; +} + +- (void)OBEXDisconnectHandler:(const OBEXSessionEvent*)event +{ + Q_UNUSED(event) + + Q_ASSERT_X(session, "-OBEXDisconnectHandler:", "invalid session (nil)"); + + // Event can have an error type, but there's nothing + // we can do - even "cleanup" failed. + connected = false; +} + +- (bool)isConnected +{ + return device && session && connected; +} + +- (void)closeSession +{ + // Clear the delegate and reset the request, + // do not try any of OBEX commands - the session will be deleted + // immediately. + delegate = Q_NULLPTR; + // This will stop any handler (callback) preventing + // any read/write to potentially deleted objects. + currentRequest = OSXBluetooth::OBEXNoop; +} + +- (bool)hasActiveRequest +{ + return currentRequest != OSXBluetooth::OBEXNoop && !pendingAbort; +} + +@end diff --git a/src/bluetooth/osx/osxbtobexsession_p.h b/src/bluetooth/osx/osxbtobexsession_p.h new file mode 100644 index 00000000..aa142b5a --- /dev/null +++ b/src/bluetooth/osx/osxbtobexsession_p.h @@ -0,0 +1,147 @@ +/**************************************************************************** +** +** 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 <QtCore/qvariant.h> +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +// Import - Obj-C headers do not have inclusion guards: +#import <IOBluetooth/objc/OBEXSession.h> + +@class IOBluetoothOBEXSession; +@class IOBluetoothDevice; + +@class QT_MANGLE_NAMESPACE(OSXBTOBEXSession); + +QT_BEGIN_NAMESPACE + +class QBluetoothAddress; +class QIODevice; +class QString; + +namespace OSXBluetooth +{ + +class OBEXSessionDelegate +{ +public: + typedef QT_MANGLE_NAMESPACE(OSXBTOBEXSession) ObjCOBEXSession; + + virtual ~OBEXSessionDelegate(); + + virtual void OBEXConnectError(OBEXError error, OBEXOpCode responseCode) = 0; + virtual void OBEXConnectSuccess() = 0; + + virtual void OBEXAbortSuccess() = 0; + + virtual void OBEXPutDataSent(quint32 current, quint32 total) = 0; + virtual void OBEXPutSuccess() = 0; + virtual void OBEXPutError(OBEXError error, OBEXOpCode responseCode) = 0; +}; + +enum OBEXRequest { + OBEXNoop, + OBEXConnect, + OBEXDisconnect, + OBEXPut, + OBEXGet, + OBEXSetPath, + OBEXAbort +}; + +} + +QT_END_NAMESPACE + +// OBEX Session, it's a "single-shot" operation as our QBluetoothTransferReply is +// (it does not have an interface to re-send data or re-use the same transfer reply). +// It either succeeds or fails and tries to cleanup in any case. +@interface QT_MANGLE_NAMESPACE(OSXBTOBEXSession) : NSObject +{ + QT_PREPEND_NAMESPACE(OSXBluetooth::OBEXSessionDelegate) *delegate; + IOBluetoothDevice *device; + quint16 channelID; + IOBluetoothOBEXSession *session; + + QT_PREPEND_NAMESPACE(OSXBluetooth)::OBEXRequest currentRequest; + + bool connected; + bool connectionIDFound; + quint32 connectionID; + + QT_PREPEND_NAMESPACE(QIODevice) *inputStream; + + // TODO: switch to scoped pointers or strong reference objects instead. + NSMutableData *headersData; + NSMutableData *bodyData; + + quint32 bytesSent; + bool pendingAbort; +} + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(OSXBluetooth::OBEXSessionDelegate) *)aDelegate + remoteDevice:(const QBluetoothAddress &)deviceAddress channelID:(quint16)port; + +- (void)dealloc; + +// Below I have pairs: OBEX operation and its callback method. +- (OBEXError)OBEXConnect; +- (void)OBEXConnectHandler:(const OBEXSessionEvent*)event; + +- (OBEXError)OBEXAbort; +- (void)OBEXAbortHandler:(const OBEXSessionEvent*)event; + +- (OBEXError)OBEXPutFile:(QT_PREPEND_NAMESPACE(QIODevice) *)inputStream withName:(const QString &)name; +- (void)OBEXPutHandler:(const OBEXSessionEvent*)event; + +// Aux. methods. +- (bool)isConnected; + +// To be called from C++ destructors. OBEXSession is not +// valid anymore after this call (no more OBEX operations +// can be executed). It's an ABORT/DISCONNECT sequence. +// It also resets a delegate to null. +- (void)closeSession; +// +- (bool)hasActiveRequest; + +@end diff --git a/src/bluetooth/osx/osxbtutility_p.h b/src/bluetooth/osx/osxbtutility_p.h index 1c20c91a..cad84b7e 100644 --- a/src/bluetooth/osx/osxbtutility_p.h +++ b/src/bluetooth/osx/osxbtutility_p.h @@ -159,6 +159,13 @@ public: { return m_ptr; } + + T *take() + { + T * p = m_ptr; + m_ptr = nil; + return p; + } private: T *m_ptr; }; diff --git a/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm b/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm index 9924f9ae..21744f0c 100644 --- a/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm +++ b/src/bluetooth/qbluetoothservicediscoveryagent_osx.mm @@ -309,6 +309,10 @@ void QBluetoothServiceDiscoveryAgentPrivate::SDPInquiryFinished(IOBluetoothDevic if (!isDuplicatedService(serviceInfo)) { discoveredServices.append(serviceInfo); emit q_ptr->serviceDiscovered(serviceInfo); + // Here a user code can ... interrupt us by calling + // stop. Check this. + if (state == Inactive) + break; } } diff --git a/src/bluetooth/qbluetoothtransfermanager.cpp b/src/bluetooth/qbluetoothtransfermanager.cpp index 0754fb9a..0897465d 100644 --- a/src/bluetooth/qbluetoothtransfermanager.cpp +++ b/src/bluetooth/qbluetoothtransfermanager.cpp @@ -38,7 +38,8 @@ #include "qbluetoothtransferreply_bluez_p.h" #elif QT_QNX_BLUETOOTH #include "qbluetoothtransferreply_qnx_p.h" -#else +#elif QT_OSX_BLUETOOTH +#include "qbluetoothtransferreply_osx_p.h" #endif QT_BEGIN_NAMESPACE @@ -99,6 +100,10 @@ QBluetoothTransferReply *QBluetoothTransferManager::put(const QBluetoothTransfer QBluetoothTransferReplyQnx *reply = new QBluetoothTransferReplyQnx(data, request, this); connect(reply, SIGNAL(finished(QBluetoothTransferReply*)), this, SIGNAL(finished(QBluetoothTransferReply*))); return reply; +#elif QT_OSX_BLUETOOTH + QBluetoothTransferReply *reply = new QBluetoothTransferReplyOSX(data, request, this); + connect(reply, SIGNAL(finished(QBluetoothTransferReply*)), this, SIGNAL(finished(QBluetoothTransferReply*))); + return reply; #else Q_UNUSED(request); Q_UNUSED(data); diff --git a/src/bluetooth/qbluetoothtransferreply_osx.mm b/src/bluetooth/qbluetoothtransferreply_osx.mm new file mode 100644 index 00000000..3204e7b6 --- /dev/null +++ b/src/bluetooth/qbluetoothtransferreply_osx.mm @@ -0,0 +1,473 @@ +/**************************************************************************** +** +** 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:LGPL21$ +** 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 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** 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. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qbluetoothservicediscoveryagent.h" +#include "qbluetoothtransferreply_osx_p.h" +#include "osx/osxbtobexsession_p.h" +#include "qbluetoothserviceinfo.h" +#include "osx/osxbtutility_p.h" +#include "qbluetoothuuid.h" + + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qtemporaryfile.h> +#include <QtCore/qmetaobject.h> +#include <QtCore/qfileinfo.h> +#include <QtCore/qstring.h> +#include <QtCore/qdebug.h> +#include <QtCore/qfile.h> +#include <QtCore/qdir.h> + +QT_BEGIN_NAMESPACE + +class QBluetoothTransferReplyOSXPrivate : OSXBluetooth::OBEXSessionDelegate +{ + friend class QBluetoothTransferReplyOSX; +public: + QBluetoothTransferReplyOSXPrivate(QBluetoothTransferReplyOSX *q, QIODevice *inputStream); + + ~QBluetoothTransferReplyOSXPrivate(); + + bool isActive() const; + bool startOPP(const QBluetoothAddress &device); + + // + void sendConnect(const QBluetoothAddress &device, quint16 channelID); + void sendPut(); + +private: + // OBEX session delegate: + void OBEXConnectError(OBEXError errorCode, OBEXOpCode response) Q_DECL_OVERRIDE; + void OBEXConnectSuccess() Q_DECL_OVERRIDE; + + void OBEXAbortSuccess() Q_DECL_OVERRIDE; + + void OBEXPutDataSent(quint32 current, quint32 total) Q_DECL_OVERRIDE; + void OBEXPutSuccess() Q_DECL_OVERRIDE; + void OBEXPutError(OBEXError error, OBEXOpCode response) Q_DECL_OVERRIDE; + + QBluetoothTransferReplyOSX *q_ptr; + + QIODevice *inputStream; + + QBluetoothTransferReply::TransferError error; + QString errorString; + + // Set requestComplete, error, description, emit error, emit finished. + // Too many things in one, but not to repeat this code everywhere. + void setReplyError(QBluetoothTransferReply::TransferError errorCode, + const QString &errorMessage); + + // With a given API, we have to discover a service first + // since we need a channel ID to work with OBEX session. + // Also, service discovery agent does not have an interface + // to test discovery mode, that's why we have this bool here. + bool minimalScan; + QScopedPointer<QBluetoothServiceDiscoveryAgent> agent; + + // The next step is to create an OBEX session: + typedef OSXBluetooth::ObjCScopedPointer<ObjCOBEXSession> OBEXSession; + OBEXSession session; + + // Both success and failure to send - transfer is complete. + bool requestComplete; + + // We need a temporary file to generate an unique name + // in case inputStream is not a file. QTemporaryFile not + // only creates a random name, it also guarantees this name + // is unique. The amount of code to generate such a name + // is amaizingly huge (and will require global variables) + // - so a temporary file can help. + QScopedPointer<QTemporaryFile> temporaryFile; +}; + +QBluetoothTransferReplyOSXPrivate::QBluetoothTransferReplyOSXPrivate(QBluetoothTransferReplyOSX *q, + QIODevice *input) + : q_ptr(q), + inputStream(input), + error(QBluetoothTransferReply::NoError), + minimalScan(true), + requestComplete(false) +{ + Q_ASSERT_X(q, "QBluetoothTransferReplyOSXPrivate", "invalid q_ptr (null)"); +} + +QBluetoothTransferReplyOSXPrivate::~QBluetoothTransferReplyOSXPrivate() +{ + // closeSession will set a delegate to null. + // The OBEX session will be closed then. If + // somehow IOBluetooth/OBEX still has a reference to our + // session, it will not call any of delegate's callbacks. + if (session) + [session closeSession]; +} + +bool QBluetoothTransferReplyOSXPrivate::isActive() const +{ + return agent || (session && [session hasActiveRequest]); +} + +bool QBluetoothTransferReplyOSXPrivate::startOPP(const QBluetoothAddress &device) +{ + Q_ASSERT_X(!isActive(), "startOPP", "already started"); + Q_ASSERT_X(!device.isNull(), "startOPP", "invalid device address"); + + errorString.clear(); + error = QBluetoothTransferReply::NoError; + + agent.reset(new QBluetoothServiceDiscoveryAgent); + + agent->setRemoteAddress(device); + agent->setUuidFilter(QBluetoothUuid(QBluetoothUuid::ObexObjectPush)); + + QObject::connect(agent.data(), SIGNAL(finished()), q_ptr, SLOT(serviceDiscoveryFinished())); + QObject::connect(agent.data(), SIGNAL(error(QBluetoothServiceDiscoveryAgent::Error)), + q_ptr, SLOT(serviceDiscoveryError(QBluetoothServiceDiscoveryAgent::Error))); + + minimalScan = true; + agent->start(QBluetoothServiceDiscoveryAgent::MinimalDiscovery); + + // We probably failed already. + return error == QBluetoothTransferReply::NoError; +} + +void QBluetoothTransferReplyOSXPrivate::sendConnect(const QBluetoothAddress &device, quint16 channelID) +{ + Q_ASSERT_X(!session, "sendConnect", "session is already active"); + + error = QBluetoothTransferReply::NoError; + errorString.clear(); + + if (device.isNull() || !channelID) { + qCWarning(QT_BT_OSX) << "QBluetoothTransferReplyOSXPrivate::sendConnect(), " + "invalid device address or port"; + + setReplyError(QBluetoothTransferReply::HostNotFoundError, + QObject::tr("Invalid target address")); + return; + } + + OBEXSession newSession([[ObjCOBEXSession alloc] initWithDelegate:this + remoteDevice:device channelID:channelID]); + if (!newSession) { + qCWarning(QT_BT_OSX) << "QBluetoothTransferReplyOSXPrivate::sendConnect(), " + "failed to allocate OSXBTOBEXSession object"; + + setReplyError(QBluetoothTransferReply::UnknownError, + QObject::tr("Failed to create an OBEX session")); + return; + } + + const OBEXError status = [newSession OBEXConnect]; + + if ((status == kOBEXSuccess || status == kOBEXSessionAlreadyConnectedError) + && error == QBluetoothTransferReply::NoError) { + session.reset(newSession.take()); + if ([session isConnected]) + sendPut();// Connected, send a PUT request. + } else { + qCWarning(QT_BT_OSX) << "QBluetoothTransferReplyOSXPrivate::sendConnect(), " + "OBEXConnect failed"; + + if (error == QBluetoothTransferReply::NoError) { + // The error is not set yet. + error = QBluetoothTransferReply::SessionError; + errorString = QObject::tr("OBEXConnect failed"); + } + + requestComplete = true; + emit q_ptr->error(error); + emit q_ptr->finished(q_ptr); + } +} + +void QBluetoothTransferReplyOSXPrivate::sendPut() +{ + Q_ASSERT_X(inputStream, "sendPut", "invalid input stream (null)"); + Q_ASSERT_X(session, "sendPut", "invalid OBEX session (nil)"); + Q_ASSERT_X([session isConnected], "sendPut", "not connected"); + Q_ASSERT_X(![session hasActiveRequest], "sendPut", + "session already has an active request"); + + QString fileName; + QFile *const file = qobject_cast<QFile *>(inputStream); + if (file) { + if (!file->exists()) { + setReplyError(QBluetoothTransferReply::FileNotFoundError, + QObject::tr("Source file does not exist")); + return; + } else if (!file->isReadable()) { + file->open(QIODevice::ReadOnly); + if (!file->isReadable()) { + setReplyError(QBluetoothTransferReply::IODeviceNotReadableError, + QObject::tr("File cannot be read. " + "Make sure it is open for reading.")); + return; + } + } + + fileName = file->fileName(); + } else { + if (!inputStream->isReadable()) { + setReplyError(QBluetoothTransferReply::IODeviceNotReadableError, + QObject::tr("QIODevice cannot be read. " + "Make sure it is open for reading.")); + return; + } + + temporaryFile.reset(new QTemporaryFile); + temporaryFile->open(); + fileName = temporaryFile->fileName(); + } + + const QFileInfo fileInfo(fileName); + fileName = fileInfo.fileName(); + + if ([session OBEXPutFile:inputStream withName:fileName] != kOBEXSuccess) { + // TODO: convert OBEXError into something reasonable? + setReplyError(QBluetoothTransferReply::SessionError, + QObject::tr("OBEX put failed")); + } +} + + +void QBluetoothTransferReplyOSXPrivate::OBEXConnectError(OBEXError errorCode, OBEXOpCode response) +{ + Q_UNUSED(errorCode) + Q_UNUSED(response) + + if (session) { + setReplyError(QBluetoothTransferReply::SessionError, + QObject::tr("OBEX connect failed")); + } else { + // Else we're still in OBEXConnect, in a call-back + // and do not want to emit yet (will be done a bit later). + error = QBluetoothTransferReply::SessionError; + errorString = QObject::tr("OBEX connect failed"); + requestComplete = true; + } +} + +void QBluetoothTransferReplyOSXPrivate::OBEXConnectSuccess() +{ + // Now that OBEX connect succeeded, we can send an OBEX put request. + if (!session) { + // We're still in OBEXConnect(), it'll take care of next steps. + return; + } + + sendPut(); +} + +void QBluetoothTransferReplyOSXPrivate::OBEXAbortSuccess() +{ + // TODO: +} + +void QBluetoothTransferReplyOSXPrivate::OBEXPutDataSent(quint32 current, quint32 total) +{ + emit q_ptr->transferProgress(current, total); +} + +void QBluetoothTransferReplyOSXPrivate::OBEXPutSuccess() +{ + requestComplete = true; + emit q_ptr->finished(q_ptr); +} + +void QBluetoothTransferReplyOSXPrivate::OBEXPutError(OBEXError errorCode, OBEXOpCode responseCode) +{ + // Error can be reported by errorCode or responseCode + // (that's how errors are reported in OBEXSession events). + // errorCode and responseCode are "mutually exclusive". + + Q_UNUSED(responseCode) + + if (errorCode != kOBEXSuccess) { + // TODO: errorCode -> TransferError. + } else { + // TODO: a response code can give some interesting information, + // like "forbidden" etc. - convert this into more reasonable error. + } + + setReplyError(QBluetoothTransferReply::SessionError, + QObject::tr("OBEX PUT command failed")); +} + +void QBluetoothTransferReplyOSXPrivate::setReplyError(QBluetoothTransferReply::TransferError errorCode, + const QString &description) +{ + // Not to be used to clear an error! + + error = errorCode; + errorString = description; + requestComplete = true; + emit q_ptr->error(error); + emit q_ptr->finished(q_ptr); +} + + +QBluetoothTransferReplyOSX::QBluetoothTransferReplyOSX(QIODevice *input, + const QBluetoothTransferRequest &request, + QBluetoothTransferManager *manager) + : QBluetoothTransferReply(manager) + +{ + Q_UNUSED(input) + + setManager(manager); + setRequest(request); + + osx_d_ptr.reset(new QBluetoothTransferReplyOSXPrivate(this, input)); + + if (input) { + QMetaObject::invokeMethod(this, "start", Qt::QueuedConnection); + } else { + qCWarning(QT_BT_OSX) << "QBluetoothTransferReplyOSX::QBluetoothTransferReplyOSX(), " + "invalid input stream (null)"; + + osx_d_ptr->requestComplete = true; + osx_d_ptr->errorString = tr("Invalid input file (null)"); + osx_d_ptr->error = FileNotFoundError; + QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, + Q_ARG(QBluetoothTransferReply::TransferError, FileNotFoundError)); + } +} + +QBluetoothTransferReplyOSX::~QBluetoothTransferReplyOSX() +{ + // A dtor to make a scoped pointer with incomplete type happy. +} + +QBluetoothTransferReply::TransferError QBluetoothTransferReplyOSX::error() const +{ + return osx_d_ptr->error; +} + +QString QBluetoothTransferReplyOSX::errorString() const +{ + return osx_d_ptr->errorString; +} + +bool QBluetoothTransferReplyOSX::isFinished() const +{ + return osx_d_ptr->requestComplete; +} + +bool QBluetoothTransferReplyOSX::isRunning() const +{ + return osx_d_ptr->isActive(); +} + +bool QBluetoothTransferReplyOSX::abort() +{ + // Reset a delegate. + [osx_d_ptr->session closeSession]; + // Should never be called from an OBEX callback! + osx_d_ptr->session.reset(Q_NULLPTR); + + // Not setReplyError, we emit finished only! + osx_d_ptr->requestComplete = true; + osx_d_ptr->errorString = tr("Operation canceled"); + osx_d_ptr->error = UserCanceledTransferError; + + emit finished(this); + + return true; +} + +bool QBluetoothTransferReplyOSX::start() +{ + // OBEXSession requires a channel ID and we have to find it first, + // using QBluetoothServiceDiscoveryAgent (singleDevice, OBEX uuid filter, start + // from MinimalDiscovery mode and continue with FullDiscovery if + // MinimalDiscovery fails. + + if (!osx_d_ptr->isActive()) { + // Step 0: find a channelID. + if (request().address().isNull()) { + qCWarning(QT_BT_OSX) << "QBluetoothTransferReplyOSX::start(), " + "invalid device address"; + osx_d_ptr->setReplyError(HostNotFoundError, tr("Invalid target address")); + return false; + } + + return osx_d_ptr->startOPP(request().address()); + } else { + osx_d_ptr->setReplyError(UnknownError, tr("Transfer already started")); + return false; + } +} + +void QBluetoothTransferReplyOSX::serviceDiscoveryFinished() +{ + Q_ASSERT_X(osx_d_ptr->agent.data(), "serviceDiscoveryFinished", + "invalid service discovery agent (null)"); + + const QList<QBluetoothServiceInfo> services = osx_d_ptr->agent->discoveredServices(); + if (services.size()) { + // TODO: what if we have several? + const QBluetoothServiceInfo &foundOBEX = services.front(); + osx_d_ptr->sendConnect(request().address(), foundOBEX.serverChannel()); + } else { + if (osx_d_ptr->minimalScan) { + // Try full discovery now. + osx_d_ptr->minimalScan = false; + osx_d_ptr->agent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery); + } else { + // No service record, no channel ID, no OBEX session. + osx_d_ptr->setReplyError(HostNotFoundError, tr("OBEX/OPP serice not found")); + } + } +} + +void QBluetoothTransferReplyOSX::serviceDiscoveryError(QBluetoothServiceDiscoveryAgent::Error errorCode) +{ + Q_ASSERT_X(osx_d_ptr->agent.data(), "serviceDiscoveryError", "invalid service discovery agent (null)"); + + if (errorCode == QBluetoothServiceDiscoveryAgent::PoweredOffError) { + // There's nothing else we can do. + osx_d_ptr->setReplyError(UnknownError, tr("Bluetooth adapter is powered off")); + return; + } + + if (osx_d_ptr->minimalScan) {// Try again, this time in FullDiscovery mode. + osx_d_ptr->minimalScan = false; + osx_d_ptr->agent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery); + } else { + osx_d_ptr->setReplyError(HostNotFoundError, tr("Invalid target address")); + } +} + +QT_END_NAMESPACE diff --git a/src/bluetooth/qbluetoothtransferreply_osx_p.h b/src/bluetooth/qbluetoothtransferreply_osx_p.h new file mode 100644 index 00000000..cc973bfa --- /dev/null +++ b/src/bluetooth/qbluetoothtransferreply_osx_p.h @@ -0,0 +1,84 @@ +/**************************************************************************** +** +** 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:LGPL21$ +** 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 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** 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. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QBLUETOOTHTRANSFERREPLY_OSX_P_H +#define QBLUETOOTHTRANSFERREPLY_OSX_P_H + +#include "qbluetoothservicediscoveryagent.h" +#include "qbluetoothtransferreply.h" + +#include <QtCore/qscopedpointer.h> +#include <QtCore/qglobal.h> + +QT_BEGIN_NAMESPACE + +class QBluetoothTransferReplyOSXPrivate; +class QBluetoothServiceInfo; +class QBluetoothAddress; +class QIODevice; + +class Q_BLUETOOTH_EXPORT QBluetoothTransferReplyOSX : public QBluetoothTransferReply +{ + Q_OBJECT + +public: + QBluetoothTransferReplyOSX(QIODevice *input, const QBluetoothTransferRequest &request, + QBluetoothTransferManager *parent); + ~QBluetoothTransferReplyOSX(); + + TransferError error() const Q_DECL_OVERRIDE; + QString errorString() const Q_DECL_OVERRIDE; + + bool isFinished() const Q_DECL_OVERRIDE; + bool isRunning() const Q_DECL_OVERRIDE; + +Q_SIGNALS: + void error(QBluetoothTransferReply::TransferError lastError); + +public slots: + bool abort(); + +private slots: + bool start(); + + void serviceDiscoveryFinished(); + void serviceDiscoveryError(QBluetoothServiceDiscoveryAgent::Error error); + +private: + // OS X private data (not to be seen by moc). + QScopedPointer<QBluetoothTransferReplyOSXPrivate> osx_d_ptr; +}; + +QT_END_NAMESPACE + +#endif |