| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 Lauri Laanmets (Proekspert AS) <lauri.laanmets@eesti.ee> |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtBluetooth module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "android/devicediscoverybroadcastreceiver_p.h" |
| #include <QtCore/QtEndian> |
| #include <QtCore/QLoggingCategory> |
| #include <QtBluetooth/QBluetoothAddress> |
| #include <QtBluetooth/QBluetoothDeviceInfo> |
| #include <QtBluetooth/QBluetoothUuid> |
| #include "android/jni_android_p.h" |
| #include <QtCore/private/qjnihelpers_p.h> |
| #include <QtCore/QHash> |
| #include <QtCore/qbitarray.h> |
| #include <algorithm> |
| |
| QT_BEGIN_NAMESPACE |
| |
| Q_DECLARE_LOGGING_CATEGORY(QT_BT_ANDROID) |
| |
| typedef QHash<jint, QBluetoothDeviceInfo::CoreConfigurations> JCachedBtTypes; |
| Q_GLOBAL_STATIC(JCachedBtTypes, cachedBtTypes) |
| typedef QHash<jint, QBluetoothDeviceInfo::MajorDeviceClass> JCachedMajorTypes; |
| Q_GLOBAL_STATIC(JCachedMajorTypes, cachedMajorTypes) |
| |
| typedef QHash<jint, quint8> JCachedMinorTypes; |
| Q_GLOBAL_STATIC(JCachedMinorTypes, cachedMinorTypes) |
| |
| static QBitArray initializeMinorCaches() |
| { |
| const int numberOfMajorDeviceClasses = 11; // count QBluetoothDeviceInfo::MajorDeviceClass values |
| |
| // switch below used to ensure that we notice additions to MajorDeviceClass enum |
| const QBluetoothDeviceInfo::MajorDeviceClass classes = QBluetoothDeviceInfo::ComputerDevice; |
| switch (classes) { |
| case QBluetoothDeviceInfo::MiscellaneousDevice: |
| case QBluetoothDeviceInfo::ComputerDevice: |
| case QBluetoothDeviceInfo::PhoneDevice: |
| case QBluetoothDeviceInfo::LANAccessDevice: |
| case QBluetoothDeviceInfo::AudioVideoDevice: |
| case QBluetoothDeviceInfo::PeripheralDevice: |
| case QBluetoothDeviceInfo::ImagingDevice: |
| case QBluetoothDeviceInfo::WearableDevice: |
| case QBluetoothDeviceInfo::ToyDevice: |
| case QBluetoothDeviceInfo::HealthDevice: |
| case QBluetoothDeviceInfo::UncategorizedDevice: |
| break; |
| default: |
| qCWarning(QT_BT_ANDROID) << "Unknown category of major device class:" << classes; |
| } |
| |
| return QBitArray(numberOfMajorDeviceClasses, false); |
| } |
| |
| Q_GLOBAL_STATIC_WITH_ARGS(QBitArray, initializedCacheTracker, (initializeMinorCaches())) |
| |
| |
| // class names |
| static const char * const javaBluetoothDeviceClassName = "android/bluetooth/BluetoothDevice"; |
| static const char * const javaBluetoothClassDeviceMajorClassName = "android/bluetooth/BluetoothClass$Device$Major"; |
| static const char * const javaBluetoothClassDeviceClassName = "android/bluetooth/BluetoothClass$Device"; |
| |
| // field names device type (LE vs classic) |
| static const char * const javaDeviceTypeClassic = "DEVICE_TYPE_CLASSIC"; |
| static const char * const javaDeviceTypeDual = "DEVICE_TYPE_DUAL"; |
| static const char * const javaDeviceTypeLE = "DEVICE_TYPE_LE"; |
| static const char * const javaDeviceTypeUnknown = "DEVICE_TYPE_UNKNOWN"; |
| |
| struct MajorClassJavaToQtMapping |
| { |
| char const * javaFieldName; |
| QBluetoothDeviceInfo::MajorDeviceClass qtMajor; |
| }; |
| |
| static const MajorClassJavaToQtMapping majorMappings[] = { |
| { "AUDIO_VIDEO", QBluetoothDeviceInfo::AudioVideoDevice }, |
| { "COMPUTER", QBluetoothDeviceInfo::ComputerDevice }, |
| { "HEALTH", QBluetoothDeviceInfo::HealthDevice }, |
| { "IMAGING", QBluetoothDeviceInfo::ImagingDevice }, |
| { "MISC", QBluetoothDeviceInfo::MiscellaneousDevice }, |
| { "NETWORKING", QBluetoothDeviceInfo::LANAccessDevice }, |
| { "PERIPHERAL", QBluetoothDeviceInfo::PeripheralDevice }, |
| { "PHONE", QBluetoothDeviceInfo::PhoneDevice }, |
| { "TOY", QBluetoothDeviceInfo::ToyDevice }, |
| { "UNCATEGORIZED", QBluetoothDeviceInfo::UncategorizedDevice }, |
| { "WEARABLE", QBluetoothDeviceInfo::WearableDevice }, |
| { nullptr, QBluetoothDeviceInfo::UncategorizedDevice } //end of list |
| }; |
| |
| // QBluetoothDeviceInfo::MajorDeviceClass value plus 1 matches index |
| // UncategorizedDevice shifts to index 0 |
| static const int minorIndexSizes[] = { |
| 64, // QBluetoothDevice::UncategorizedDevice |
| 61, // QBluetoothDevice::MiscellaneousDevice |
| 18, // QBluetoothDevice::ComputerDevice |
| 35, // QBluetoothDevice::PhoneDevice |
| 62, // QBluetoothDevice::LANAccessDevice |
| 0, // QBluetoothDevice::AudioVideoDevice |
| 56, // QBluetoothDevice::PeripheralDevice |
| 63, // QBluetoothDevice::ImagingDEvice |
| 49, // QBluetoothDevice::WearableDevice |
| 42, // QBluetoothDevice::ToyDevice |
| 26, // QBluetoothDevice::HealthDevice |
| }; |
| |
| struct MinorClassJavaToQtMapping |
| { |
| char const * javaFieldName; |
| quint8 qtMinor; |
| }; |
| |
| static const MinorClassJavaToQtMapping minorMappings[] = { |
| // QBluetoothDevice::AudioVideoDevice -> 17 entries |
| { "AUDIO_VIDEO_CAMCORDER", QBluetoothDeviceInfo::Camcorder }, //index 0 |
| { "AUDIO_VIDEO_CAR_AUDIO", QBluetoothDeviceInfo::CarAudio }, |
| { "AUDIO_VIDEO_HANDSFREE", QBluetoothDeviceInfo::HandsFreeDevice }, |
| { "AUDIO_VIDEO_HEADPHONES", QBluetoothDeviceInfo::Headphones }, |
| { "AUDIO_VIDEO_HIFI_AUDIO", QBluetoothDeviceInfo::HiFiAudioDevice }, |
| { "AUDIO_VIDEO_LOUDSPEAKER", QBluetoothDeviceInfo::Loudspeaker }, |
| { "AUDIO_VIDEO_MICROPHONE", QBluetoothDeviceInfo::Microphone }, |
| { "AUDIO_VIDEO_PORTABLE_AUDIO", QBluetoothDeviceInfo::PortableAudioDevice }, |
| { "AUDIO_VIDEO_SET_TOP_BOX", QBluetoothDeviceInfo::SetTopBox }, |
| { "AUDIO_VIDEO_UNCATEGORIZED", QBluetoothDeviceInfo::UncategorizedAudioVideoDevice }, |
| { "AUDIO_VIDEO_VCR", QBluetoothDeviceInfo::Vcr }, |
| { "AUDIO_VIDEO_VIDEO_CAMERA", QBluetoothDeviceInfo::VideoCamera }, |
| { "AUDIO_VIDEO_VIDEO_CONFERENCING", QBluetoothDeviceInfo::VideoConferencing }, |
| { "AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER", QBluetoothDeviceInfo::VideoDisplayAndLoudspeaker }, |
| { "AUDIO_VIDEO_VIDEO_GAMING_TOY", QBluetoothDeviceInfo::GamingDevice }, |
| { "AUDIO_VIDEO_VIDEO_MONITOR", QBluetoothDeviceInfo::VideoMonitor }, |
| { "AUDIO_VIDEO_WEARABLE_HEADSET", QBluetoothDeviceInfo::WearableHeadsetDevice }, |
| { nullptr, 0 }, // separator |
| |
| // QBluetoothDevice::ComputerDevice -> 7 entries |
| { "COMPUTER_UNCATEGORIZED", QBluetoothDeviceInfo::UncategorizedComputer }, // index 18 |
| { "COMPUTER_DESKTOP", QBluetoothDeviceInfo::DesktopComputer }, |
| { "COMPUTER_HANDHELD_PC_PDA", QBluetoothDeviceInfo::HandheldComputer }, |
| { "COMPUTER_LAPTOP", QBluetoothDeviceInfo::LaptopComputer }, |
| { "COMPUTER_PALM_SIZE_PC_PDA", QBluetoothDeviceInfo::HandheldClamShellComputer }, |
| { "COMPUTER_SERVER", QBluetoothDeviceInfo::ServerComputer }, |
| { "COMPUTER_WEARABLE", QBluetoothDeviceInfo::WearableComputer }, |
| { nullptr, 0 }, // separator |
| |
| // QBluetoothDevice::HealthDevice -> 8 entries |
| { "HEALTH_BLOOD_PRESSURE", QBluetoothDeviceInfo::HealthBloodPressureMonitor }, // index 26 |
| { "HEALTH_DATA_DISPLAY", QBluetoothDeviceInfo::HealthDataDisplay }, |
| { "HEALTH_GLUCOSE", QBluetoothDeviceInfo::HealthGlucoseMeter }, |
| { "HEALTH_PULSE_OXIMETER", QBluetoothDeviceInfo::HealthPulseOximeter }, |
| { "HEALTH_PULSE_RATE", QBluetoothDeviceInfo::HealthStepCounter }, |
| { "HEALTH_THERMOMETER", QBluetoothDeviceInfo::HealthThermometer }, |
| { "HEALTH_UNCATEGORIZED", QBluetoothDeviceInfo::UncategorizedHealthDevice }, |
| { "HEALTH_WEIGHING", QBluetoothDeviceInfo::HealthWeightScale }, |
| { nullptr, 0 }, // separator |
| |
| // QBluetoothDevice::PhoneDevice -> 6 entries |
| { "PHONE_CELLULAR", QBluetoothDeviceInfo::CellularPhone }, // index 35 |
| { "PHONE_CORDLESS", QBluetoothDeviceInfo::CordlessPhone }, |
| { "PHONE_ISDN", QBluetoothDeviceInfo::CommonIsdnAccessPhone }, |
| { "PHONE_MODEM_OR_GATEWAY", QBluetoothDeviceInfo::WiredModemOrVoiceGatewayPhone }, |
| { "PHONE_SMART", QBluetoothDeviceInfo::SmartPhone }, |
| { "PHONE_UNCATEGORIZED", QBluetoothDeviceInfo::UncategorizedPhone }, |
| { nullptr, 0 }, // separator |
| |
| // QBluetoothDevice::ToyDevice -> 6 entries |
| { "TOY_CONTROLLER", QBluetoothDeviceInfo::ToyController }, // index 42 |
| { "TOY_DOLL_ACTION_FIGURE", QBluetoothDeviceInfo::ToyDoll }, |
| { "TOY_GAME", QBluetoothDeviceInfo::ToyGame }, |
| { "TOY_ROBOT", QBluetoothDeviceInfo::ToyRobot }, |
| { "TOY_UNCATEGORIZED", QBluetoothDeviceInfo::UncategorizedToy }, |
| { "TOY_VEHICLE", QBluetoothDeviceInfo::ToyVehicle }, |
| { nullptr, 0 }, // separator |
| |
| // QBluetoothDevice::WearableDevice -> 6 entries |
| { "WEARABLE_GLASSES", QBluetoothDeviceInfo::WearableGlasses }, // index 49 |
| { "WEARABLE_HELMET", QBluetoothDeviceInfo::WearableHelmet }, |
| { "WEARABLE_JACKET", QBluetoothDeviceInfo::WearableJacket }, |
| { "WEARABLE_PAGER", QBluetoothDeviceInfo::WearablePager }, |
| { "WEARABLE_UNCATEGORIZED", QBluetoothDeviceInfo::UncategorizedWearableDevice }, |
| { "WEARABLE_WRIST_WATCH", QBluetoothDeviceInfo::WearableWristWatch }, |
| { nullptr, 0 }, // separator |
| |
| // QBluetoothDevice::PeripheralDevice -> 3 entries |
| // For some reason these are not mentioned in Android docs but still exist |
| { "PERIPHERAL_NON_KEYBOARD_NON_POINTING", QBluetoothDeviceInfo::UncategorizedPeripheral }, // index 56 |
| { "PERIPHERAL_KEYBOARD", QBluetoothDeviceInfo::KeyboardPeripheral }, |
| { "PERIPHERAL_POINTING", QBluetoothDeviceInfo::PointingDevicePeripheral }, |
| { "PERIPHERAL_KEYBOARD_POINTING", QBluetoothDeviceInfo::KeyboardWithPointingDevicePeripheral }, |
| { nullptr, 0 }, // separator |
| |
| // the following entries do not exist on Android |
| // we map them to the unknown minor version case |
| // QBluetoothDevice::Miscellaneous |
| { nullptr, 0 }, // index 61 & separator |
| |
| // QBluetoothDevice::LANAccessDevice |
| { nullptr, 0 }, // index 62 & separator |
| |
| // QBluetoothDevice::ImagingDevice |
| { nullptr, 0 }, // index 63 & separator |
| |
| // QBluetoothDevice::UncategorizedDevice |
| { nullptr, 0 }, // index 64 & separator |
| }; |
| |
| /* Advertising Data Type (AD type) for LE scan records, as defined in Bluetooth CSS v6. */ |
| enum ADType { |
| ADType16BitUuidIncomplete = 0x02, |
| ADType16BitUuidComplete = 0x03, |
| ADType32BitUuidIncomplete = 0x04, |
| ADType32BitUuidComplete = 0x05, |
| ADType128BitUuidIncomplete = 0x06, |
| ADType128BitUuidComplete = 0x07, |
| ADTypeManufacturerSpecificData = 0xff, |
| // .. more will be added when required |
| }; |
| |
| // Endianness conversion for quint128 doesn't (yet) exist in qtendian.h |
| template <> |
| inline quint128 qbswap<quint128>(const quint128 src) |
| { |
| quint128 dst; |
| for (int i = 0; i < 16; i++) |
| dst.data[i] = src.data[15 - i]; |
| return dst; |
| } |
| |
| QBluetoothDeviceInfo::CoreConfigurations qtBtTypeForJavaBtType(jint javaType) |
| { |
| const JCachedBtTypes::iterator it = cachedBtTypes()->find(javaType); |
| if (it == cachedBtTypes()->end()) { |
| QAndroidJniEnvironment env; |
| |
| if (javaType == QAndroidJniObject::getStaticField<jint>( |
| javaBluetoothDeviceClassName, javaDeviceTypeClassic)) { |
| cachedBtTypes()->insert(javaType, |
| QBluetoothDeviceInfo::BaseRateCoreConfiguration); |
| return QBluetoothDeviceInfo::BaseRateCoreConfiguration; |
| } else if (javaType == QAndroidJniObject::getStaticField<jint>( |
| javaBluetoothDeviceClassName, javaDeviceTypeLE)) { |
| cachedBtTypes()->insert(javaType, |
| QBluetoothDeviceInfo::LowEnergyCoreConfiguration); |
| return QBluetoothDeviceInfo::LowEnergyCoreConfiguration; |
| } else if (javaType == QAndroidJniObject::getStaticField<jint>( |
| javaBluetoothDeviceClassName, javaDeviceTypeDual)) { |
| cachedBtTypes()->insert(javaType, |
| QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration); |
| return QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration; |
| } else if (javaType == QAndroidJniObject::getStaticField<jint>( |
| javaBluetoothDeviceClassName, javaDeviceTypeUnknown)) { |
| cachedBtTypes()->insert(javaType, |
| QBluetoothDeviceInfo::UnknownCoreConfiguration); |
| } else { |
| if (env->ExceptionCheck()) { |
| env->ExceptionDescribe(); |
| env->ExceptionClear(); |
| } |
| qCWarning(QT_BT_ANDROID) << "Unknown Bluetooth device type value"; |
| } |
| |
| return QBluetoothDeviceInfo::UnknownCoreConfiguration; |
| } else { |
| return it.value(); |
| } |
| } |
| |
| QBluetoothDeviceInfo::MajorDeviceClass resolveAndroidMajorClass(jint javaType) |
| { |
| QAndroidJniEnvironment env; |
| |
| const JCachedMajorTypes::iterator it = cachedMajorTypes()->find(javaType); |
| if (it == cachedMajorTypes()->end()) { |
| QAndroidJniEnvironment env; |
| // precache all major device class fields |
| int i = 0; |
| jint fieldValue; |
| QBluetoothDeviceInfo::MajorDeviceClass result = QBluetoothDeviceInfo::UncategorizedDevice; |
| while (majorMappings[i].javaFieldName != nullptr) { |
| fieldValue = QAndroidJniObject::getStaticField<jint>( |
| javaBluetoothClassDeviceMajorClassName, majorMappings[i].javaFieldName); |
| if (env->ExceptionCheck()) { |
| qCWarning(QT_BT_ANDROID) << "Unknown BluetoothClass.Device.Major field" << javaType; |
| env->ExceptionDescribe(); |
| env->ExceptionClear(); |
| |
| // add fallback value because field not readable |
| cachedMajorTypes()->insert(javaType, QBluetoothDeviceInfo::UncategorizedDevice); |
| } else { |
| cachedMajorTypes()->insert(fieldValue, majorMappings[i].qtMajor); |
| } |
| |
| if (fieldValue == javaType) |
| result = majorMappings[i].qtMajor; |
| |
| i++; |
| } |
| |
| return result; |
| } else { |
| return it.value(); |
| } |
| } |
| |
| /* |
| The index for major into the MinorClassJavaToQtMapping and initializedCacheTracker |
| is major+1 except for UncategorizedDevice which is at index 0. |
| */ |
| int mappingIndexForMajor(QBluetoothDeviceInfo::MajorDeviceClass major) |
| { |
| int mappingIndex = (int) major; |
| if (major == QBluetoothDeviceInfo::UncategorizedDevice) |
| mappingIndex = 0; |
| else |
| mappingIndex++; |
| |
| Q_ASSERT(mappingIndex >=0 |
| && mappingIndex <= (QBluetoothDeviceInfo::HealthDevice + 1)); |
| |
| return mappingIndex; |
| } |
| |
| void triggerCachingOfMinorsForMajor(QBluetoothDeviceInfo::MajorDeviceClass major) |
| { |
| //qCDebug(QT_BT_ANDROID) << "Caching minor values for major" << major; |
| int mappingIndex = mappingIndexForMajor(major); |
| int sizeIndex = minorIndexSizes[mappingIndex]; |
| QAndroidJniEnvironment env; |
| |
| while (minorMappings[sizeIndex].javaFieldName != nullptr) { |
| jint fieldValue = QAndroidJniObject::getStaticField<jint>( |
| javaBluetoothClassDeviceClassName, minorMappings[sizeIndex].javaFieldName); |
| if (env->ExceptionCheck()) { // field lookup failed? skip it |
| env->ExceptionDescribe(); |
| env->ExceptionClear(); |
| } |
| |
| Q_ASSERT(fieldValue >= 0); |
| cachedMinorTypes()->insert(fieldValue, minorMappings[sizeIndex].qtMinor); |
| sizeIndex++; |
| } |
| |
| initializedCacheTracker()->setBit(mappingIndex); |
| } |
| |
| quint8 resolveAndroidMinorClass(QBluetoothDeviceInfo::MajorDeviceClass major, jint javaMinor) |
| { |
| // there are no minor device classes in java with value 0 |
| //qCDebug(QT_BT_ANDROID) << "received minor class device:" << javaMinor; |
| if (javaMinor == 0) |
| return 0; |
| |
| int mappingIndex = mappingIndexForMajor(major); |
| |
| // whenever we encounter a not yet seen major device class |
| // we populate the cache with all its related minor values |
| if (!initializedCacheTracker()->at(mappingIndex)) |
| triggerCachingOfMinorsForMajor(major); |
| |
| const JCachedMinorTypes::iterator it = cachedMinorTypes()->find(javaMinor); |
| if (it == cachedMinorTypes()->end()) |
| return 0; |
| else |
| return it.value(); |
| } |
| |
| |
| DeviceDiscoveryBroadcastReceiver::DeviceDiscoveryBroadcastReceiver(QObject* parent): AndroidBroadcastReceiver(parent) |
| { |
| addAction(valueForStaticField(JavaNames::BluetoothDevice, JavaNames::ActionFound)); |
| addAction(valueForStaticField(JavaNames::BluetoothAdapter, JavaNames::ActionDiscoveryStarted)); |
| addAction(valueForStaticField(JavaNames::BluetoothAdapter, JavaNames::ActionDiscoveryFinished)); |
| } |
| |
| // Runs in Java thread |
| void DeviceDiscoveryBroadcastReceiver::onReceive(JNIEnv *env, jobject context, jobject intent) |
| { |
| Q_UNUSED(context); |
| Q_UNUSED(env); |
| |
| QAndroidJniObject intentObject(intent); |
| const QString action = intentObject.callObjectMethod("getAction", "()Ljava/lang/String;").toString(); |
| |
| qCDebug(QT_BT_ANDROID) << "DeviceDiscoveryBroadcastReceiver::onReceive() - event:" << action; |
| |
| if (action == valueForStaticField(JavaNames::BluetoothAdapter, |
| JavaNames::ActionDiscoveryFinished).toString()) { |
| emit finished(); |
| } else if (action == valueForStaticField(JavaNames::BluetoothAdapter, |
| JavaNames::ActionDiscoveryStarted).toString()) { |
| |
| } else if (action == valueForStaticField(JavaNames::BluetoothDevice, |
| JavaNames::ActionFound).toString()) { |
| //get BluetoothDevice |
| QAndroidJniObject keyExtra = valueForStaticField(JavaNames::BluetoothDevice, |
| JavaNames::ExtraDevice); |
| const QAndroidJniObject bluetoothDevice = |
| intentObject.callObjectMethod("getParcelableExtra", |
| "(Ljava/lang/String;)Landroid/os/Parcelable;", |
| keyExtra.object<jstring>()); |
| |
| if (!bluetoothDevice.isValid()) |
| return; |
| |
| keyExtra = valueForStaticField(JavaNames::BluetoothDevice, |
| JavaNames::ExtraRssi); |
| int rssi = intentObject.callMethod<jshort>("getShortExtra", |
| "(Ljava/lang/String;S)S", |
| keyExtra.object<jstring>(), |
| 0); |
| |
| const QBluetoothDeviceInfo info = retrieveDeviceInfo(env, bluetoothDevice, rssi); |
| if (info.isValid()) |
| emit deviceDiscovered(info, false); |
| } |
| } |
| |
| // Runs in Java thread |
| void DeviceDiscoveryBroadcastReceiver::onReceiveLeScan( |
| JNIEnv *env, jobject jBluetoothDevice, jint rssi, jbyteArray scanRecord) |
| { |
| const QAndroidJniObject bluetoothDevice(jBluetoothDevice); |
| if (!bluetoothDevice.isValid()) |
| return; |
| |
| const QBluetoothDeviceInfo info = retrieveDeviceInfo(env, bluetoothDevice, rssi, scanRecord); |
| if (info.isValid()) |
| emit deviceDiscovered(info, true); |
| } |
| |
| QBluetoothDeviceInfo DeviceDiscoveryBroadcastReceiver::retrieveDeviceInfo(JNIEnv *env, const QAndroidJniObject &bluetoothDevice, int rssi, jbyteArray scanRecord) |
| { |
| const QString deviceName = bluetoothDevice.callObjectMethod<jstring>("getName").toString(); |
| const QBluetoothAddress deviceAddress(bluetoothDevice.callObjectMethod<jstring>("getAddress").toString()); |
| |
| const QAndroidJniObject bluetoothClass = bluetoothDevice.callObjectMethod("getBluetoothClass", |
| "()Landroid/bluetooth/BluetoothClass;"); |
| if (!bluetoothClass.isValid()) |
| return QBluetoothDeviceInfo(); |
| |
| QBluetoothDeviceInfo::MajorDeviceClass majorClass = resolveAndroidMajorClass( |
| bluetoothClass.callMethod<jint>("getMajorDeviceClass")); |
| // major device class is 5 bits from index 8 - 12 |
| quint32 classType = ((quint32(majorClass) & 0x1f) << 8); |
| |
| jint javaMinor = bluetoothClass.callMethod<jint>("getDeviceClass"); |
| quint8 minorDeviceType = resolveAndroidMinorClass(majorClass, javaMinor); |
| |
| // minor device class is 6 bits from index 2 - 7 |
| classType |= ((quint32(minorDeviceType) & 0x3f) << 2); |
| |
| static QList<quint32> services; |
| if (services.count() == 0) |
| services << QBluetoothDeviceInfo::PositioningService |
| << QBluetoothDeviceInfo::NetworkingService |
| << QBluetoothDeviceInfo::RenderingService |
| << QBluetoothDeviceInfo::CapturingService |
| << QBluetoothDeviceInfo::ObjectTransferService |
| << QBluetoothDeviceInfo::AudioService |
| << QBluetoothDeviceInfo::TelephonyService |
| << QBluetoothDeviceInfo::InformationService; |
| |
| // Matching BluetoothClass.Service values |
| quint32 serviceResult = 0; |
| quint32 current = 0; |
| for (int i = 0; i < services.count(); i++) { |
| current = services.at(i); |
| int androidId = (current << 16); // Android values shift by 2 bytes compared to Qt enums |
| if (bluetoothClass.callMethod<jboolean>("hasService", "(I)Z", androidId)) |
| serviceResult |= current; |
| } |
| |
| // service class info is 11 bits from index 13 - 23 |
| classType |= (serviceResult << 13); |
| |
| QBluetoothDeviceInfo info(deviceAddress, deviceName, classType); |
| info.setRssi(rssi); |
| |
| if (scanRecord != nullptr) { |
| // Parse scan record |
| jboolean isCopy; |
| jbyte *elems = env->GetByteArrayElements(scanRecord, &isCopy); |
| const char *scanRecordBuffer = reinterpret_cast<const char *>(elems); |
| const int scanRecordLength = env->GetArrayLength(scanRecord); |
| |
| QVector<QBluetoothUuid> serviceUuids; |
| int i = 0; |
| |
| // Spec 4.2, Vol 3, Part C, Chapter 11 |
| while (i < scanRecordLength) { |
| // sizeof(EIR Data) = sizeof(Length) + sizeof(EIR data Type) + sizeof(EIR Data) |
| // Length = sizeof(EIR data Type) + sizeof(EIR Data) |
| |
| const int nBytes = scanRecordBuffer[i]; |
| if (nBytes == 0) |
| break; |
| |
| if ((i + nBytes) >= scanRecordLength) |
| break; |
| |
| const int adType = scanRecordBuffer[i+1]; |
| const char *dataPtr = &scanRecordBuffer[i+2]; |
| QBluetoothUuid foundService; |
| |
| switch (adType) { |
| case ADType16BitUuidIncomplete: |
| case ADType16BitUuidComplete: |
| foundService = QBluetoothUuid(qFromLittleEndian<quint16>(dataPtr)); |
| break; |
| case ADType32BitUuidIncomplete: |
| case ADType32BitUuidComplete: |
| foundService = QBluetoothUuid(qFromLittleEndian<quint32>(dataPtr)); |
| break; |
| case ADType128BitUuidIncomplete: |
| case ADType128BitUuidComplete: |
| foundService = |
| QBluetoothUuid(qToBigEndian<quint128>(qFromLittleEndian<quint128>(dataPtr))); |
| break; |
| case ADTypeManufacturerSpecificData: |
| if (nBytes >= 3) { |
| info.setManufacturerData(qFromLittleEndian<quint16>(dataPtr), |
| QByteArray(dataPtr + 2, nBytes - 3)); |
| } |
| break; |
| default: |
| // no other types supported yet and therefore skipped |
| // https://www.bluetooth.org/en-us/specification/assigned-numbers/generic-access-profile |
| break; |
| } |
| |
| i += nBytes + 1; |
| |
| if (!foundService.isNull() && !serviceUuids.contains(foundService)) |
| serviceUuids.append(foundService); |
| } |
| |
| info.setServiceUuids(serviceUuids); |
| |
| env->ReleaseByteArrayElements(scanRecord, elems, JNI_ABORT); |
| } |
| |
| if (QtAndroidPrivate::androidSdkVersion() >= 18) { |
| jint javaBtType = bluetoothDevice.callMethod<jint>("getType"); |
| |
| if (env->ExceptionCheck()) { |
| env->ExceptionDescribe(); |
| env->ExceptionClear(); |
| } else { |
| info.setCoreConfigurations(qtBtTypeForJavaBtType(javaBtType)); |
| } |
| } |
| |
| return info; |
| } |
| |
| QT_END_NAMESPACE |
| |