Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
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
5#include "android/devicediscoverybroadcastreceiver_p.h"
6#include "android/androidutils_p.h"
7#include "android/jni_android_p.h"
9#include <QCoreApplication>
10#include <QtCore/QLoggingCategory>
11#include <QtCore/qpermissions.h>
12#include <QtBluetooth/QBluetoothAddress>
13#include <QtBluetooth/QBluetoothDeviceInfo>
14#include <QtCore/QJniEnvironment>
15#include <QtCore/private/qandroidextras_p.h>
16
18
19Q_DECLARE_LOGGING_CATEGORY(QT_BT_ANDROID)
20
21enum {
22 NoScanActive = 0,
23 SDPScanActive = 1,
24 BtleScanActive = 2
25};
26
27static constexpr auto deviceDiscoveryStartTimeLimit = std::chrono::seconds{5};
28static constexpr short deviceDiscoveryStartMaxAttempts = 6;
29
30QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(
31 const QBluetoothAddress &deviceAdapter, QBluetoothDeviceDiscoveryAgent *parent) :
32 adapterAddress(deviceAdapter),
33 m_active(NoScanActive),
34 deviceDiscoveryStartAttemptsLeft(deviceDiscoveryStartMaxAttempts),
35 q_ptr(parent)
36{
37 adapter = getDefaultBluetoothAdapter();
38
39 if (!adapter.isValid())
40 qCWarning(QT_BT_ANDROID) << "Device does not support Bluetooth";
41}
42
43QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate()
44{
45 if (m_active != NoScanActive)
46 stop();
47
48 if (leScanner.isValid())
49 leScanner.setField("qtObject", reinterpret_cast<jlong>(nullptr));
50
51 if (receiver) {
52 receiver->unregisterReceiver();
53 delete receiver;
54 }
55}
56
57bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const
58{
59 if (pendingStart)
60 return true;
61 if (pendingCancel)
62 return false;
63 return m_active != NoScanActive;
64}
65
66QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods()
67{
68 return (LowEnergyMethod | ClassicMethod);
69}
70
71void QBluetoothDeviceDiscoveryAgentPrivate::classicDiscoveryStartFail()
72{
73 Q_Q(QBluetoothDeviceDiscoveryAgent);
74 lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
75 errorString = QBluetoothDeviceDiscoveryAgent::tr("Classic Discovery cannot be started");
76 emit q->errorOccurred(lastError);
77}
78
79// Sets & emits an error and returns true if bluetooth is off
80bool QBluetoothDeviceDiscoveryAgentPrivate::setErrorIfPowerOff()
81{
82 Q_Q(QBluetoothDeviceDiscoveryAgent);
83
84 const int state = adapter.callMethod<jint>("getState");
85 if (state != 12) { // BluetoothAdapter.STATE_ON
86 lastError = QBluetoothDeviceDiscoveryAgent::PoweredOffError;
87 m_active = NoScanActive;
88 errorString = QBluetoothDeviceDiscoveryAgent::tr("Device is powered off");
89 emit q->errorOccurred(lastError);
90 return true;
91 }
92 return false;
93}
94
95/*
96The Classic/LE discovery method precedence is handled as follows:
97
98If only classic method is set => only classic method is used
99If only LE method is set => only LE method is used
100If both classic and LE methods are set, start classic scan first
101 If classic scan fails to start, start LE scan immediately in the start function
102 Otherwise start LE scan when classic scan completes
103*/
104
105void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods)
106{
107 requestedMethods = methods;
108
109 if (pendingCancel) {
110 pendingStart = true;
111 return;
112 }
113
114 Q_Q(QBluetoothDeviceDiscoveryAgent);
115
116 if (!adapter.isValid()) {
117 qCWarning(QT_BT_ANDROID) << "Device does not support Bluetooth";
118 lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
119 errorString = QBluetoothDeviceDiscoveryAgent::tr("Device does not support Bluetooth");
120 emit q->errorOccurred(lastError);
121 return;
122 }
123
124 if (!adapterAddress.isNull()
125 && adapter.callMethod<jstring>("getAddress").toString()
126 != adapterAddress.toString()) {
127 qCWarning(QT_BT_ANDROID) << "Incorrect local adapter passed.";
128 lastError = QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError;
129 errorString = QBluetoothDeviceDiscoveryAgent::tr("Passed address is not a local device.");
130 emit q->errorOccurred(lastError);
131 return;
132 }
133
134 if (setErrorIfPowerOff())
135 return;
136
137 if (!ensureAndroidPermission(QBluetoothPermission::Access)) {
138 qCWarning(QT_BT_ANDROID)
139 << "Search not possible due to missing QBluetoothPermission::Access permission";
140 errorString = QBluetoothDeviceDiscoveryAgent::tr(
141 "Failed to start device discovery due to missing permissions.");
142 lastError = QBluetoothDeviceDiscoveryAgent::MissingPermissionsError;
143 emit q->errorOccurred(lastError);
144 return;
145 }
146 qCDebug(QT_BT_ANDROID) << "QBluetoothPermission::Access permission available";
147
148 const bool scanNeedsLocation = (bool)QtJniTypes::QtBtUtility::callStaticMethod<jboolean>(
149 "bluetoothScanRequiresLocation", QNativeInterface::QAndroidApplication::context());
150
151 qCDebug(QT_BT_ANDROID) << "Is location service and location permission required for scan:"
152 << scanNeedsLocation;
153
154 if (scanNeedsLocation) {
155 // Double check we have location permission. With API-level < 31 it is already
156 // guaranteed by the ensureAndroidPermission() above, but with API-level 31+ it is
157 // required additionally if 'neverForLocation' assertion isn't set.
158 QLocationPermission locationPermission;
159 locationPermission.setAccuracy(QLocationPermission::Accuracy::Precise);
160
161 if (qApp->checkPermission(locationPermission) != Qt::PermissionStatus::Granted) {
162 qCWarning(QT_BT_ANDROID) << "Search not possible due to missing location permissions";
163 lastError = QBluetoothDeviceDiscoveryAgent::LocationServiceTurnedOffError;
164 errorString = QBluetoothDeviceDiscoveryAgent::tr("Location permission not granted. Search is not possible.");
165 emit q->errorOccurred(lastError);
166 return;
167 }
168
169 qCDebug(QT_BT_ANDROID) << "Location permission granted";
170
171 // Double check Location service is turned on
172 bool locationTurnedOn = true; // backwards compatible behavior to previous Qt versions
173 const QJniObject locString = QJniObject::getStaticObjectField(
174 "android/content/Context", "LOCATION_SERVICE", "Ljava/lang/String;");
175
176 const QJniObject locService =
177 QJniObject(QNativeInterface::QAndroidApplication::context()).callMethod<jobject>(
178 "getSystemService",
179 locString.object<jstring>());
180
181 if (locService.isValid()) {
182 if (QNativeInterface::QAndroidApplication::sdkVersion() >= 28) {
183 locationTurnedOn = bool(locService.callMethod<jboolean>("isLocationEnabled"));
184 } else {
185 // check whether there is any enabled provider
186 QJniObject listOfEnabledProviders =
187 locService.callMethod<QtJniTypes::List>("getProviders", true);
188
189 if (listOfEnabledProviders.isValid()) {
190 int size = listOfEnabledProviders.callMethod<jint>("size");
191 locationTurnedOn = size > 0;
192 qCDebug(QT_BT_ANDROID) << size << "enabled location providers detected.";
193 }
194 }
195 }
196
197 if (!locationTurnedOn) {
198 qCWarning(QT_BT_ANDROID) << "Search not possible due to turned off Location service";
199 lastError = QBluetoothDeviceDiscoveryAgent::LocationServiceTurnedOffError;
200 errorString = QBluetoothDeviceDiscoveryAgent::tr("Location service turned off. Search is not possible.");
201 emit q->errorOccurred(lastError);
202 return;
203 }
204
205 qCDebug(QT_BT_ANDROID) << "Location turned on";
206 }
207
208 // install Java BroadcastReceiver
209 if (!receiver) {
210 // SDP based device discovery
211 receiver = new DeviceDiscoveryBroadcastReceiver();
212 qRegisterMetaType<QBluetoothDeviceInfo>();
213 QObject::connect(receiver, SIGNAL(deviceDiscovered(QBluetoothDeviceInfo,bool)),
214 this, SLOT(processDiscoveredDevices(QBluetoothDeviceInfo,bool)));
215 QObject::connect(receiver, SIGNAL(finished()), this, SLOT(processSdpDiscoveryFinished()));
216 }
217
218 lastError = QBluetoothDeviceDiscoveryAgent::NoError;
219 errorString.clear();
220 discoveredDevices.clear();
221
222 // by arbitrary definition we run classic search first
223 if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) {
224 const bool success = adapter.callMethod<jboolean>("startDiscovery");
225 if (!success) {
226 qCDebug(QT_BT_ANDROID) << "Classic Discovery cannot be started";
227 // Check if only classic discovery requested -> error out and return.
228 // Otherwise since LE was also requested => don't return but allow the
229 // function to continue to LE scanning
230 if (requestedMethods == QBluetoothDeviceDiscoveryAgent::ClassicMethod) {
231 classicDiscoveryStartFail();
232 return;
233 }
234 } else {
235 m_active = SDPScanActive;
236 if (!deviceDiscoveryStartTimeout) {
237 // In some bluetooth environments device discovery does not start properly
238 // if it is done shortly after (up to 20 seconds) a full service discovery.
239 // In that case we never get DISOVERY_STARTED action and device discovery never
240 // finishes. Here we use a small timeout to guard it; if we don't get the
241 // 'started' action in time, we restart the query. In the normal case the action
242 // is received in < 1 second. See QTBUG-101066
243 deviceDiscoveryStartTimeout = new QTimer(this);
244 deviceDiscoveryStartTimeout->setInterval(deviceDiscoveryStartTimeLimit);
245 deviceDiscoveryStartTimeout->setSingleShot(true);
246 QObject::connect(receiver, &DeviceDiscoveryBroadcastReceiver::discoveryStarted,
247 deviceDiscoveryStartTimeout, &QTimer::stop);
248 QObject::connect(deviceDiscoveryStartTimeout, &QTimer::timeout, this, [this]() {
249 deviceDiscoveryStartAttemptsLeft -= 1;
250 qCWarning(QT_BT_ANDROID) << "Discovery start not received, attempts left:"
251 << deviceDiscoveryStartAttemptsLeft;
252 // Check that bluetooth is not switched off
253 if (setErrorIfPowerOff())
254 return;
255 // If this was the last retry attempt, cancel the discovery just in case
256 // as a good cleanup practice
257 if (deviceDiscoveryStartAttemptsLeft <= 0) {
258 qCWarning(QT_BT_ANDROID) << "Classic device discovery failed to start";
259 (void)adapter.callMethod<jboolean>("cancelDiscovery");
260 }
261 // Restart the discovery and retry timer.
262 // The logic below is similar as in the start()
263 if (deviceDiscoveryStartAttemptsLeft > 0 &&
264 adapter.callMethod<jboolean>("startDiscovery"))
265 deviceDiscoveryStartTimeout->start();
266 else if (requestedMethods == QBluetoothDeviceDiscoveryAgent::ClassicMethod)
267 classicDiscoveryStartFail(); // No LE scan requested, scan is done
268 else
269 startLowEnergyScan(); // Continue to LE scan
270 });
271 }
272 deviceDiscoveryStartAttemptsLeft = deviceDiscoveryStartMaxAttempts;
273 deviceDiscoveryStartTimeout->start();
274
275 qCDebug(QT_BT_ANDROID)
276 << "QBluetoothDeviceDiscoveryAgentPrivate::start() - Classic search successfully started.";
277 return;
278 }
279 }
280
281 if (requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) {
282 // LE search only requested or classic discovery failed but lets try LE scan anyway
283 startLowEnergyScan();
284 }
285}
286
287void QBluetoothDeviceDiscoveryAgentPrivate::stop()
288{
289 Q_Q(QBluetoothDeviceDiscoveryAgent);
290
291 pendingStart = false;
292
293 if (deviceDiscoveryStartTimeout)
294 deviceDiscoveryStartTimeout->stop();
295
296 if (m_active == NoScanActive)
297 return;
298
299 if (m_active == SDPScanActive) {
300 if (pendingCancel) {
301 // If we had both a pending cancel and a pending start,
302 // we now have only a pending cancel.
303 // The pending start was canceled above.
304 return;
305 }
306
307 pendingCancel = true;
308 bool success = adapter.callMethod<jboolean>("cancelDiscovery");
309 if (!success) {
310 lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError;
311 errorString = QBluetoothDeviceDiscoveryAgent::tr("Discovery cannot be stopped");
312 emit q->errorOccurred(lastError);
313 return;
314 }
315 } else if (m_active == BtleScanActive) {
316 if (pendingCancel)
317 return;
318 pendingCancel = true;
319 if (leScanTimeout->isActive())
320 leScanTimeout->stop();
321 stopLowEnergyScan();
322 }
323}
324
325void QBluetoothDeviceDiscoveryAgentPrivate::processSdpDiscoveryFinished()
326{
327 // We need to guard because Android sends two DISCOVERY_FINISHED when cancelling
328 // Also if we have two active agents both receive the same signal.
329 // If this one is not active ignore the device information
330 if (m_active != SDPScanActive)
331 return;
332
333 Q_Q(QBluetoothDeviceDiscoveryAgent);
334
335 if (pendingCancel && !pendingStart) {
336 m_active = NoScanActive;
337 pendingCancel = false;
338 emit q->canceled();
339 } else if (pendingStart) {
340 pendingStart = pendingCancel = false;
341 start(requestedMethods);
342 } else {
343 // check that it didn't finish due to turned off Bluetooth Device
344 if (setErrorIfPowerOff())
345 return;
346 // Since no BTLE scan requested and classic scan is done => finished()
347 if (!(requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)) {
348 m_active = NoScanActive;
349 emit q->finished();
350 return;
351 }
352
353 startLowEnergyScan();
354 }
355}
356
357void QBluetoothDeviceDiscoveryAgentPrivate::processDiscoveredDevices(
358 const QBluetoothDeviceInfo &info, bool isLeResult)
359{
360 // If we have two active agents both receive the same signal.
361 // If this one is not active ignore the device information
362 if (m_active != SDPScanActive && !isLeResult)
363 return;
364 if (m_active != BtleScanActive && isLeResult)
365 return;
366
367 Q_Q(QBluetoothDeviceDiscoveryAgent);
368
369 // Android Classic scan and LE scan can find the same device under different names
370 // The classic name finds the SDP based device name, the LE scan finds the name in
371 // the advertisement package.
372 // If address is same but name different then we keep both entries.
373
374 for (qsizetype i = 0; i < discoveredDevices.size(); ++i) {
375 if (discoveredDevices[i].address() == info.address()) {
376 QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None;
377 if (discoveredDevices[i].rssi() != info.rssi()) {
378 qCDebug(QT_BT_ANDROID) << "Updating RSSI for" << info.address()
379 << info.rssi();
380 discoveredDevices[i].setRssi(info.rssi());
381 updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI);
382 }
383 if (discoveredDevices[i].manufacturerData() != info.manufacturerData()) {
384 qCDebug(QT_BT_ANDROID) << "Updating manufacturer data for" << info.address();
385 const QList<quint16> keys = info.manufacturerIds();
386 for (auto key: keys)
387 discoveredDevices[i].setManufacturerData(key, info.manufacturerData(key));
388 updatedFields.setFlag(QBluetoothDeviceInfo::Field::ManufacturerData);
389 }
390 if (discoveredDevices[i].serviceData() != info.serviceData()) {
391 qCDebug(QT_BT_ANDROID) << "Updating service data for" << info.address();
392 const QList<QBluetoothUuid> keys = info.serviceIds();
393 for (auto key : keys)
394 discoveredDevices[i].setServiceData(key, info.serviceData(key));
395 updatedFields.setFlag(QBluetoothDeviceInfo::Field::ServiceData);
396 }
397
398 if (lowEnergySearchTimeout > 0) {
399 if (discoveredDevices[i] != info) {
400 if (discoveredDevices.at(i).name() == info.name()) {
401 qCDebug(QT_BT_ANDROID) << "Almost Duplicate " << info.address()
402 << info.name() << "- replacing in place";
403 discoveredDevices.replace(i, info);
404 emit q->deviceDiscovered(info);
405 }
406 } else {
407 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
408 emit q->deviceUpdated(discoveredDevices[i], updatedFields);
409 }
410
411 return;
412 }
413
414 discoveredDevices.replace(i, info);
415 emit q->deviceDiscovered(info);
416
417 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
418 emit q->deviceUpdated(discoveredDevices[i], updatedFields);
419
420 return;
421 }
422 }
423
424 discoveredDevices.append(info);
425 qCDebug(QT_BT_ANDROID) << "Device found: " << info.name() << info.address().toString()
426 << "isLeScanResult:" << isLeResult
427 << "Manufacturer data size:" << info.manufacturerData().size();
428 emit q->deviceDiscovered(info);
429}
430
431void QBluetoothDeviceDiscoveryAgentPrivate::startLowEnergyScan()
432{
433 Q_Q(QBluetoothDeviceDiscoveryAgent);
434
435 m_active = BtleScanActive;
436 ++leScanEpoch;
437
438 if (!leScanner.isValid()) {
439 leScanner = QJniObject::construct<QtJniTypes::QtBtLECentral>(
440 QNativeInterface::QAndroidApplication::context());
441 if (!leScanner.isValid()) {
442 qCWarning(QT_BT_ANDROID) << "Cannot load BTLE device scan class";
443 m_active = NoScanActive;
444 emit q->finished();
445 return;
446 }
447
448 leScanner.setField("qtObject", reinterpret_cast<jlong>(receiver));
449 }
450
451 jboolean result = leScanner.callMethod<jboolean>("scanForLeDevice", true);
452 if (!result) {
453 qCWarning(QT_BT_ANDROID) << "Cannot start BTLE device scanner";
454 m_active = NoScanActive;
455 emit q->finished();
456 return;
457 }
458
459 // wait interval and sum up what was found
460 if (!leScanTimeout) {
461 leScanTimeout = new QTimer(this);
462 leScanTimeout->setSingleShot(true);
463 connect(leScanTimeout, &QTimer::timeout,
464 this, &QBluetoothDeviceDiscoveryAgentPrivate::stopLowEnergyScan);
465 }
466
467 if (lowEnergySearchTimeout > 0) { // otherwise no timeout and stop() required
468 leScanTimeout->setInterval(lowEnergySearchTimeout);
469 leScanTimeout->start();
470 }
471
472 qCDebug(QT_BT_ANDROID)
473 << "QBluetoothDeviceDiscoveryAgentPrivate::start() - Low Energy search successfully started.";
474}
475
476void QBluetoothDeviceDiscoveryAgentPrivate::processLeDiscoveryFinished(int epoch)
477{
478 // Drop stale or duplicate invocations.
479 if (epoch != leScanEpoch || m_active != BtleScanActive)
480 return;
481
482 m_active = NoScanActive;
483
484 // Same three-way branch as processSdpDiscoveryFinished().
485 Q_Q(QBluetoothDeviceDiscoveryAgent);
486 if (pendingCancel && !pendingStart) {
487 pendingCancel = false;
488 emit q->canceled();
489 } else if (pendingStart) {
490 pendingStart = pendingCancel = false;
491 start(requestedMethods);
492 } else {
493 // timeout -> regular stop
494 emit q->finished();
495 }
496}
497
498void QBluetoothDeviceDiscoveryAgentPrivate::stopLowEnergyScan()
499{
500 jboolean result = leScanner.callMethod<jboolean>("scanForLeDevice", false);
501 if (!result)
502 qCWarning(QT_BT_ANDROID) << "Cannot stop BTLE device scanner";
503
504 // Defer so a follow-up start() can set pendingStart; the captured epoch
505 // skips this handler if a fresh scan starts first.
506 QMetaObject::invokeMethod(this,
507 [this, epoch = leScanEpoch]() { processLeDiscoveryFinished(epoch); },
508 Qt::QueuedConnection);
509}
510QT_END_NAMESPACE
Combined button and popup list for selecting options.
static constexpr auto deviceDiscoveryStartTimeLimit
static constexpr short deviceDiscoveryStartMaxAttempts