| 1 | // Copyright (C) 2016 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include "qbluetoothservicediscoveryagent.h" |
| 5 | #include "qbluetoothservicediscoveryagent_p.h" |
| 6 | |
| 7 | #include "bluez/bluez5_helper_p.h" |
| 8 | #include "bluez/objectmanager_p.h" |
| 9 | #include "bluez/adapter1_bluez5_p.h" |
| 10 | |
| 11 | #include <QtCore/QFile> |
| 12 | #include <QtCore/QLibraryInfo> |
| 13 | #include <QtCore/QLoggingCategory> |
| 14 | #include <QtCore/QProcess> |
| 15 | #include <QtCore/QScopeGuard> |
| 16 | |
| 17 | #include <QtDBus/QDBusPendingCallWatcher> |
| 18 | |
| 19 | QT_BEGIN_NAMESPACE |
| 20 | |
| 21 | Q_DECLARE_LOGGING_CATEGORY(QT_BT_BLUEZ) |
| 22 | |
| 23 | QBluetoothServiceDiscoveryAgentPrivate::QBluetoothServiceDiscoveryAgentPrivate( |
| 24 | QBluetoothServiceDiscoveryAgent *qp, const QBluetoothAddress &deviceAdapter) |
| 25 | : error(QBluetoothServiceDiscoveryAgent::NoError), m_deviceAdapterAddress(deviceAdapter), state(Inactive), |
| 26 | mode(QBluetoothServiceDiscoveryAgent::MinimalDiscovery), singleDevice(false), |
| 27 | q_ptr(qp) |
| 28 | { |
| 29 | initializeBluez5(); |
| 30 | manager = new OrgFreedesktopDBusObjectManagerInterface( |
| 31 | QStringLiteral("org.bluez" ), QStringLiteral("/" ), QDBusConnection::systemBus()); |
| 32 | qRegisterMetaType<QBluetoothServiceDiscoveryAgent::Error>(); |
| 33 | } |
| 34 | |
| 35 | QBluetoothServiceDiscoveryAgentPrivate::~QBluetoothServiceDiscoveryAgentPrivate() |
| 36 | { |
| 37 | delete manager; |
| 38 | } |
| 39 | |
| 40 | void QBluetoothServiceDiscoveryAgentPrivate::start(const QBluetoothAddress &address) |
| 41 | { |
| 42 | Q_Q(QBluetoothServiceDiscoveryAgent); |
| 43 | |
| 44 | qCDebug(QT_BT_BLUEZ) << "Discovery on: " << address.toString() << "Mode:" << DiscoveryMode(); |
| 45 | |
| 46 | if (foundHostAdapterPath.isEmpty()) { |
| 47 | // check that we match adapter addresses or use first if it wasn't specified |
| 48 | |
| 49 | bool ok = false; |
| 50 | foundHostAdapterPath = findAdapterForAddress(wantedAddress: m_deviceAdapterAddress, ok: &ok); |
| 51 | if (!ok) { |
| 52 | discoveredDevices.clear(); |
| 53 | error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
| 54 | errorString = QBluetoothDeviceDiscoveryAgent::tr(s: "Cannot access adapter during service discovery" ); |
| 55 | emit q->errorOccurred(error); |
| 56 | _q_serviceDiscoveryFinished(); |
| 57 | return; |
| 58 | } |
| 59 | |
| 60 | if (foundHostAdapterPath.isEmpty()) { |
| 61 | // Cannot find a local adapter |
| 62 | // Abort any outstanding discoveries |
| 63 | discoveredDevices.clear(); |
| 64 | |
| 65 | error = QBluetoothServiceDiscoveryAgent::InvalidBluetoothAdapterError; |
| 66 | errorString = QBluetoothServiceDiscoveryAgent::tr(s: "Cannot find local Bluetooth adapter" ); |
| 67 | emit q->errorOccurred(error); |
| 68 | _q_serviceDiscoveryFinished(); |
| 69 | |
| 70 | return; |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | // ensure we didn't go offline yet |
| 75 | OrgBluezAdapter1Interface adapter(QStringLiteral("org.bluez" ), |
| 76 | foundHostAdapterPath, QDBusConnection::systemBus()); |
| 77 | if (!adapter.powered()) { |
| 78 | discoveredDevices.clear(); |
| 79 | |
| 80 | error = QBluetoothServiceDiscoveryAgent::PoweredOffError; |
| 81 | errorString = QBluetoothServiceDiscoveryAgent::tr(s: "Local device is powered off" ); |
| 82 | emit q->errorOccurred(error); |
| 83 | |
| 84 | _q_serviceDiscoveryFinished(); |
| 85 | return; |
| 86 | } |
| 87 | |
| 88 | if (DiscoveryMode() == QBluetoothServiceDiscoveryAgent::MinimalDiscovery) { |
| 89 | performMinimalServiceDiscovery(deviceAddress: address); |
| 90 | } else { |
| 91 | runExternalSdpScan(remoteAddress: address, localAddress: QBluetoothAddress(adapter.address())); |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | /* Bluez 5 |
| 96 | * src/tools/sdpscanner performs an SDP scan. This is |
| 97 | * done out-of-process to avoid license issues. At this stage Bluez uses GPLv2. |
| 98 | */ |
| 99 | void QBluetoothServiceDiscoveryAgentPrivate::runExternalSdpScan( |
| 100 | const QBluetoothAddress &remoteAddress, const QBluetoothAddress &localAddress) |
| 101 | { |
| 102 | Q_Q(QBluetoothServiceDiscoveryAgent); |
| 103 | |
| 104 | if (!sdpScannerProcess) { |
| 105 | const QString binPath = QLibraryInfo::path(p: QLibraryInfo::LibraryExecutablesPath); |
| 106 | QFileInfo fileInfo(binPath, QStringLiteral("sdpscanner" )); |
| 107 | if (!fileInfo.exists() || !fileInfo.isExecutable()) { |
| 108 | _q_finishSdpScan(errorCode: QBluetoothServiceDiscoveryAgent::InputOutputError, |
| 109 | errorDescription: QBluetoothServiceDiscoveryAgent::tr(s: "Unable to find sdpscanner" ), |
| 110 | xmlRecords: QStringList()); |
| 111 | qCWarning(QT_BT_BLUEZ) << "Cannot find sdpscanner:" |
| 112 | << fileInfo.canonicalFilePath(); |
| 113 | return; |
| 114 | } |
| 115 | |
| 116 | sdpScannerProcess = new QProcess(q); |
| 117 | sdpScannerProcess->setReadChannel(QProcess::StandardOutput); |
| 118 | if (QT_BT_BLUEZ().isDebugEnabled()) |
| 119 | sdpScannerProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel); |
| 120 | sdpScannerProcess->setProgram(fileInfo.canonicalFilePath()); |
| 121 | q->connect(sender: sdpScannerProcess, |
| 122 | signal: QOverload<int, QProcess::ExitStatus>::of(ptr: &QProcess::finished), |
| 123 | context: q, slot: [this](int exitCode, QProcess::ExitStatus status){ |
| 124 | this->_q_sdpScannerDone(exitCode, status); |
| 125 | }); |
| 126 | } |
| 127 | |
| 128 | QStringList arguments; |
| 129 | arguments << remoteAddress.toString() << localAddress.toString(); |
| 130 | |
| 131 | // No filter implies PUBLIC_BROWSE_GROUP based SDP scan |
| 132 | if (!uuidFilter.isEmpty()) { |
| 133 | arguments << QLatin1String("-u" ); // cmd line option for list of uuids |
| 134 | for (const QBluetoothUuid& uuid : std::as_const(t&: uuidFilter)) |
| 135 | arguments << uuid.toString(); |
| 136 | } |
| 137 | |
| 138 | sdpScannerProcess->setArguments(arguments); |
| 139 | sdpScannerProcess->start(); |
| 140 | } |
| 141 | |
| 142 | void QBluetoothServiceDiscoveryAgentPrivate::_q_sdpScannerDone(int exitCode, QProcess::ExitStatus status) |
| 143 | { |
| 144 | if (status != QProcess::NormalExit || exitCode != 0) { |
| 145 | qCWarning(QT_BT_BLUEZ) << "SDP scan failure" << status << exitCode; |
| 146 | if (singleDevice) { |
| 147 | _q_finishSdpScan(errorCode: QBluetoothServiceDiscoveryAgent::InputOutputError, |
| 148 | errorDescription: QBluetoothServiceDiscoveryAgent::tr(s: "Unable to perform SDP scan" ), |
| 149 | xmlRecords: QStringList()); |
| 150 | } else { |
| 151 | // go to next device |
| 152 | _q_finishSdpScan(errorCode: QBluetoothServiceDiscoveryAgent::NoError, errorDescription: QString(), xmlRecords: QStringList()); |
| 153 | } |
| 154 | return; |
| 155 | } |
| 156 | |
| 157 | QStringList xmlRecords; |
| 158 | const QByteArray utf8Data = QByteArray::fromBase64(base64: sdpScannerProcess->readAllStandardOutput()); |
| 159 | const QByteArrayView utf8View = utf8Data; |
| 160 | |
| 161 | // split the various xml docs up |
| 162 | constexpr auto matcher = qMakeStaticByteArrayMatcher(pattern: "<?xml" ); |
| 163 | qsizetype next; |
| 164 | qsizetype start = matcher.indexIn(haystack: utf8View, from: 0); |
| 165 | if (start != -1) { |
| 166 | do { |
| 167 | next = matcher.indexIn(haystack: utf8View, from: start + 1); |
| 168 | if (next != -1) |
| 169 | xmlRecords.append(t: QString::fromUtf8(utf8: utf8View.sliced(pos: start, n: next - start))); |
| 170 | else |
| 171 | xmlRecords.append(t: QString::fromUtf8(utf8: utf8View.sliced(pos: start))); |
| 172 | start = next; |
| 173 | } while ( start != -1); |
| 174 | } |
| 175 | |
| 176 | _q_finishSdpScan(errorCode: QBluetoothServiceDiscoveryAgent::NoError, errorDescription: QString(), xmlRecords); |
| 177 | } |
| 178 | |
| 179 | void QBluetoothServiceDiscoveryAgentPrivate::_q_finishSdpScan(QBluetoothServiceDiscoveryAgent::Error errorCode, |
| 180 | const QString &errorDescription, |
| 181 | const QStringList &xmlRecords) |
| 182 | { |
| 183 | Q_Q(QBluetoothServiceDiscoveryAgent); |
| 184 | |
| 185 | if (errorCode != QBluetoothServiceDiscoveryAgent::NoError) { |
| 186 | qCWarning(QT_BT_BLUEZ) << "SDP search failed for" |
| 187 | << (!discoveredDevices.isEmpty() |
| 188 | ? discoveredDevices.at(i: 0).address().toString() |
| 189 | : QStringLiteral("<Unknown>" )); |
| 190 | // We have an error which we need to indicate and stop further processing |
| 191 | discoveredDevices.clear(); |
| 192 | error = errorCode; |
| 193 | errorString = errorDescription; |
| 194 | emit q->errorOccurred(error); |
| 195 | } else if (!xmlRecords.isEmpty() && discoveryState() != Inactive) { |
| 196 | for (const QString &record : xmlRecords) { |
| 197 | QBluetoothServiceInfo serviceInfo = parseServiceXml(xml: record); |
| 198 | |
| 199 | //apply uuidFilter |
| 200 | if (!uuidFilter.isEmpty()) { |
| 201 | bool serviceNameMatched = uuidFilter.contains(t: serviceInfo.serviceUuid()); |
| 202 | bool serviceClassMatched = false; |
| 203 | const QList<QBluetoothUuid> serviceClassUuids |
| 204 | = serviceInfo.serviceClassUuids(); |
| 205 | for (const QBluetoothUuid &id : serviceClassUuids) { |
| 206 | if (uuidFilter.contains(t: id)) { |
| 207 | serviceClassMatched = true; |
| 208 | break; |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | if (!serviceNameMatched && !serviceClassMatched) |
| 213 | continue; |
| 214 | } |
| 215 | |
| 216 | if (!serviceInfo.isValid()) |
| 217 | continue; |
| 218 | |
| 219 | // Bluez sdpscanner declares custom uuids into the service class uuid list. |
| 220 | // Let's move a potential custom uuid from QBluetoothServiceInfo::serviceClassUuids() |
| 221 | // to QBluetoothServiceInfo::serviceUuid(). If there is more than one, just move the first uuid |
| 222 | const QList<QBluetoothUuid> serviceClassUuids = serviceInfo.serviceClassUuids(); |
| 223 | for (const QBluetoothUuid &id : serviceClassUuids) { |
| 224 | if (id.minimumSize() == 16) { |
| 225 | serviceInfo.setServiceUuid(id); |
| 226 | if (serviceInfo.serviceName().isEmpty()) { |
| 227 | serviceInfo.setServiceName( |
| 228 | QBluetoothServiceDiscoveryAgent::tr(s: "Custom Service" )); |
| 229 | } |
| 230 | QBluetoothServiceInfo::Sequence modSeq = |
| 231 | serviceInfo.attribute(attributeId: QBluetoothServiceInfo::ServiceClassIds).value<QBluetoothServiceInfo::Sequence>(); |
| 232 | modSeq.removeOne(t: QVariant::fromValue(value: id)); |
| 233 | serviceInfo.setAttribute(attributeId: QBluetoothServiceInfo::ServiceClassIds, value: modSeq); |
| 234 | break; |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | if (!isDuplicatedService(serviceInfo)) { |
| 239 | discoveredServices.append(t: serviceInfo); |
| 240 | qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(i: 0).address().toString() |
| 241 | << serviceInfo.serviceName() << serviceInfo.serviceUuid() |
| 242 | << ">>>" << serviceInfo.serviceClassUuids(); |
| 243 | // Use queued connection to allow us finish the service looping; the application |
| 244 | // might call stop() when it has detected the service-of-interest. |
| 245 | QMetaObject::invokeMethod(obj: q, member: "serviceDiscovered" , c: Qt::QueuedConnection, |
| 246 | Q_ARG(QBluetoothServiceInfo, serviceInfo)); |
| 247 | } |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | _q_serviceDiscoveryFinished(); |
| 252 | } |
| 253 | |
| 254 | void QBluetoothServiceDiscoveryAgentPrivate::stop() |
| 255 | { |
| 256 | qCDebug(QT_BT_BLUEZ) << Q_FUNC_INFO << "Stop called" ; |
| 257 | |
| 258 | discoveredDevices.clear(); |
| 259 | setDiscoveryState(Inactive); |
| 260 | |
| 261 | // must happen after discoveredDevices.clear() above to avoid retrigger of next scan |
| 262 | // while waitForFinished() is waiting |
| 263 | if (sdpScannerProcess) { // Bluez 5 |
| 264 | if (sdpScannerProcess->state() != QProcess::NotRunning) { |
| 265 | sdpScannerProcess->kill(); |
| 266 | sdpScannerProcess->waitForFinished(); |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | Q_Q(QBluetoothServiceDiscoveryAgent); |
| 271 | emit q->canceled(); |
| 272 | } |
| 273 | |
| 274 | QBluetoothServiceInfo QBluetoothServiceDiscoveryAgentPrivate::parseServiceXml( |
| 275 | const QString& xmlRecord) |
| 276 | { |
| 277 | QXmlStreamReader xml(xmlRecord); |
| 278 | |
| 279 | QBluetoothServiceInfo serviceInfo; |
| 280 | serviceInfo.setDevice(discoveredDevices.at(i: 0)); |
| 281 | |
| 282 | while (!xml.atEnd()) { |
| 283 | xml.readNext(); |
| 284 | |
| 285 | if (xml.tokenType() == QXmlStreamReader::StartElement && |
| 286 | xml.name() == QLatin1String("attribute" )) { |
| 287 | quint16 attributeId = |
| 288 | xml.attributes().value(qualifiedName: QLatin1String("id" )).toUShort(ok: nullptr, base: 0); |
| 289 | |
| 290 | if (xml.readNextStartElement()) { |
| 291 | const QVariant value = readAttributeValue(xml); |
| 292 | serviceInfo.setAttribute(attributeId, value); |
| 293 | } |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | return serviceInfo; |
| 298 | } |
| 299 | |
| 300 | // Bluez 5 |
| 301 | void QBluetoothServiceDiscoveryAgentPrivate::performMinimalServiceDiscovery(const QBluetoothAddress &deviceAddress) |
| 302 | { |
| 303 | if (foundHostAdapterPath.isEmpty()) { |
| 304 | _q_serviceDiscoveryFinished(); |
| 305 | return; |
| 306 | } |
| 307 | |
| 308 | Q_Q(QBluetoothServiceDiscoveryAgent); |
| 309 | |
| 310 | QDBusPendingReply<ManagedObjectList> reply = manager->GetManagedObjects(); |
| 311 | reply.waitForFinished(); |
| 312 | if (reply.isError()) { |
| 313 | if (singleDevice) { |
| 314 | error = QBluetoothServiceDiscoveryAgent::InputOutputError; |
| 315 | errorString = reply.error().message(); |
| 316 | emit q->errorOccurred(error); |
| 317 | } |
| 318 | _q_serviceDiscoveryFinished(); |
| 319 | return; |
| 320 | } |
| 321 | |
| 322 | QStringList uuidStrings; |
| 323 | |
| 324 | ManagedObjectList managedObjectList = reply.value(); |
| 325 | for (ManagedObjectList::const_iterator it = managedObjectList.constBegin(); it != managedObjectList.constEnd(); ++it) { |
| 326 | const InterfaceList &ifaceList = it.value(); |
| 327 | |
| 328 | for (InterfaceList::const_iterator jt = ifaceList.constBegin(); jt != ifaceList.constEnd(); ++jt) { |
| 329 | const QString &iface = jt.key(); |
| 330 | const QVariantMap &ifaceValues = jt.value(); |
| 331 | |
| 332 | if (iface == QStringLiteral("org.bluez.Device1" )) { |
| 333 | if (deviceAddress.toString() == ifaceValues.value(QStringLiteral("Address" )).toString()) { |
| 334 | uuidStrings = ifaceValues.value(QStringLiteral("UUIDs" )).toStringList(); |
| 335 | break; |
| 336 | } |
| 337 | } |
| 338 | } |
| 339 | if (!uuidStrings.isEmpty()) |
| 340 | break; |
| 341 | } |
| 342 | |
| 343 | if (uuidStrings.isEmpty() || discoveredDevices.isEmpty()) { |
| 344 | qCWarning(QT_BT_BLUEZ) << "No uuids found for" << deviceAddress.toString(); |
| 345 | // nothing found -> go to next uuid |
| 346 | _q_serviceDiscoveryFinished(); |
| 347 | return; |
| 348 | } |
| 349 | |
| 350 | qCDebug(QT_BT_BLUEZ) << "Minimal uuid list for" << deviceAddress.toString() << uuidStrings; |
| 351 | |
| 352 | QBluetoothUuid uuid; |
| 353 | for (qsizetype i = 0; i < uuidStrings.size(); ++i) { |
| 354 | uuid = QBluetoothUuid(uuidStrings.at(i)); |
| 355 | if (uuid.isNull()) |
| 356 | continue; |
| 357 | |
| 358 | //apply uuidFilter |
| 359 | if (!uuidFilter.isEmpty() && !uuidFilter.contains(t: uuid)) |
| 360 | continue; |
| 361 | |
| 362 | QBluetoothServiceInfo serviceInfo; |
| 363 | serviceInfo.setDevice(discoveredDevices.at(i: 0)); |
| 364 | |
| 365 | if (uuid.minimumSize() == 16) { // not derived from Bluetooth Base UUID |
| 366 | serviceInfo.setServiceUuid(uuid); |
| 367 | serviceInfo.setServiceName(QBluetoothServiceDiscoveryAgent::tr(s: "Custom Service" )); |
| 368 | } else { |
| 369 | // set uuid as service class id |
| 370 | QBluetoothServiceInfo::Sequence classId; |
| 371 | classId << QVariant::fromValue(value: uuid); |
| 372 | serviceInfo.setAttribute(attributeId: QBluetoothServiceInfo::ServiceClassIds, value: classId); |
| 373 | QBluetoothUuid::ServiceClassUuid clsId |
| 374 | = static_cast<QBluetoothUuid::ServiceClassUuid>(uuid.data1 & 0xffff); |
| 375 | serviceInfo.setServiceName(QBluetoothUuid::serviceClassToString(uuid: clsId)); |
| 376 | } |
| 377 | |
| 378 | QBluetoothServiceInfo::Sequence protocolDescriptorList; |
| 379 | { |
| 380 | QBluetoothServiceInfo::Sequence protocol; |
| 381 | protocol << QVariant::fromValue(value: QBluetoothUuid(QBluetoothUuid::ProtocolUuid::L2cap)); |
| 382 | protocolDescriptorList.append(t: QVariant::fromValue(value: protocol)); |
| 383 | } |
| 384 | { |
| 385 | QBluetoothServiceInfo::Sequence protocol; |
| 386 | protocol << QVariant::fromValue(value: QBluetoothUuid(QBluetoothUuid::ProtocolUuid::Att)); |
| 387 | protocolDescriptorList.append(t: QVariant::fromValue(value: protocol)); |
| 388 | } |
| 389 | serviceInfo.setAttribute(attributeId: QBluetoothServiceInfo::ProtocolDescriptorList, value: protocolDescriptorList); |
| 390 | |
| 391 | //don't include the service if we already discovered it before |
| 392 | if (!isDuplicatedService(serviceInfo)) { |
| 393 | discoveredServices << serviceInfo; |
| 394 | qCDebug(QT_BT_BLUEZ) << "Discovered services" << discoveredDevices.at(i: 0).address().toString() |
| 395 | << serviceInfo.serviceName(); |
| 396 | emit q->serviceDiscovered(info: serviceInfo); |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | _q_serviceDiscoveryFinished(); |
| 401 | } |
| 402 | |
| 403 | QVariant QBluetoothServiceDiscoveryAgentPrivate::readAttributeValue(QXmlStreamReader &xml) |
| 404 | { |
| 405 | auto skippingCurrentElementByDefault = qScopeGuard(f: [&] { xml.skipCurrentElement(); }); |
| 406 | |
| 407 | if (xml.name() == QLatin1String("boolean" )) { |
| 408 | return xml.attributes().value(qualifiedName: QLatin1String("value" )) == QLatin1String("true" ); |
| 409 | } else if (xml.name() == QLatin1String("uint8" )) { |
| 410 | quint8 value = xml.attributes().value(qualifiedName: QLatin1String("value" )).toUShort(ok: nullptr, base: 0); |
| 411 | return value; |
| 412 | } else if (xml.name() == QLatin1String("uint16" )) { |
| 413 | quint16 value = xml.attributes().value(qualifiedName: QLatin1String("value" )).toUShort(ok: nullptr, base: 0); |
| 414 | return value; |
| 415 | } else if (xml.name() == QLatin1String("uint32" )) { |
| 416 | quint32 value = xml.attributes().value(qualifiedName: QLatin1String("value" )).toUInt(ok: nullptr, base: 0); |
| 417 | return value; |
| 418 | } else if (xml.name() == QLatin1String("uint64" )) { |
| 419 | quint64 value = xml.attributes().value(qualifiedName: QLatin1String("value" )).toULongLong(ok: nullptr, base: 0); |
| 420 | return value; |
| 421 | } else if (xml.name() == QLatin1String("uuid" )) { |
| 422 | QBluetoothUuid uuid; |
| 423 | const QStringView value = xml.attributes().value(qualifiedName: QLatin1String("value" )); |
| 424 | if (value.startsWith(s: QLatin1String("0x" ))) { |
| 425 | if (value.size() == 6) { |
| 426 | quint16 v = value.toUShort(ok: nullptr, base: 0); |
| 427 | uuid = QBluetoothUuid(v); |
| 428 | } else if (value.size() == 10) { |
| 429 | quint32 v = value.toUInt(ok: nullptr, base: 0); |
| 430 | uuid = QBluetoothUuid(v); |
| 431 | } |
| 432 | } else { |
| 433 | uuid = QBluetoothUuid(value.toString()); |
| 434 | } |
| 435 | return QVariant::fromValue(value: uuid); |
| 436 | } else if (xml.name() == QLatin1String("text" ) || xml.name() == QLatin1String("url" )) { |
| 437 | const QStringView value = xml.attributes().value(qualifiedName: QLatin1String("value" )); |
| 438 | if (xml.attributes().value(qualifiedName: QLatin1String("encoding" )) == QLatin1String("hex" )) |
| 439 | return QString::fromUtf8(ba: QByteArray::fromHex(hexEncoded: value.toLatin1())); |
| 440 | return value.toString(); |
| 441 | } else if (xml.name() == QLatin1String("sequence" )) { |
| 442 | QBluetoothServiceInfo::Sequence sequence; |
| 443 | |
| 444 | skippingCurrentElementByDefault.dismiss(); // we skip several elements here |
| 445 | |
| 446 | while (xml.readNextStartElement()) { |
| 447 | QVariant value = readAttributeValue(xml); |
| 448 | sequence.append(t: value); |
| 449 | } |
| 450 | |
| 451 | return QVariant::fromValue<QBluetoothServiceInfo::Sequence>(value: sequence); |
| 452 | } else { |
| 453 | qCWarning(QT_BT_BLUEZ) << "unknown attribute type" |
| 454 | << xml.name() |
| 455 | << xml.attributes().value(qualifiedName: QLatin1String("value" )); |
| 456 | return QVariant(); |
| 457 | } |
| 458 | } |
| 459 | |
| 460 | QT_END_NAMESPACE |
| 461 | |