// Copyright (C) 2016 Lauri Laanmets (Proekspert AS) // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qbluetoothlocaldevice_p.h" #include "android/localdevicebroadcastreceiver_p.h" #include "android/androidutils_p.h" #include "android/jni_android_p.h" #include #include #include #include #include #include QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_BT_ANDROID) QBluetoothLocalDevicePrivate::QBluetoothLocalDevicePrivate( QBluetoothLocalDevice *q, const QBluetoothAddress &address) : q_ptr(q) { registerQBluetoothLocalDeviceMetaType(); initialize(address); receiver = new LocalDeviceBroadcastReceiver(q_ptr); connect(receiver, &LocalDeviceBroadcastReceiver::hostModeStateChanged, this, &QBluetoothLocalDevicePrivate::processHostModeChange); connect(receiver, &LocalDeviceBroadcastReceiver::pairingStateChanged, this, &QBluetoothLocalDevicePrivate::processPairingStateChanged); connect(receiver, &LocalDeviceBroadcastReceiver::connectDeviceChanges, this, &QBluetoothLocalDevicePrivate::processConnectDeviceChanges); } QBluetoothLocalDevicePrivate::~QBluetoothLocalDevicePrivate() { receiver->unregisterReceiver(); delete receiver; delete obj; } QJniObject *QBluetoothLocalDevicePrivate::adapter() { return obj; } void QBluetoothLocalDevicePrivate::initialize(const QBluetoothAddress &address) { QJniObject adapter = getDefaultBluetoothAdapter(); if (!adapter.isValid()) { qCWarning(QT_BT_ANDROID) << "Device does not support Bluetooth"; return; } if (!ensureAndroidPermission(QBluetoothPermission::Access)) { qCWarning(QT_BT_ANDROID) << "Local device initialize() failed due to missing permissions"; return; } obj = new QJniObject(adapter); if (!address.isNull()) { const QString localAddress = obj->callMethod("getAddress").toString(); if (localAddress != address.toString()) { // passed address not local one -> invalid delete obj; obj = nullptr; } } } bool QBluetoothLocalDevicePrivate::isValid() const { return obj ? true : false; } void QBluetoothLocalDevicePrivate::processHostModeChange(QBluetoothLocalDevice::HostMode newMode) { qCDebug(QT_BT_ANDROID) << "Processing host mode change:" << newMode << ", pending transition:" << pendingConnectableHostModeTransition; if (!pendingConnectableHostModeTransition) { // If host mode is not in transition -> pass data on emit q_ptr->hostModeStateChanged(newMode); return; } // Host mode is in transition: check if the new mode is 'off' in which state // we can enter the targeted 'Connectable' state if (isValid() && newMode == QBluetoothLocalDevice::HostPoweredOff) { const bool success = (bool)QJniObject::callStaticMethod( QtJniTypes::className(), "setEnabled"); if (!success) { qCWarning(QT_BT_ANDROID) << "Transitioning Bluetooth from OFF to ON failed"; emit q_ptr->errorOccurred(QBluetoothLocalDevice::UnknownError); } } pendingConnectableHostModeTransition = false; } // Return -1 if address is not part of a pending pairing request // Otherwise it returns the index of address in pendingPairings int QBluetoothLocalDevicePrivate::pendingPairing(const QBluetoothAddress &address) { for (qsizetype i = 0; i < pendingPairings.size(); ++i) { if (pendingPairings.at(i).first == address) return i; } return -1; } void QBluetoothLocalDevicePrivate::processPairingStateChanged( const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing) { int index = pendingPairing(address); if (index < 0) return; // ignore unrelated pairing signals QPair entry = pendingPairings.takeAt(index); if ((entry.second && pairing == QBluetoothLocalDevice::Paired) || (!entry.second && pairing == QBluetoothLocalDevice::Unpaired)) { emit q_ptr->pairingFinished(address, pairing); } else { emit q_ptr->errorOccurred(QBluetoothLocalDevice::PairingError); } } void QBluetoothLocalDevicePrivate::processConnectDeviceChanges(const QBluetoothAddress &address, bool isConnectEvent) { if (isConnectEvent) { // connect event if (connectedDevices.contains(address)) return; connectedDevices.append(address); emit q_ptr->deviceConnected(address); } else { // disconnect event connectedDevices.removeAll(address); emit q_ptr->deviceDisconnected(address); } } QBluetoothLocalDevice::QBluetoothLocalDevice(QObject *parent) : QObject(parent), d_ptr(new QBluetoothLocalDevicePrivate(this, QBluetoothAddress())) { } QBluetoothLocalDevice::QBluetoothLocalDevice(const QBluetoothAddress &address, QObject *parent) : QObject(parent), d_ptr(new QBluetoothLocalDevicePrivate(this, address)) { } QString QBluetoothLocalDevice::name() const { if (d_ptr->adapter()) return d_ptr->adapter()->callMethod("getName").toString(); return QString(); } QBluetoothAddress QBluetoothLocalDevice::address() const { QString result; if (d_ptr->adapter()) result = d_ptr->adapter()->callMethod("getAddress").toString(); QBluetoothAddress address(result); return address; } void QBluetoothLocalDevice::powerOn() { if (hostMode() != HostPoweredOff) return; if (d_ptr->adapter()) { bool success(false); if (QNativeInterface::QAndroidApplication::sdkVersion() >= 31) { success = (bool)QJniObject::callStaticMethod( QtJniTypes::className(), "setEnabled"); } else { success = (bool)d_ptr->adapter()->callMethod("enable"); } if (!success) { qCWarning(QT_BT_ANDROID) << "Enabling bluetooth failed"; emit errorOccurred(QBluetoothLocalDevice::UnknownError); } } } void QBluetoothLocalDevice::setHostMode(QBluetoothLocalDevice::HostMode requestedMode) { QBluetoothLocalDevice::HostMode nextMode = requestedMode; if (requestedMode == HostDiscoverableLimitedInquiry) nextMode = HostDiscoverable; if (nextMode == hostMode()) return; switch (nextMode) { case QBluetoothLocalDevice::HostPoweredOff: { bool success = false; if (d_ptr->adapter()) { if (QNativeInterface::QAndroidApplication::sdkVersion() >= 31) { success = (bool)QJniObject::callStaticMethod( QtJniTypes::className(), "setDisabled"); } else { success = (bool)d_ptr->adapter()->callMethod("disable"); } } if (!success) { qCWarning(QT_BT_ANDROID) << "Unable to power off the adapter"; emit errorOccurred(QBluetoothLocalDevice::UnknownError); } break; } case QBluetoothLocalDevice::HostConnectable: { if (hostMode() == QBluetoothLocalDevice::HostDiscoverable) { // On Android 'Discoverable' is actually 'CONNECTABLE_DISCOVERABLE', and // it seems we cannot go directly from "Discoverable" to "Connectable". Instead // we need to go to disabled mode first and then to the 'Connectable' mode setHostMode(QBluetoothLocalDevice::HostPoweredOff); d_ptr->pendingConnectableHostModeTransition = true; } else { const bool success = (bool)QJniObject::callStaticMethod( QtJniTypes::className(), "setEnabled"); if (!success) { qCWarning(QT_BT_ANDROID) << "Unable to enable the Bluetooth"; emit errorOccurred(QBluetoothLocalDevice::UnknownError); } } break; } case QBluetoothLocalDevice::HostDiscoverable: { if (!ensureAndroidPermission(QBluetoothPermission::Advertise)) { qCWarning(QT_BT_ANDROID) << "Local device setHostMode() failed due to " "missing permissions"; emit errorOccurred(QBluetoothLocalDevice::MissingPermissionsError); return; } const bool success = (bool)QJniObject::callStaticMethod( QtJniTypes::className(), "setDiscoverable"); if (!success) { qCWarning(QT_BT_ANDROID) << "Unable to set Bluetooth as discoverable"; emit errorOccurred(QBluetoothLocalDevice::UnknownError); } break; } default: qCWarning(QT_BT_ANDROID) << "setHostMode() unsupported host mode:" << nextMode; break; } } QBluetoothLocalDevice::HostMode QBluetoothLocalDevice::hostMode() const { if (d_ptr->adapter()) { jint scanMode = d_ptr->adapter()->callMethod("getScanMode"); switch (scanMode) { case 20: // BluetoothAdapter.SCAN_MODE_NONE return HostPoweredOff; case 21: // BluetoothAdapter.SCAN_MODE_CONNECTABLE return HostConnectable; case 23: // BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE return HostDiscoverable; default: break; } } return HostPoweredOff; } QList QBluetoothLocalDevice::allDevices() { // As a static class function we need to ensure permissions here (in addition to initialize()) if (!ensureAndroidPermission(QBluetoothPermission::Access)) { qCWarning(QT_BT_ANDROID) << "Local device allDevices() failed due to " "missing permissions"; return {}; } // Android only supports max of one device (so far) QList localDevices; QJniObject o = getDefaultBluetoothAdapter(); if (o.isValid()) { QBluetoothHostInfo info; info.setName(o.callMethod("getName").toString()); info.setAddress(QBluetoothAddress(o.callMethod("getAddress").toString())); localDevices.append(info); } return localDevices; } void QBluetoothLocalDevice::requestPairing(const QBluetoothAddress &address, Pairing pairing) { if (address.isNull()) { QMetaObject::invokeMethod(this, "errorOccurred", Qt::QueuedConnection, Q_ARG(QBluetoothLocalDevice::Error, QBluetoothLocalDevice::PairingError)); return; } const Pairing previousPairing = pairingStatus(address); Pairing newPairing = pairing; if (pairing == AuthorizedPaired) // AuthorizedPaired same as Paired on Android newPairing = Paired; if (previousPairing == newPairing) { QMetaObject::invokeMethod(this, "pairingFinished", Qt::QueuedConnection, Q_ARG(QBluetoothAddress, address), Q_ARG(QBluetoothLocalDevice::Pairing, newPairing)); return; } if (!d_ptr->adapter()) { qCWarning(QT_BT_ANDROID) << "Unable to pair, invalid adapter"; QMetaObject::invokeMethod(this, "errorOccurred", Qt::QueuedConnection, Q_ARG(QBluetoothLocalDevice::Error, QBluetoothLocalDevice::PairingError)); return; } QJniObject inputString = QJniObject::fromString(address.toString()); jboolean success = QJniObject::callStaticMethod( QtJniTypes::className(), "setPairingMode", inputString.object(), jboolean(newPairing == Paired ? JNI_TRUE : JNI_FALSE)); if (!success) { QMetaObject::invokeMethod(this, "errorOccurred", Qt::QueuedConnection, Q_ARG(QBluetoothLocalDevice::Error, QBluetoothLocalDevice::PairingError)); } else { d_ptr->pendingPairings.append(qMakePair(address, newPairing == Paired ? true : false)); } } QBluetoothLocalDevice::Pairing QBluetoothLocalDevice::pairingStatus( const QBluetoothAddress &address) const { if (address.isNull() || !d_ptr->adapter()) return Unpaired; QJniObject inputString = QJniObject::fromString(address.toString()); QJniObject remoteDevice = d_ptr->adapter()->callMethod("getRemoteDevice", inputString.object()); if (!remoteDevice.isValid()) return Unpaired; jint bondState = remoteDevice.callMethod("getBondState"); switch (bondState) { case 12: // BluetoothDevice.BOND_BONDED return Paired; default: break; } return Unpaired; } QList QBluetoothLocalDevice::connectedDevices() const { /* * Android does not have an API to list all connected devices. We have to collect * the information based on a few indicators. * * Primarily we detect connected devices by monitoring connect/disconnect signals. * Unfortunately the list may only be complete after very long monitoring time. * However there are some Android APIs which provide the list of connected devices * for specific Bluetooth profiles. QtBluetoothBroadcastReceiver.getConnectedDevices() * returns a few connections of common profiles. The returned list is not complete either * but at least it can complement our already detected connections. */ QJniObject connectedDevices = QJniObject::callStaticMethod( QtJniTypes::className(), "getConnectedDevices"); if (!connectedDevices.isValid()) return d_ptr->connectedDevices; jobjectArray connectedDevicesArray = connectedDevices.object(); if (!connectedDevicesArray) return d_ptr->connectedDevices; QJniEnvironment env; QList knownAddresses = d_ptr->connectedDevices; QJniObject p; jint size = env->GetArrayLength(connectedDevicesArray); for (int i = 0; i < size; i++) { p = env->GetObjectArrayElement(connectedDevicesArray, i); QBluetoothAddress address(p.toString()); if (!address.isNull() && !knownAddresses.contains(address)) knownAddresses.append(address); } return knownAddresses; } QT_END_NAMESPACE