| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** 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 The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/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 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $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 "osx/uistrings_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) override; |
| void OBEXConnectSuccess() override; |
| |
| void OBEXAbortSuccess() override; |
| |
| void OBEXPutDataSent(quint32 current, quint32 total) override; |
| void OBEXPutSuccess() override; |
| void OBEXPutError(OBEXError error, OBEXOpCode response) 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, Q_FUNC_INFO, "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.data()) |
| [session closeSession]; |
| } |
| |
| bool QBluetoothTransferReplyOSXPrivate::isActive() const |
| { |
| return agent.data() || (session.data() && [session hasActiveRequest]); |
| } |
| |
| bool QBluetoothTransferReplyOSXPrivate::startOPP(const QBluetoothAddress &device) |
| { |
| Q_ASSERT_X(!isActive(), Q_FUNC_INFO, "already started"); |
| Q_ASSERT_X(!device.isNull(), Q_FUNC_INFO, "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, Q_FUNC_INFO, "session is already active"); |
| |
| error = QBluetoothTransferReply::NoError; |
| errorString.clear(); |
| |
| if (device.isNull() || !channelID) { |
| qCWarning(QT_BT_OSX) << "invalid device address or port"; |
| setReplyError(QBluetoothTransferReply::HostNotFoundError, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_INVAL_TARGET)); |
| return; |
| } |
| |
| OBEXSession newSession([[ObjCOBEXSession alloc] initWithDelegate:this |
| remoteDevice:device channelID:channelID]); |
| if (!newSession) { |
| qCWarning(QT_BT_OSX) << "failed to allocate OSXBTOBEXSession object"; |
| |
| setReplyError(QBluetoothTransferReply::UnknownError, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_SESSION_NO_START)); |
| 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) << "OBEXConnect failed"; |
| |
| if (error == QBluetoothTransferReply::NoError) { |
| // The error is not set yet. |
| error = QBluetoothTransferReply::SessionError; |
| errorString = QCoreApplication::translate(TRANSFER_REPLY, TR_CONNECT_FAILED); |
| } |
| |
| requestComplete = true; |
| emit q_ptr->error(error); |
| emit q_ptr->finished(q_ptr); |
| } |
| } |
| |
| void QBluetoothTransferReplyOSXPrivate::sendPut() |
| { |
| Q_ASSERT_X(inputStream, Q_FUNC_INFO, "invalid input stream (null)"); |
| Q_ASSERT_X(session.data(), Q_FUNC_INFO, "invalid OBEX session (nil)"); |
| Q_ASSERT_X([session isConnected], Q_FUNC_INFO, "not connected"); |
| Q_ASSERT_X(![session hasActiveRequest], Q_FUNC_INFO, |
| "session already has an active request"); |
| |
| QString fileName; |
| QFile *const file = qobject_cast<QFile *>(inputStream); |
| if (file) { |
| if (!file->exists()) { |
| setReplyError(QBluetoothTransferReply::FileNotFoundError, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_FILE_NOT_EXIST)); |
| return; |
| } else if (!file->isReadable()) { |
| file->open(QIODevice::ReadOnly); |
| if (!file->isReadable()) { |
| setReplyError(QBluetoothTransferReply::IODeviceNotReadableError, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_NOT_READ_IODEVICE)); |
| return; |
| } |
| } |
| |
| fileName = file->fileName(); |
| } else { |
| if (!inputStream->isReadable()) { |
| setReplyError(QBluetoothTransferReply::IODeviceNotReadableError, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_NOT_READ_IODEVICE)); |
| 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, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_SESSION_FAILED)); |
| } |
| } |
| |
| |
| void QBluetoothTransferReplyOSXPrivate::OBEXConnectError(OBEXError errorCode, OBEXOpCode response) |
| { |
| Q_UNUSED(errorCode) |
| Q_UNUSED(response) |
| |
| if (session.data()) { |
| setReplyError(QBluetoothTransferReply::SessionError, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_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 = QCoreApplication::translate(TRANSFER_REPLY, TR_CONNECT_FAILED); |
| requestComplete = true; |
| } |
| } |
| |
| void QBluetoothTransferReplyOSXPrivate::OBEXConnectSuccess() |
| { |
| // Now that OBEX connect succeeded, we can send an OBEX put request. |
| if (!session.data()) { |
| // 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, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_SESSION_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) << "invalid input stream (null)"; |
| osx_d_ptr->requestComplete = true; |
| osx_d_ptr->errorString = QCoreApplication::translate(TRANSFER_REPLY, TR_INVALID_DEVICE); |
| 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(nullptr); |
| |
| // Not setReplyError, we emit finished only! |
| osx_d_ptr->requestComplete = true; |
| osx_d_ptr->errorString = QCoreApplication::translate(TRANSFER_REPLY, TR_OP_CANCEL); |
| 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) << "invalid device address"; |
| osx_d_ptr->setReplyError(HostNotFoundError, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_INVAL_TARGET)); |
| return false; |
| } |
| |
| return osx_d_ptr->startOPP(request().address()); |
| } else { |
| osx_d_ptr->setReplyError(UnknownError, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_IN_PROGRESS)); |
| return false; |
| } |
| } |
| |
| void QBluetoothTransferReplyOSX::serviceDiscoveryFinished() |
| { |
| Q_ASSERT_X(osx_d_ptr->agent.data(), Q_FUNC_INFO, |
| "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, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_SERVICE_NO_FOUND)); |
| } |
| } |
| } |
| |
| void QBluetoothTransferReplyOSX::serviceDiscoveryError(QBluetoothServiceDiscoveryAgent::Error errorCode) |
| { |
| Q_ASSERT_X(osx_d_ptr->agent.data(), Q_FUNC_INFO, |
| "invalid service discovery agent (null)"); |
| |
| if (errorCode == QBluetoothServiceDiscoveryAgent::PoweredOffError) { |
| // There's nothing else we can do. |
| osx_d_ptr->setReplyError(UnknownError, |
| QCoreApplication::translate(DEV_DISCOVERY, DD_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, |
| QCoreApplication::translate(TRANSFER_REPLY, TR_INVAL_TARGET)); |
| } |
| } |
| |
| QT_END_NAMESPACE |