Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
qbluetoothdevicediscoveryagent_android.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 Lauri Laanmets (Proekspert AS) <lauri.laanmets@eesti.ee>
2// Copyright (C) 2016 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
9#include <QCoreApplication>
10#include <QtCore/QLoggingCategory>
11#include <QtBluetooth/QBluetoothAddress>
12#include <QtBluetooth/QBluetoothDeviceInfo>
13#include <QtCore/QJniEnvironment>
14#include <QtCore/private/qandroidextras_p.h>
15
17
18Q_DECLARE_LOGGING_CATEGORY(QT_BT_ANDROID)
19
20enum {
24};
25
26static constexpr auto deviceDiscoveryStartTimeLimit = std::chrono::seconds{5};
27static constexpr short deviceDiscoveryStartMaxAttempts = 6;
28
30 const QBluetoothAddress &deviceAdapter, QBluetoothDeviceDiscoveryAgent *parent) :
31 adapterAddress(deviceAdapter),
33 deviceDiscoveryStartAttemptsLeft(deviceDiscoveryStartMaxAttempts),
34 q_ptr(parent)
35{
37
38 if (!adapter.isValid())
39 qCWarning(QT_BT_ANDROID) << "Device does not support Bluetooth";
40}
41
43{
45 stop();
46
47 if (leScanner.isValid())
48 leScanner.setField<jlong>("qtObject", reinterpret_cast<jlong>(nullptr));
49
50 if (receiver) {
51 receiver->unregisterReceiver();
52 delete receiver;
53 }
54}
55
57{
58 if (pendingStart)
59 return true;
60 if (pendingCancel)
61 return false;
62 return m_active != NoScanActive;
63}
64
65QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods()
66{
68}
69
70void QBluetoothDeviceDiscoveryAgentPrivate::classicDiscoveryStartFail()
71{
74 errorString = QBluetoothDeviceDiscoveryAgent::tr("Classic Discovery cannot be started");
75 emit q->errorOccurred(lastError);
76}
77
78// Sets & emits an error and returns true if bluetooth is off
79bool QBluetoothDeviceDiscoveryAgentPrivate::setErrorIfPowerOff()
80{
82
83 const int state = adapter.callMethod<jint>("getState");
84 if (state != 12) { // BluetoothAdapter.STATE_ON
87 errorString = QBluetoothDeviceDiscoveryAgent::tr("Device is powered off");
88 emit q->errorOccurred(lastError);
89 return true;
90 }
91 return false;
92}
93
94/*
95The Classic/LE discovery method precedence is handled as follows:
96
97If only classic method is set => only classic method is used
98If only LE method is set => only LE method is used
99If both classic and LE methods are set, start classic scan first
100 If classic scan fails to start, start LE scan immediately in the start function
101 Otherwise start LE scan when classic scan completes
102*/
103
104void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
105{
106 requestedMethods = methods;
107
108 if (pendingCancel) {
109 pendingStart = true;
110 return;
111 }
112
114
115 if (!adapter.isValid()) {
116 qCWarning(QT_BT_ANDROID) << "Device does not support Bluetooth";
118 errorString = QBluetoothDeviceDiscoveryAgent::tr("Device does not support Bluetooth");
119 emit q->errorOccurred(lastError);
120 return;
121 }
122
123 if (!adapterAddress.isNull()
124 && adapter.callMethod<jstring>("getAddress").toString()
125 != adapterAddress.toString()) {
126 qCWarning(QT_BT_ANDROID) << "Incorrect local adapter passed.";
128 errorString = QBluetoothDeviceDiscoveryAgent::tr("Passed address is not a local device.");
129 emit q->errorOccurred(lastError);
130 return;
131 }
132
133 if (setErrorIfPowerOff())
134 return;
135
137 qCWarning(QT_BT_ANDROID)
138 << "Search not possible due to missing QBluetoothPermission::Access permission";
139 errorString = QBluetoothDeviceDiscoveryAgent::tr(
140 "Failed to start device discovery due to missing permissions.");
142 emit q->errorOccurred(lastError);
143 return;
144 }
145 qCDebug(QT_BT_ANDROID) << "QBluetoothPermission::Access permission available";
146
147 // Double check Location service is turned on
148 bool locationTurnedOn = true; // backwards compatible behavior to previous Qt versions
149 const QJniObject locString = QJniObject::getStaticObjectField(
150 "android/content/Context", "LOCATION_SERVICE", "Ljava/lang/String;");
151
152 const QJniObject locService =
153 QJniObject(QNativeInterface::QAndroidApplication::context()).callMethod<jobject>(
154 "getSystemService",
155 locString.object<jstring>());
156
157 if (locService.isValid()) {
158 if (QNativeInterface::QAndroidApplication::sdkVersion() >= 28) {
159 locationTurnedOn = bool(locService.callMethod<jboolean>("isLocationEnabled"));
160 } else {
161 // check whether there is any enabled provider
162 QJniObject listOfEnabledProviders =
163 locService.callMethod<QtJniTypes::List>("getProviders", true);
164
165 if (listOfEnabledProviders.isValid()) {
166 int size = listOfEnabledProviders.callMethod<jint>("size");
167 locationTurnedOn = size > 0;
168 qCDebug(QT_BT_ANDROID) << size << "enabled location providers detected.";
169 }
170 }
171 }
172
173 if (!locationTurnedOn) {
174 qCWarning(QT_BT_ANDROID) << "Search not possible due to turned off Location service";
176 errorString = QBluetoothDeviceDiscoveryAgent::tr("Location service turned off. Search is not possible.");
177 emit q->errorOccurred(lastError);
178 return;
179 }
180
181 qCDebug(QT_BT_ANDROID) << "Location turned on";
182
183 // install Java BroadcastReceiver
184 if (!receiver) {
185 // SDP based device discovery
186 receiver = new DeviceDiscoveryBroadcastReceiver();
187 qRegisterMetaType<QBluetoothDeviceInfo>();
188 QObject::connect(receiver, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo,bool)),
189 this, SLOT(processDiscoveredDevices(QBluetoothDeviceInfo,bool)));
190 QObject::connect(receiver, SIGNAL(finished()), this, SLOT(processSdpDiscoveryFinished()));
191 }
192
194 errorString.clear();
195 discoveredDevices.clear();
196
197 // by arbitrary definition we run classic search first
198 if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) {
199 const bool success = adapter.callMethod<jboolean>("startDiscovery");
200 if (!success) {
201 qCDebug(QT_BT_ANDROID) << "Classic Discovery cannot be started";
202 // Check if only classic discovery requested -> error out and return.
203 // Otherwise since LE was also requested => don't return but allow the
204 // function to continue to LE scanning
205 if (requestedMethods == QBluetoothDeviceDiscoveryAgent::ClassicMethod) {
206 classicDiscoveryStartFail();
207 return;
208 }
209 } else {
211 if (!deviceDiscoveryStartTimeout) {
212 // In some bluetooth environments device discovery does not start properly
213 // if it is done shortly after (up to 20 seconds) a full service discovery.
214 // In that case we never get DISOVERY_STARTED action and device discovery never
215 // finishes. Here we use a small timeout to guard it; if we don't get the
216 // 'started' action in time, we restart the query. In the normal case the action
217 // is received in < 1 second. See QTBUG-101066
218 deviceDiscoveryStartTimeout = new QTimer(this);
219 deviceDiscoveryStartTimeout->setInterval(deviceDiscoveryStartTimeLimit);
220 deviceDiscoveryStartTimeout->setSingleShot(true);
222 deviceDiscoveryStartTimeout, &QTimer::stop);
223 QObject::connect(deviceDiscoveryStartTimeout, &QTimer::timeout, this, [this]() {
224 deviceDiscoveryStartAttemptsLeft -= 1;
225 qCWarning(QT_BT_ANDROID) << "Discovery start not received, attempts left:"
226 << deviceDiscoveryStartAttemptsLeft;
227 // Check that bluetooth is not switched off
228 if (setErrorIfPowerOff())
229 return;
230 // If this was the last retry attempt, cancel the discovery just in case
231 // as a good cleanup practice
232 if (deviceDiscoveryStartAttemptsLeft <= 0) {
233 qCWarning(QT_BT_ANDROID) << "Classic device discovery failed to start";
234 (void)adapter.callMethod<jboolean>("cancelDiscovery");
235 }
236 // Restart the discovery and retry timer.
237 // The logic below is similar as in the start()
238 if (deviceDiscoveryStartAttemptsLeft > 0 &&
239 adapter.callMethod<jboolean>("startDiscovery"))
240 deviceDiscoveryStartTimeout->start();
241 else if (requestedMethods == QBluetoothDeviceDiscoveryAgent::ClassicMethod)
242 classicDiscoveryStartFail(); // No LE scan requested, scan is done
243 else
244 startLowEnergyScan(); // Continue to LE scan
245 });
246 }
247 deviceDiscoveryStartAttemptsLeft = deviceDiscoveryStartMaxAttempts;
248 deviceDiscoveryStartTimeout->start();
249
250 qCDebug(QT_BT_ANDROID)
251 << "QBluetoothDeviceDiscoveryAgentPrivate::start() - Classic search successfully started.";
252 return;
253 }
254 }
255
257 // LE search only requested or classic discovery failed but lets try LE scan anyway
258 startLowEnergyScan();
259 }
260}
261
263{
265
266 pendingStart = false;
267
268 if (deviceDiscoveryStartTimeout)
269 deviceDiscoveryStartTimeout->stop();
270
271 if (m_active == NoScanActive)
272 return;
273
274 if (m_active == SDPScanActive) {
275 if (pendingCancel) {
276 // If we had both a pending cancel and a pending start,
277 // we now have only a pending cancel.
278 // The pending start was canceled above.
279 return;
280 }
281
282 pendingCancel = true;
283 bool success = adapter.callMethod<jboolean>("cancelDiscovery");
284 if (!success) {
286 errorString = QBluetoothDeviceDiscoveryAgent::tr("Discovery cannot be stopped");
287 emit q->errorOccurred(lastError);
288 return;
289 }
290 } else if (m_active == BtleScanActive) {
291 stopLowEnergyScan();
292 }
293}
294
295void QBluetoothDeviceDiscoveryAgentPrivate::processSdpDiscoveryFinished()
296{
297 // We need to guard because Android sends two DISCOVERY_FINISHED when cancelling
298 // Also if we have two active agents both receive the same signal.
299 // If this one is not active ignore the device information
300 if (m_active != SDPScanActive)
301 return;
302
304
305 if (pendingCancel && !pendingStart) {
307 pendingCancel = false;
308 emit q->canceled();
309 } else if (pendingStart) {
310 pendingStart = pendingCancel = false;
311 start(requestedMethods);
312 } else {
313 // check that it didn't finish due to turned off Bluetooth Device
314 if (setErrorIfPowerOff())
315 return;
316 // Since no BTLE scan requested and classic scan is done => finished()
317 if (!(requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)) {
319 emit q->finished();
320 return;
321 }
322
323 startLowEnergyScan();
324 }
325}
326
327void QBluetoothDeviceDiscoveryAgentPrivate::processDiscoveredDevices(
328 const QBluetoothDeviceInfo &info, bool isLeResult)
329{
330 // If we have two active agents both receive the same signal.
331 // If this one is not active ignore the device information
332 if (m_active != SDPScanActive && !isLeResult)
333 return;
334 if (m_active != BtleScanActive && isLeResult)
335 return;
336
338
339 // Android Classic scan and LE scan can find the same device under different names
340 // The classic name finds the SDP based device name, the LE scan finds the name in
341 // the advertisement package.
342 // If address is same but name different then we keep both entries.
343
344 for (qsizetype i = 0; i < discoveredDevices.size(); ++i) {
345 if (discoveredDevices[i].address() == info.address()) {
346 QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None;
347 if (discoveredDevices[i].rssi() != info.rssi()) {
348 qCDebug(QT_BT_ANDROID) << "Updating RSSI for" << info.address()
349 << info.rssi();
350 discoveredDevices[i].setRssi(info.rssi());
351 updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI);
352 }
353 if (discoveredDevices[i].manufacturerData() != info.manufacturerData()) {
354 qCDebug(QT_BT_ANDROID) << "Updating manufacturer data for" << info.address();
355 const QList<quint16> keys = info.manufacturerIds();
356 for (auto key: keys)
357 discoveredDevices[i].setManufacturerData(key, info.manufacturerData(key));
359 }
360 if (discoveredDevices[i].serviceData() != info.serviceData()) {
361 qCDebug(QT_BT_ANDROID) << "Updating service data for" << info.address();
362 const QList<QBluetoothUuid> keys = info.serviceIds();
363 for (auto key : keys)
364 discoveredDevices[i].setServiceData(key, info.serviceData(key));
365 updatedFields.setFlag(QBluetoothDeviceInfo::Field::ServiceData);
366 }
367
368 if (lowEnergySearchTimeout > 0) {
369 if (discoveredDevices[i] != info) {
370 if (discoveredDevices.at(i).name() == info.name()) {
371 qCDebug(QT_BT_ANDROID) << "Almost Duplicate " << info.address()
372 << info.name() << "- replacing in place";
373 discoveredDevices.replace(i, info);
374 emit q->deviceDiscovered(info);
375 }
376 } else {
377 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
378 emit q->deviceUpdated(discoveredDevices[i], updatedFields);
379 }
380
381 return;
382 }
383
384 discoveredDevices.replace(i, info);
385 emit q->deviceDiscovered(info);
386
387 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
388 emit q->deviceUpdated(discoveredDevices[i], updatedFields);
389
390 return;
391 }
392 }
393
394 discoveredDevices.append(info);
395 qCDebug(QT_BT_ANDROID) << "Device found: " << info.name() << info.address().toString()
396 << "isLeScanResult:" << isLeResult
397 << "Manufacturer data size:" << info.manufacturerData().size();
398 emit q->deviceDiscovered(info);
399}
400
401void QBluetoothDeviceDiscoveryAgentPrivate::startLowEnergyScan()
402{
404
406
407 if (!leScanner.isValid()) {
408 leScanner = QJniObject::construct<QtJniTypes::QtBtLECentral>(
409 QNativeInterface::QAndroidApplication::context());
410 if (!leScanner.isValid()) {
411 qCWarning(QT_BT_ANDROID) << "Cannot load BTLE device scan class";
413 emit q->finished();
414 return;
415 }
416
417 leScanner.setField<jlong>("qtObject", reinterpret_cast<long>(receiver));
418 }
419
420 jboolean result = leScanner.callMethod<jboolean>("scanForLeDevice", true);
421 if (!result) {
422 qCWarning(QT_BT_ANDROID) << "Cannot start BTLE device scanner";
424 emit q->finished();
425 return;
426 }
427
428 // wait interval and sum up what was found
429 if (!leScanTimeout) {
430 leScanTimeout = new QTimer(this);
431 leScanTimeout->setSingleShot(true);
432 connect(leScanTimeout, &QTimer::timeout,
433 this, &QBluetoothDeviceDiscoveryAgentPrivate::stopLowEnergyScan);
434 }
435
436 if (lowEnergySearchTimeout > 0) { // otherwise no timeout and stop() required
437 leScanTimeout->setInterval(lowEnergySearchTimeout);
438 leScanTimeout->start();
439 }
440
441 qCDebug(QT_BT_ANDROID)
442 << "QBluetoothDeviceDiscoveryAgentPrivate::start() - Low Energy search successfully started.";
443}
444
445void QBluetoothDeviceDiscoveryAgentPrivate::stopLowEnergyScan()
446{
447 jboolean result = leScanner.callMethod<jboolean>("scanForLeDevice", false);
448 if (!result)
449 qCWarning(QT_BT_ANDROID) << "Cannot stop BTLE device scanner";
450
452
454 if (leScanTimeout->isActive()) {
455 // still active if this function was called from stop()
456 leScanTimeout->stop();
457 emit q->canceled();
458 } else {
459 // timeout -> regular stop
460 emit q->finished();
461 }
462}
static JNINativeMethod methods[]
QJniObject getDefaultBluetoothAdapter()
QT_BEGIN_NAMESPACE bool ensureAndroidPermission(QBluetoothPermission::CommunicationModes modes)
quint8 rssi
bool m_active
\inmodule QtBluetooth
void start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress &deviceAdapter, QBluetoothDeviceDiscoveryAgent *parent)
static DiscoveryMethods supportedDiscoveryMethods()
This function returns the discovery methods supported by the current platform.
\inmodule QtBluetooth
QString name() const
Returns the name assigned to the device.
\inmodule QtCore
qsizetype size() const noexcept
Definition qlist.h:397
const_reference at(qsizetype i) const noexcept
Definition qlist.h:446
void replace(qsizetype i, parameter_type t)
Definition qlist.h:543
void append(parameter_type t)
Definition qlist.h:458
void clear()
Definition qlist.h:434
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2960
void clear()
Clears the contents of the string and makes it null.
Definition qstring.h:1252
\inmodule QtCore
Definition qtimer.h:20
void stop()
Stops the timer.
Definition qtimer.cpp:267
void timeout(QPrivateSignal)
This signal is emitted when the timer times out.
else opt state
[0]
Combined button and popup list for selecting options.
static constexpr auto deviceDiscoveryStartTimeLimit
static constexpr short deviceDiscoveryStartMaxAttempts
DBusConnection const char DBusError DBusBusType DBusError return DBusConnection DBusHandleMessageFunction void DBusFreeFunction return DBusConnection return DBusConnection return const char DBusError return DBusConnection DBusMessage dbus_uint32_t return DBusConnection dbus_bool_t DBusConnection DBusAddWatchFunction DBusRemoveWatchFunction DBusWatchToggledFunction void DBusFreeFunction return DBusConnection DBusDispatchStatusFunction void DBusFreeFunction DBusTimeout return DBusTimeout return DBusWatch return DBusWatch unsigned int return DBusError const DBusError return const DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessageIter int const void return DBusMessageIter DBusMessageIter return DBusMessageIter void DBusMessageIter void int return DBusMessage DBusMessageIter return DBusMessageIter return DBusMessageIter DBusMessageIter const char const char const char const char return DBusMessage return DBusMessage const char return DBusMessage dbus_bool_t return DBusMessage dbus_uint32_t return DBusMessage void
#define qCWarning(category,...)
#define qCDebug(category,...)
#define Q_DECLARE_LOGGING_CATEGORY(name)
#define SLOT(a)
Definition qobjectdefs.h:52
#define SIGNAL(a)
Definition qobjectdefs.h:53
GLuint64 key
GLenum GLuint GLintptr GLsizeiptr size
[1]
GLuint start
GLuint GLuint64EXT address
GLdouble GLdouble GLdouble GLdouble q
Definition qopenglext.h:259
GLuint64EXT * result
[6]
#define emit
ptrdiff_t qsizetype
Definition qtypes.h:165
QStringList keys
connect(quitButton, &QPushButton::clicked, &app, &QCoreApplication::quit, Qt::QueuedConnection)
QHostInfo info
[0]