// 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 "qbluetoothservicediscoveryagent_p.h" #include "qbluetoothsocket_android_p.h" #include "android/servicediscoverybroadcastreceiver_p.h" #include "android/localdevicebroadcastreceiver_p.h" #include "android/androidutils_p.h" #include "android/jni_android_p.h" #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(QT_BT_ANDROID) static constexpr auto uuidFetchTimeLimit = std::chrono::seconds{4}; QBluetoothServiceDiscoveryAgentPrivate::QBluetoothServiceDiscoveryAgentPrivate( QBluetoothServiceDiscoveryAgent *qp, const QBluetoothAddress &deviceAdapter) : error(QBluetoothServiceDiscoveryAgent::NoError), m_deviceAdapterAddress(deviceAdapter), state(Inactive), mode(QBluetoothServiceDiscoveryAgent::MinimalDiscovery), singleDevice(false), q_ptr(qp) { // If a specific adapter address is requested we need to check it matches // the current local adapter. If it does not match we emit // InvalidBluetoothAdapterError when calling start() bool createAdapter = true; if (!deviceAdapter.isNull()) { const QList devices = QBluetoothLocalDevice::allDevices(); if (devices.isEmpty()) { createAdapter = false; } else { auto match = [deviceAdapter](const QBluetoothHostInfo& info) { return info.address() == deviceAdapter; }; auto result = std::find_if(devices.begin(), devices.end(), match); if (result == devices.end()) createAdapter = false; } } /* We assume that the current local adapter has been passed. The logic below must change once there is more than one adapter. */ if (createAdapter) btAdapter = getDefaultBluetoothAdapter(); if (!btAdapter.isValid()) qCWarning(QT_BT_ANDROID) << "Platform does not support Bluetooth"; qRegisterMetaType >(); } QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate() { if (receiver) { receiver->unregisterReceiver(); delete receiver; } if (localDeviceReceiver) { localDeviceReceiver->unregisterReceiver(); delete localDeviceReceiver; } } void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &address) { Q_Q(QBluetoothServiceDiscoveryAgent); if (!ensureAndroidPermission(QBluetoothPermission::Access)) { qCWarning(QT_BT_ANDROID) << "Service discovery start() failed due to missing permissions"; error = QBluetoothServiceDiscoveryAgent::MissingPermissionsError; errorString = QBluetoothServiceDiscoveryAgent::tr( "Failed to start service discovery due to missing permissions."); emit q->errorOccurred(error); _q_serviceDiscoveryFinished(); return; } if (!btAdapter.isValid()) { if (m_deviceAdapterAddress.isNull()) { error = QBluetoothServiceDiscoveryAgent::UnknownError; errorString = QBluetoothServiceDiscoveryAgent::tr("Platform does not support Bluetooth"); } else { // specific adapter was requested which does not match the locally // existing adapter error = QBluetoothServiceDiscoveryAgent::InvalidBluetoothAdapterError; errorString = QBluetoothServiceDiscoveryAgent::tr("Invalid Bluetooth adapter address"); } //abort any outstanding discoveries discoveredDevices.clear(); emit q->errorOccurred(error); _q_serviceDiscoveryFinished(); return; } QJniObject inputString = QJniObject::fromString(address.toString()); QJniObject remoteDevice = btAdapter.callMethod("getRemoteDevice", inputString.object()); if (!remoteDevice.isValid()) { //if it was only device then its error -> otherwise go to next device if (singleDevice) { error = QBluetoothServiceDiscoveryAgent::InputOutputError; errorString = QBluetoothServiceDiscoveryAgent::tr("Cannot create Android BluetoothDevice"); qCWarning(QT_BT_ANDROID) << "Cannot start SDP for" << discoveredDevices.at(0).name() << "(" << address.toString() << ")"; emit q->errorOccurred(error); } _q_serviceDiscoveryFinished(); return; } if (mode == QBluetoothServiceDiscoveryAgent::MinimalDiscovery) { qCDebug(QT_BT_ANDROID) << "Minimal discovery on (" << discoveredDevices.at(0).name() << ")" << address.toString() ; //Minimal discovery uses BluetoothDevice.getUuids() QJniObject parcelUuidArray = remoteDevice.callMethod("getUuids"); if (!parcelUuidArray.isValid()) { if (singleDevice) { error = QBluetoothServiceDiscoveryAgent::InputOutputError; errorString = QBluetoothServiceDiscoveryAgent::tr("Cannot obtain service uuids"); emit q->errorOccurred(error); } qCWarning(QT_BT_ANDROID) << "Cannot retrieve SDP UUIDs for" << discoveredDevices.at(0).name() << "(" << address.toString() << ")"; _q_serviceDiscoveryFinished(); return; } const QList results = ServiceDiscoveryBroadcastReceiver::convertParcelableArray(parcelUuidArray); populateDiscoveredServices(discoveredDevices.at(0), results); _q_serviceDiscoveryFinished(); } else { qCDebug(QT_BT_ANDROID) << "Full discovery on (" << discoveredDevices.at(0).name() << ")" << address.toString(); //Full discovery uses BluetoothDevice.fetchUuidsWithSdp() if (!receiver) { receiver = new ServiceDiscoveryBroadcastReceiver(); QObject::connect(receiver, &ServiceDiscoveryBroadcastReceiver::uuidFetchFinished, q, [this](const QBluetoothAddress &address, const QList& uuids) { this->_q_processFetchedUuids(address, uuids); }); } if (!localDeviceReceiver) { localDeviceReceiver = new LocalDeviceBroadcastReceiver(); QObject::connect(localDeviceReceiver, &LocalDeviceBroadcastReceiver::hostModeStateChanged, q, [this](QBluetoothLocalDevice::HostMode state){ this->_q_hostModeStateChanged(state); }); } jboolean result = remoteDevice.callMethod("fetchUuidsWithSdp"); if (!result) { //kill receiver to limit load of signals receiver->unregisterReceiver(); receiver->deleteLater(); receiver = nullptr; qCWarning(QT_BT_ANDROID) << "Cannot start dynamic fetch."; _q_serviceDiscoveryFinished(); } } } void QBluetoothServiceDiscoveryAgentPrivate::stop() { sdpCache.clear(); discoveredDevices.clear(); //kill receiver to limit load of signals if (receiver) { receiver->unregisterReceiver(); receiver->deleteLater(); receiver = nullptr; } Q_Q(QBluetoothServiceDiscoveryAgent); emit q->canceled(); } void QBluetoothServiceDiscoveryAgentPrivate::_q_processFetchedUuids( const QBluetoothAddress &address, const QList &uuids) { //don't leave more data through if we are not interested anymore if (discoveredDevices.isEmpty()) return; //could not find any service for the current address/device -> go to next one if (address.isNull() || uuids.isEmpty()) { if (discoveredDevices.size() == 1) { Q_Q(QBluetoothServiceDiscoveryAgent); QTimer::singleShot(uuidFetchTimeLimit, q, [this]() { this->_q_fetchUuidsTimeout(); }); // will also call _q_serviceDiscoveryFinished() } else { _q_serviceDiscoveryFinished(); } return; } if (QT_BT_ANDROID().isDebugEnabled()) { qCDebug(QT_BT_ANDROID) << "Found UUID for" << address.toString() << "\ncount: " << uuids.size(); QString result; for (const QBluetoothUuid &uuid : uuids) result += uuid.toString() + QLatin1String("**"); qCDebug(QT_BT_ANDROID) << result; } /* In general there may be up-to two uuid events per device. * We'll wait for the second event to arrive before we process the UUIDs. * We utilize a timeout to catch cases when the second * event doesn't arrive at all. * Generally we assume that the second uuid event carries the most up-to-date * set of uuids and discard the first events results. */ if (sdpCache.contains(address)) { //second event QPair > pair = sdpCache.take(address); //prefer second uuid set over first populateDiscoveredServices(pair.first, uuids); if (discoveredDevices.size() == 1 && sdpCache.isEmpty()) { //last regular uuid data set from OS -> we finish here _q_serviceDiscoveryFinished(); } } else { //first event QPair > pair; pair.first = discoveredDevices.at(0); pair.second = uuids; if (pair.first.address() != address) return; sdpCache.insert(address, pair); //the discovery on the last device cannot immediately finish //we have to grant the timeout delay to allow potential second event to arrive if (discoveredDevices.size() == 1) { Q_Q(QBluetoothServiceDiscoveryAgent); QTimer::singleShot(uuidFetchTimeLimit, q, [this]() { this->_q_fetchUuidsTimeout(); }); return; } _q_serviceDiscoveryFinished(); } } void QBluetoothServiceDiscoveryAgentPrivate::populateDiscoveredServices(const QBluetoothDeviceInfo &remoteDevice, const QList &uuids) { /* Android doesn't provide decent SDP data. A flat list of uuids is all we get. * * The following approach is chosen: * - If we see an SPP service class and we see * one or more custom uuids we match them up. Such services will always * be SPP services. There is the chance that a custom uuid is eronously * mapped as being an SPP service. In addition, the SPP uuid will be mapped as * standalone SPP service. * - If we see a custom uuid but no SPP uuid then we return * BluetoothServiceInfo instance with just a serviceUuid (no service class set) * - If we don't find any custom uuid but the SPP uuid, we return a * BluetoothServiceInfo instance where classId and serviceUuid() are set to SPP. * - Any other service uuid will stand on its own. * */ Q_Q(QBluetoothServiceDiscoveryAgent); //find SPP and custom uuid bool haveSppClass = false; QVarLengthArray customUuids; for (qsizetype i = 0; i < uuids.size(); ++i) { const QBluetoothUuid uuid = uuids.at(i); if (uuid.isNull()) continue; //check for SPP protocol haveSppClass |= uuid == QBluetoothUuid::ServiceClassUuid::SerialPort; //check for custom uuid if (uuid.minimumSize() == 16) customUuids.append(i); } auto rfcommProtocolDescriptorList = []() -> QBluetoothServiceInfo::Sequence { QBluetoothServiceInfo::Sequence protocol; protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ProtocolUuid::Rfcomm)) << QVariant::fromValue(0); return protocol; }; auto sppProfileDescriptorList = []() -> QBluetoothServiceInfo::Sequence { QBluetoothServiceInfo::Sequence profileSequence; QBluetoothServiceInfo::Sequence classId; classId << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::SerialPort)); classId << QVariant::fromValue(quint16(0x100)); profileSequence.append(QVariant::fromValue(classId)); return profileSequence; }; for (qsizetype i = 0; i < uuids.size(); ++i) { const QBluetoothUuid &uuid = uuids.at(i); if (uuid.isNull()) continue; QBluetoothServiceInfo serviceInfo; serviceInfo.setDevice(remoteDevice); QBluetoothServiceInfo::Sequence protocolDescriptorList; { QBluetoothServiceInfo::Sequence protocol; protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ProtocolUuid::L2cap)); protocolDescriptorList.append(QVariant::fromValue(protocol)); } if (customUuids.contains(i) && haveSppClass) { //we have a custom uuid of service class type SPP //set rfcomm protocol protocolDescriptorList.append(QVariant::fromValue(rfcommProtocolDescriptorList())); //set SPP profile descriptor list serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescriptorList, sppProfileDescriptorList()); QBluetoothServiceInfo::Sequence classId; //set SPP service class uuid classId << QVariant::fromValue(uuid); classId << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::SerialPort)); serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId); serviceInfo.setServiceName(QBluetoothServiceDiscoveryAgent::tr("Serial Port Profile")); serviceInfo.setServiceUuid(uuid); } else if (uuid == QBluetoothUuid{QBluetoothUuid::ServiceClassUuid::SerialPort}) { //set rfcomm protocol protocolDescriptorList.append(QVariant::fromValue(rfcommProtocolDescriptorList())); //set SPP profile descriptor list serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescriptorList, sppProfileDescriptorList()); //also we need to set the custom uuid to the SPP uuid //otherwise QBluetoothSocket::connectToService() would fail due to a missing service uuid serviceInfo.setServiceUuid(uuid); } else if (customUuids.contains(i)) { //custom uuid but no serial port serviceInfo.setServiceUuid(uuid); } serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList, protocolDescriptorList); QBluetoothServiceInfo::Sequence publicBrowse; publicBrowse << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::PublicBrowseGroup)); serviceInfo.setAttribute(QBluetoothServiceInfo::BrowseGroupList, publicBrowse); if (!customUuids.contains(i)) { //if we don't have custom uuid use it as class id as well QBluetoothServiceInfo::Sequence classId; classId << QVariant::fromValue(uuid); serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId); auto clsId = QBluetoothUuid::ServiceClassUuid(uuid.toUInt16()); serviceInfo.setServiceName(QBluetoothUuid::serviceClassToString(clsId)); } //Check if the service is in the uuidFilter if (!uuidFilter.isEmpty()) { bool match = uuidFilter.contains(serviceInfo.serviceUuid()); match |= uuidFilter.contains(QBluetoothSocketPrivateAndroid::reverseUuid(serviceInfo.serviceUuid())); for (const auto &uuid : std::as_const(uuidFilter)) { match |= serviceInfo.serviceClassUuids().contains(uuid); match |= serviceInfo.serviceClassUuids().contains(QBluetoothSocketPrivateAndroid::reverseUuid(uuid)); } if (!match) continue; } //don't include the service if we already discovered it before if (!isDuplicatedService(serviceInfo)) { discoveredServices << serviceInfo; // Use queued connection to allow us finish the service discovery reporting; // the application might call stop() when it has detected the service-of-interest, // which in turn can cause the use of already released resources QMetaObject::invokeMethod(q, "serviceDiscovered", Qt::QueuedConnection, Q_ARG(QBluetoothServiceInfo, serviceInfo)); } } } void QBluetoothServiceDiscoveryAgentPrivate::_q_fetchUuidsTimeout() { // In practice if device list is empty, discovery has been stopped or bluetooth is offline if (discoveredDevices.isEmpty()) return; // Process remaining services in the cache (these didn't get a second UUID event) if (!sdpCache.isEmpty()) { QPair > pair; const QList keys = sdpCache.keys(); for (const QBluetoothAddress &key : keys) { pair = sdpCache.take(key); populateDiscoveredServices(pair.first, pair.second); } } Q_ASSERT(sdpCache.isEmpty()); //kill receiver to limit load of signals if (receiver) { receiver->unregisterReceiver(); receiver->deleteLater(); receiver = nullptr; } _q_serviceDiscoveryFinished(); } void QBluetoothServiceDiscoveryAgentPrivate::_q_hostModeStateChanged(QBluetoothLocalDevice::HostMode state) { if (discoveryState() == QBluetoothServiceDiscoveryAgentPrivate::ServiceDiscovery && state == QBluetoothLocalDevice::HostPoweredOff ) { discoveredDevices.clear(); sdpCache.clear(); error = QBluetoothServiceDiscoveryAgent::PoweredOffError; errorString = QBluetoothServiceDiscoveryAgent::tr("Device is powered off"); //kill receiver to limit load of signals if (receiver) { receiver->unregisterReceiver(); receiver->deleteLater(); receiver = nullptr; } Q_Q(QBluetoothServiceDiscoveryAgent); emit q->errorOccurred(error); _q_serviceDiscoveryFinished(); } } QT_END_NAMESPACE