diff options
Diffstat (limited to 'src/bluetooth/android/devicediscoverybroadcastreceiver.cpp')
-rw-r--r-- | src/bluetooth/android/devicediscoverybroadcastreceiver.cpp | 397 |
1 files changed, 382 insertions, 15 deletions
diff --git a/src/bluetooth/android/devicediscoverybroadcastreceiver.cpp b/src/bluetooth/android/devicediscoverybroadcastreceiver.cpp index a3d17b90..9c9c0409 100644 --- a/src/bluetooth/android/devicediscoverybroadcastreceiver.cpp +++ b/src/bluetooth/android/devicediscoverybroadcastreceiver.cpp @@ -39,12 +39,16 @@ ****************************************************************************/ #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 @@ -52,14 +56,208 @@ 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) -// class name +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 }, + { Q_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 }, + { Q_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 }, + { Q_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 }, + { Q_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 }, + { Q_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 }, + { Q_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 }, + { Q_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 }, + { Q_NULLPTR, 0 }, // separator + + // the following entries do not exist on Android + // we map them to the unknown minor version case + // QBluetoothDevice::Miscellaneous + { Q_NULLPTR, 0 }, // index 61 & separator + + // QBluetoothDevice::LANAccessDevice + { Q_NULLPTR, 0 }, // index 62 & separator + + // QBluetoothDevice::ImagingDevice + { Q_NULLPTR, 0 }, // index 63 & separator + + // QBluetoothDevice::UncategorizedDevice + { Q_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, + // .. 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); @@ -86,6 +284,10 @@ QBluetoothDeviceInfo::CoreConfigurations qtBtTypeForJavaBtType(jint javaType) cachedBtTypes()->insert(javaType, QBluetoothDeviceInfo::UnknownCoreConfiguration); } else { + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } qCWarning(QT_BT_ANDROID) << "Unknown Bluetooth device type value"; } @@ -95,6 +297,106 @@ QBluetoothDeviceInfo::CoreConfigurations qtBtTypeForJavaBtType(jint javaType) } } +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 != Q_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 != Q_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)); @@ -147,19 +449,18 @@ void DeviceDiscoveryBroadcastReceiver::onReceive(JNIEnv *env, jobject context, j // Runs in Java thread void DeviceDiscoveryBroadcastReceiver::onReceiveLeScan( - JNIEnv *env, jobject jBluetoothDevice, jint rssi) + JNIEnv *env, jobject jBluetoothDevice, jint rssi, jbyteArray scanRecord) { - qCDebug(QT_BT_ANDROID) << "DeviceDiscoveryBroadcastReceiver::onReceiveLeScan()"; const QAndroidJniObject bluetoothDevice(jBluetoothDevice); if (!bluetoothDevice.isValid()) return; - const QBluetoothDeviceInfo info = retrieveDeviceInfo(env, bluetoothDevice, rssi); + 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) +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()); @@ -168,10 +469,19 @@ QBluetoothDeviceInfo DeviceDiscoveryBroadcastReceiver::retrieveDeviceInfo(JNIEnv "()Landroid/bluetooth/BluetoothClass;"); if (!bluetoothClass.isValid()) return QBluetoothDeviceInfo(); - int classType = bluetoothClass.callMethod<jint>("getDeviceClass"); + QBluetoothDeviceInfo::MajorDeviceClass majorClass = resolveAndroidMajorClass( + bluetoothClass.callMethod<jint>("getMajorDeviceClass")); + // major device class is 5 bits from index 8 - 12 + quint32 classType = ((quint32(majorClass) & 0x1f) << 8); - static QList<qint32> services; + 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 @@ -182,22 +492,79 @@ QBluetoothDeviceInfo DeviceDiscoveryBroadcastReceiver::retrieveDeviceInfo(JNIEnv << QBluetoothDeviceInfo::TelephonyService << QBluetoothDeviceInfo::InformationService; - //Matching BluetoothClass.Service values - qint32 result = 0; - qint32 current = 0; + // Matching BluetoothClass.Service values + quint32 serviceResult = 0; + quint32 current = 0; for (int i = 0; i < services.count(); i++) { current = services.at(i); - int id = (current << 16); - if (bluetoothClass.callMethod<jboolean>("hasService", "(I)Z", id)) - result |= current; + int androidId = (current << 16); // Android values shift by 2 bytes compared to Qt enums + if (bluetoothClass.callMethod<jboolean>("hasService", "(I)Z", androidId)) + serviceResult |= current; } - result = result << 13; - classType |= result; + // 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); + + QList<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; + 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, QBluetoothDeviceInfo::DataIncomplete); + + env->ReleaseByteArrayElements(scanRecord, elems, JNI_ABORT); + } + if (QtAndroidPrivate::androidSdkVersion() >= 18) { jint javaBtType = bluetoothDevice.callMethod<jint>("getType"); |