diff options
-rw-r--r-- | src/bluetooth/qbluetoothsocket_android.cpp | 294 | ||||
-rw-r--r-- | src/bluetooth/qbluetoothsocket_p.h | 23 |
2 files changed, 244 insertions, 73 deletions
diff --git a/src/bluetooth/qbluetoothsocket_android.cpp b/src/bluetooth/qbluetoothsocket_android.cpp index 73968e05..7170a301 100644 --- a/src/bluetooth/qbluetoothsocket_android.cpp +++ b/src/bluetooth/qbluetoothsocket_android.cpp @@ -36,9 +36,9 @@ #include "qbluetoothsocket_p.h" #include "qbluetoothaddress.h" #include <QtCore/QLoggingCategory> +#include <QtCore/QThread> #include <QtCore/QTime> #include <QtCore/private/qjni_p.h> -#include <QtConcurrent/QtConcurrentRun> #include <QtAndroidExtras/QAndroidJniEnvironment> @@ -46,6 +46,127 @@ QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_BT_ANDROID) +#define FALLBACK_CHANNEL 1 +#define USE_FALLBACK true + +Q_DECLARE_METATYPE(QAndroidJniObject) + + +/* BluetoothSocket.connect() can block up to 10s. Therefore it must be + * in a separate thread. Unfortunately if BluetoothSocket.close() is + * called while connect() is still blocking the resulting behavior is not reliable. + * This may well be an Android platform bug. In any case, close() must + * be queued up until connect() has returned. + * + * The WorkerThread manages the connect() and close() calls. Interaction + * with the main thread happens via signals and slots. There is an accepted but + * undesirable side effect of this approach as the user may call connect() + * and close() and the socket would continue to successfully connect to + * the remote device just to immidiately close the physical connection again. + * + * WorkerThread and SocketConnectWorker are cleaned up via the threads + * finished() signal. + */ + +class SocketConnectWorker : public QObject +{ + Q_OBJECT +public: + SocketConnectWorker(const QAndroidJniObject& socket, + const QAndroidJniObject& targetUuid) + : QObject(), + mSocketObject(socket), + mTargetUuid(targetUuid) + { + static int t = qRegisterMetaType<QAndroidJniObject>(); + Q_UNUSED(t); + } + +signals: + void socketConnectDone(const QAndroidJniObject &socket); + void socketConnectFailed(const QAndroidJniObject &socket, + const QAndroidJniObject &targetUuid); +public slots: + void connectSocket() + { + QAndroidJniEnvironment env; + + qCDebug(QT_BT_ANDROID) << "Connecting socket"; + mSocketObject.callMethod<void>("connect"); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + + emit socketConnectFailed(mSocketObject, mTargetUuid); + QThread::currentThread()->quit(); + return; + } + + qCDebug(QT_BT_ANDROID) << "Socket connection established"; + emit socketConnectDone(mSocketObject); + } + + void closeSocket() + { + qCDebug(QT_BT_ANDROID) << "Executing queued closeSocket()"; + + QAndroidJniEnvironment env; + mSocketObject.callMethod<void>("close"); + if (env->ExceptionCheck()) { + + qCWarning(QT_BT_ANDROID) << "Error during closure of abandoned socket"; + env->ExceptionDescribe(); + env->ExceptionClear(); + } + + QThread::currentThread()->quit(); + } + +private: + QAndroidJniObject mSocketObject; + QAndroidJniObject mTargetUuid; +}; + +class WorkerThread: public QThread +{ + Q_OBJECT +public: + WorkerThread() + : QThread(), workerPointer(0) + { + } + + // Runs in same thread as QBluetoothSocketPrivate + void setupWorker(QBluetoothSocketPrivate* d_ptr, const QAndroidJniObject& socketObject, + const QAndroidJniObject& uuidObject, bool useFallback) + { + SocketConnectWorker* worker = new SocketConnectWorker( + socketObject, uuidObject); + worker->moveToThread(this); + + connect(this, &QThread::finished, worker, &QObject::deleteLater); + connect(this, &QThread::finished, this, &QObject::deleteLater); + connect(d_ptr, &QBluetoothSocketPrivate::connectJavaSocket, + worker, &SocketConnectWorker::connectSocket); + connect(d_ptr, &QBluetoothSocketPrivate::closeJavaSocket, + worker, &SocketConnectWorker::closeSocket); + connect(worker, &SocketConnectWorker::socketConnectDone, + d_ptr, &QBluetoothSocketPrivate::socketConnectSuccess); + if (useFallback) { + connect(worker, &SocketConnectWorker::socketConnectFailed, + d_ptr, &QBluetoothSocketPrivate::fallbackSocketConnectFailed); + } else { + connect(worker, &SocketConnectWorker::socketConnectFailed, + d_ptr, &QBluetoothSocketPrivate::defaultSocketConnectFailed); + } + + workerPointer = worker; + } + +private: + QPointer<SocketConnectWorker> workerPointer; +}; + QBluetoothSocketPrivate::QBluetoothSocketPrivate() : socket(-1), socketType(QBluetoothServiceInfo::UnknownProtocol), @@ -75,18 +196,6 @@ bool QBluetoothSocketPrivate::ensureNativeSocket(QBluetoothServiceInfo::Protocol return false; } -void QBluetoothSocketPrivate::connectToService(const QBluetoothAddress &address, - const QBluetoothUuid &uuid, - QIODevice::OpenMode openMode, - int fallbackServiceChannel) -{ - Q_Q(QBluetoothSocket); - - q->setSocketState(QBluetoothSocket::ConnectingState); - QtConcurrent::run(this, &QBluetoothSocketPrivate::connectToServiceConc, - address, uuid, openMode, fallbackServiceChannel); -} - bool QBluetoothSocketPrivate::fallBackConnect(QAndroidJniObject uuid, int channel) { qCWarning(QT_BT_ANDROID) << "Falling back to workaround."; @@ -182,26 +291,42 @@ bool QBluetoothSocketPrivate::fallBackConnect(QAndroidJniObject uuid, int channe } socketObject = QAndroidJniObject(invokeResult); - socketObject.callMethod<void>("connect"); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->ExceptionClear(); - qCWarning(QT_BT_ANDROID) << "Socket connect via workaround failed."; - return false; - } + WorkerThread *workerThread = new WorkerThread(); + workerThread->setupWorker(this, socketObject, uuid, USE_FALLBACK); + workerThread->start(); + emit connectJavaSocket(); - qCWarning(QT_BT_ANDROID) << "Workaround invoked."; + qCWarning(QT_BT_ANDROID) << "Workaround thread invoked."; return true; } -void QBluetoothSocketPrivate::connectToServiceConc(const QBluetoothAddress &address, - const QBluetoothUuid &uuid, QIODevice::OpenMode openMode, int fallbackServiceChannel) + +/* + * The call order during a connectToService() is as follows: + * + * 1. call connectToService() + * 2. wait for execution of SocketConnectThread::run() + * 3. if threaded connect succeeds call socketConnectSuccess() via signals + * -> done + * 4. if threaded connect fails call defaultSocketConnectFailed() via signals + * 5. call fallBackConnect() + * 6. if threaded connect on fallback channel succeeds call socketConnectSuccess() + * via signals + * -> done + * 7. if threaded connect on fallback channel fails call fallbackSocketConnectFailed() + * -> complete failure of entire connectToService() + * */ +void QBluetoothSocketPrivate::connectToService(const QBluetoothAddress &address, + const QBluetoothUuid &uuid, + QIODevice::OpenMode openMode) { Q_Q(QBluetoothSocket); Q_UNUSED(openMode); - qCDebug(QT_BT_ANDROID) << "connectToServiceConc()" << address.toString() << uuid.toString(); + qCDebug(QT_BT_ANDROID) << "connectToService()" << address.toString() << uuid.toString(); + + q->setSocketState(QBluetoothSocket::ConnectingState); if (!adapter.isValid()) { qCWarning(QT_BT_ANDROID) << "Device does not support Bluetooth"; @@ -261,22 +386,21 @@ void QBluetoothSocketPrivate::connectToServiceConc(const QBluetoothAddress &addr return; } - socketObject.callMethod<void>("connect"); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->ExceptionClear(); + WorkerThread *workerThread = new WorkerThread(); + workerThread->setupWorker(this, socketObject, uuidObject, !USE_FALLBACK); + workerThread->start(); + emit connectJavaSocket(); +} - bool success = fallBackConnect(uuidObject, fallbackServiceChannel); - if (!success) { - errorString = QBluetoothSocket::tr("Connection to service failed"); - socketObject = remoteDevice = QAndroidJniObject(); - q->setSocketError(QBluetoothSocket::ServiceNotFoundError); - q->setSocketState(QBluetoothSocket::UnconnectedState); +void QBluetoothSocketPrivate::socketConnectSuccess(const QAndroidJniObject &socket) +{ + Q_Q(QBluetoothSocket); + QAndroidJniEnvironment env; - env->ExceptionClear(); //just in case - return; - } - } + // test we didn't get a success from a previous connect + // which was cleaned up late + if (socket != socketObject) + return; if (inputThread) { inputThread->deleteLater(); @@ -290,13 +414,7 @@ void QBluetoothSocketPrivate::connectToServiceConc(const QBluetoothAddress &addr env->ExceptionDescribe(); env->ExceptionClear(); - //close socket again - socketObject.callMethod<void>("close"); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->ExceptionClear(); - } - + emit closeJavaSocket(); socketObject = inputStream = outputStream = remoteDevice = QAndroidJniObject(); @@ -314,11 +432,7 @@ void QBluetoothSocketPrivate::connectToServiceConc(const QBluetoothAddress &addr if (!inputThread->run()) { //close socket again - socketObject.callMethod<void>("close"); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->ExceptionClear(); - } + emit closeJavaSocket(); socketObject = inputStream = outputStream = remoteDevice = QAndroidJniObject(); @@ -338,6 +452,48 @@ void QBluetoothSocketPrivate::connectToServiceConc(const QBluetoothAddress &addr emit q->connected(); } +void QBluetoothSocketPrivate::defaultSocketConnectFailed( + const QAndroidJniObject &socket, const QAndroidJniObject &targetUuid) +{ + Q_Q(QBluetoothSocket); + + // test we didn't get a fail from a previous connect + // which was cleaned up late - should be same socket + if (socket != socketObject) + return; + + bool success = fallBackConnect(targetUuid, FALLBACK_CHANNEL); + if (!success) { + errorString = QBluetoothSocket::tr("Connection to service failed"); + socketObject = remoteDevice = QAndroidJniObject(); + q->setSocketError(QBluetoothSocket::ServiceNotFoundError); + q->setSocketState(QBluetoothSocket::UnconnectedState); + + QAndroidJniEnvironment env; + env->ExceptionClear(); // just in case + qCWarning(QT_BT_ANDROID) << "Workaround failed"; + } +} + +void QBluetoothSocketPrivate::fallbackSocketConnectFailed( + const QAndroidJniObject &socket, const QAndroidJniObject &targetUuid) +{ + Q_UNUSED(targetUuid); + Q_Q(QBluetoothSocket); + + // test we didn't get a fail from a previous connect + // which was cleaned up late - should be same socket + if (socket != socketObject) + return; + + qCWarning(QT_BT_ANDROID) << "Socket connect via workaround failed."; + errorString = QBluetoothSocket::tr("Connection to service failed"); + socketObject = remoteDevice = QAndroidJniObject(); + + q->setSocketError(QBluetoothSocket::ServiceNotFoundError); + q->setSocketState(QBluetoothSocket::UnconnectedState); +} + void QBluetoothSocketPrivate::abort() { if (state == QBluetoothSocket::UnconnectedState) @@ -357,23 +513,26 @@ void QBluetoothSocketPrivate::abort() if (inputThread) inputThread->prepareForClosure(); - //triggers abort of input thread as well - socketObject.callMethod<void>("close"); - if (env->ExceptionCheck()) { + emit closeJavaSocket(); - qCWarning(QT_BT_ANDROID) << "Error during closure of socket"; - env->ExceptionDescribe(); - env->ExceptionClear(); - } + inputStream = outputStream = socketObject = remoteDevice = QAndroidJniObject(); if (inputThread) { + // inputThread exists hence we had a successful connect + // which means inputThread is responsible for setting Unconnected + //don't delete here as signals caused by Java Thread are still //going to be emitted //delete occurs in inputThreadError() inputThread = 0; + } else { + // inputThread doesn't exist hence + // we abort in the middle of connect(). WorkerThread will do + // close() without further feedback. Therefore we have to set + // Unconnected (now) in advance + Q_Q(QBluetoothSocket); + q->setSocketState(QBluetoothSocket::UnconnectedState); } - - inputStream = outputStream = socketObject = remoteDevice = QAndroidJniObject(); } } @@ -487,12 +646,7 @@ void QBluetoothSocketPrivate::inputThreadError(int errorCode) //cleanup internal objects //if it was call to local close()/abort() the objects are cleaned up already - QAndroidJniEnvironment env; - socketObject.callMethod<void>("close"); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->ExceptionClear(); - } + emit closeJavaSocket(); inputStream = outputStream = remoteDevice = socketObject = QAndroidJniObject(); } @@ -576,6 +730,14 @@ bool QBluetoothSocketPrivate::setSocketDescriptor(const QAndroidJniObject &socke q->setSocketState(socketState); q->setOpenMode(openMode | QIODevice::Unbuffered); + // WorkerThread manages all sockets for us + // When we come through here the socket was already connected by + // server socket listener (see QBluetoothServer) + // Therefore we only use WorkerThread to potentially close it later on + WorkerThread *workerThread = new WorkerThread(); + workerThread->setupWorker(this, socketObject, QAndroidJniObject(), !USE_FALLBACK); + workerThread->start(); + if (openMode == QBluetoothSocket::ConnectedState) emit q->connected(); @@ -592,3 +754,5 @@ qint64 QBluetoothSocketPrivate::bytesAvailable() const } QT_END_NAMESPACE + +#include <qbluetoothsocket_android.moc> diff --git a/src/bluetooth/qbluetoothsocket_p.h b/src/bluetooth/qbluetoothsocket_p.h index e95af465..dc490f26 100644 --- a/src/bluetooth/qbluetoothsocket_p.h +++ b/src/bluetooth/qbluetoothsocket_p.h @@ -52,8 +52,10 @@ #endif #ifdef QT_ANDROID_BLUETOOTH #include <QtAndroidExtras/QAndroidJniObject> +#include <QtCore/QPointer> #include "android/inputstreamthread_p.h" #include <jni.h> +class WorkerThread; #endif #ifndef QPRIVATELINEARBUFFER_BUFFERSIZE @@ -93,18 +95,14 @@ public: ~QBluetoothSocketPrivate(); //On QNX and Android we connect using the uuid not the port -#if defined(QT_QNX_BLUETOOTH) - void connectToService(const QBluetoothAddress &address, const QBluetoothUuid &uuid, QIODevice::OpenMode openMode); -#elif defined(QT_ANDROID_BLUETOOTH) +#if defined(QT_QNX_BLUETOOTH) || defined(QT_ANDROID_BLUETOOTH) void connectToService(const QBluetoothAddress &address, const QBluetoothUuid &uuid, - QIODevice::OpenMode openMode, int fallbackServiceChannel = 1); - bool fallBackConnect(QAndroidJniObject uuid, int channel); + QIODevice::OpenMode openMode); #else void connectToService(const QBluetoothAddress &address, quint16 port, QIODevice::OpenMode openMode); #endif #ifdef QT_ANDROID_BLUETOOTH - void connectToServiceConc(const QBluetoothAddress &address, const QBluetoothUuid &uuid, - QIODevice::OpenMode openMode, int fallbackServiceChannel = 1); + bool fallBackConnect(QAndroidJniObject uuid, int channel); #endif @@ -166,9 +164,18 @@ public: QAndroidJniObject outputStream; InputStreamThread *inputThread; -private slots: +public slots: + void socketConnectSuccess(const QAndroidJniObject &socket); + void defaultSocketConnectFailed(const QAndroidJniObject & socket, + const QAndroidJniObject &targetUuid); + void fallbackSocketConnectFailed(const QAndroidJniObject &socket, + const QAndroidJniObject &targetUuid); void inputThreadError(int errorCode); +signals: + void connectJavaSocket(); + void closeJavaSocket(); + #endif #if defined(QT_QNX_BLUETOOTH) || defined(QT_BLUEZ_BLUETOOTH) |