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 /src/bluetooth/qbluetoothtransferreply_osx.mm | |
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>
Diffstat (limited to 'src/bluetooth/qbluetoothtransferreply_osx.mm')
-rw-r--r-- | src/bluetooth/qbluetoothtransferreply_osx.mm | 473 |
1 files changed, 473 insertions, 0 deletions
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 |