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<jlong>("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 stopLowEnergyScan();
317 }
318}
319
320void QBluetoothDeviceDiscoveryAgentPrivate::processSdpDiscoveryFinished()
321{
322 // We need to guard because Android sends two DISCOVERY_FINISHED when cancelling
323 // Also if we have two active agents both receive the same signal.
324 // If this one is not active ignore the device information
325 if (m_active != SDPScanActive)
326 return;
327
328 Q_Q(QBluetoothDeviceDiscoveryAgent);
329
330 if (pendingCancel && !pendingStart) {
331 m_active = NoScanActive;
332 pendingCancel = false;
333 emit q->canceled();
334 } else if (pendingStart) {
335 pendingStart = pendingCancel = false;
336 start(requestedMethods);
337 } else {
338 // check that it didn't finish due to turned off Bluetooth Device
339 if (setErrorIfPowerOff())
340 return;
341 // Since no BTLE scan requested and classic scan is done => finished()
342 if (!(requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)) {
343 m_active = NoScanActive;
344 emit q->finished();
345 return;
346 }
347
348 startLowEnergyScan();
349 }
350}
351
352void QBluetoothDeviceDiscoveryAgentPrivate::processDiscoveredDevices(
353 const QBluetoothDeviceInfo &info, bool isLeResult)
354{
355 // If we have two active agents both receive the same signal.
356 // If this one is not active ignore the device information
357 if (m_active != SDPScanActive && !isLeResult)
358 return;
359 if (m_active != BtleScanActive && isLeResult)
360 return;
361
362 Q_Q(QBluetoothDeviceDiscoveryAgent);
363
364 // Android Classic scan and LE scan can find the same device under different names
365 // The classic name finds the SDP based device name, the LE scan finds the name in
366 // the advertisement package.
367 // If address is same but name different then we keep both entries.
368
369 for (qsizetype i = 0; i < discoveredDevices.size(); ++i) {
370 if (discoveredDevices[i].address() == info.address()) {
371 QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None;
372 if (discoveredDevices[i].rssi() != info.rssi()) {
373 qCDebug(QT_BT_ANDROID) << "Updating RSSI for" << info.address()
374 << info.rssi();
375 discoveredDevices[i].setRssi(info.rssi());
376 updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI);
377 }
378 if (discoveredDevices[i].manufacturerData() != info.manufacturerData()) {
379 qCDebug(QT_BT_ANDROID) << "Updating manufacturer data for" << info.address();
380 const QList<quint16> keys = info.manufacturerIds();
381 for (auto key: keys)
382 discoveredDevices[i].setManufacturerData(key, info.manufacturerData(key));
383 updatedFields.setFlag(QBluetoothDeviceInfo::Field::ManufacturerData);
384 }
385 if (discoveredDevices[i].serviceData() != info.serviceData()) {
386 qCDebug(QT_BT_ANDROID) << "Updating service data for" << info.address();
387 const QList<QBluetoothUuid> keys = info.serviceIds();
388 for (auto key : keys)
389 discoveredDevices[i].setServiceData(key, info.serviceData(key));
390 updatedFields.setFlag(QBluetoothDeviceInfo::Field::ServiceData);
391 }
392
393 if (lowEnergySearchTimeout > 0) {
394 if (discoveredDevices[i] != info) {
395 if (discoveredDevices.at(i).name() == info.name()) {
396 qCDebug(QT_BT_ANDROID) << "Almost Duplicate " << info.address()
397 << info.name() << "- replacing in place";
398 discoveredDevices.replace(i, info);
399 emit q->deviceDiscovered(info);
400 }
401 } else {
402 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
403 emit q->deviceUpdated(discoveredDevices[i], updatedFields);
404 }
405
406 return;
407 }
408
409 discoveredDevices.replace(i, info);
410 emit q->deviceDiscovered(info);
411
412 if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None))
413 emit q->deviceUpdated(discoveredDevices[i], updatedFields);
414
415 return;
416 }
417 }
418
419 discoveredDevices.append(info);
420 qCDebug(QT_BT_ANDROID) << "Device found: " << info.name() << info.address().toString()
421 << "isLeScanResult:" << isLeResult
422 << "Manufacturer data size:" << info.manufacturerData().size();
423 emit q->deviceDiscovered(info);
424}
425
426void QBluetoothDeviceDiscoveryAgentPrivate::startLowEnergyScan()
427{
428 Q_Q(QBluetoothDeviceDiscoveryAgent);
429
430 m_active = BtleScanActive;
431
432 if (!leScanner.isValid()) {
433 leScanner = QJniObject::construct<QtJniTypes::QtBtLECentral>(
434 QNativeInterface::QAndroidApplication::context());
435 if (!leScanner.isValid()) {
436 qCWarning(QT_BT_ANDROID) << "Cannot load BTLE device scan class";
437 m_active = NoScanActive;
438 emit q->finished();
439 return;
440 }
441
442 leScanner.setField<jlong>("qtObject", reinterpret_cast<long>(receiver));
443 }
444
445 jboolean result = leScanner.callMethod<jboolean>("scanForLeDevice", true);
446 if (!result) {
447 qCWarning(QT_BT_ANDROID) << "Cannot start BTLE device scanner";
448 m_active = NoScanActive;
449 emit q->finished();
450 return;
451 }
452
453 // wait interval and sum up what was found
454 if (!leScanTimeout) {
455 leScanTimeout = new QTimer(this);
456 leScanTimeout->setSingleShot(true);
457 connect(leScanTimeout, &QTimer::timeout,
458 this, &QBluetoothDeviceDiscoveryAgentPrivate::stopLowEnergyScan);
459 }
460
461 if (lowEnergySearchTimeout > 0) { // otherwise no timeout and stop() required
462 leScanTimeout->setInterval(lowEnergySearchTimeout);
463 leScanTimeout->start();
464 }
465
466 qCDebug(QT_BT_ANDROID)
467 << "QBluetoothDeviceDiscoveryAgentPrivate::start() - Low Energy search successfully started.";
468}
469
470void QBluetoothDeviceDiscoveryAgentPrivate::stopLowEnergyScan()
471{
472 jboolean result = leScanner.callMethod<jboolean>("scanForLeDevice", false);
473 if (!result)
474 qCWarning(QT_BT_ANDROID) << "Cannot stop BTLE device scanner";
475
476 m_active = NoScanActive;
477
478 Q_Q(QBluetoothDeviceDiscoveryAgent);
479 if (leScanTimeout->isActive()) {
480 // still active if this function was called from stop()
481 leScanTimeout->stop();
482 emit q->canceled();
483 } else {
484 // timeout -> regular stop
485 emit q->finished();
486 }
487}
488QT_END_NAMESPACE
static constexpr auto deviceDiscoveryStartTimeLimit
static constexpr short deviceDiscoveryStartMaxAttempts