Revision control

++ // 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 <QCoreApplication>
++ #include <QtCore/qcoreapplication.h>
++ #include <QtCore/QLoggingCategory>
++ #include <QtCore/QTimer>
++ #include <QtCore/QJniEnvironment>
++ #include <QtBluetooth/QBluetoothHostInfo>
++ #include <QtBluetooth/QBluetoothLocalDevice>
++ #include <QtBluetooth/QBluetoothServiceDiscoveryAgent>
++
++ 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<QBluetoothHostInfo> 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<QList<QBluetoothUuid> >();
++ }
++
++ 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<QtJniTypes::BluetoothDevice>("getRemoteDevice",
++ inputString.object<jstring>());
++ 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()
++ const QJniArray parcelUuidArray =
++ remoteDevice.callMethod<QtJniTypes::ParcelUuid[]>("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<QBluetoothUuid> 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<QBluetoothUuid>& 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<jboolean>("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<QBluetoothUuid> &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<QBluetoothDeviceInfo,QList<QBluetoothUuid> > 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<QBluetoothDeviceInfo,QList<QBluetoothUuid> > 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<QBluetoothUuid> &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<qsizetype> 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<QBluetoothDeviceInfo,QList<QBluetoothUuid> > pair;
++ const QList<QBluetoothAddress> 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