From 5a7507a93ee1887dd55da7aff772e1ccc495511a Mon Sep 17 00:00:00 2001 From: Markus Bullmann Date: Tue, 2 Apr 2019 18:48:48 +0200 Subject: [PATCH] Added Android UWB code --- Manager.cpp | 164 +- Manager.h | 25 + RTT.pro | 80 +- _android/AndroidManifest.xml | 72 +- _android/build.gradle | 65 + _android/gradle.properties | 18 + _android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + _android/gradlew | 172 + _android/gradlew.bat | 84 + _android/res/values/libs.xml | 25 + _android/src/DecaManager.java | 107 + _android/src/DecaManagerCallbacks.java | 8 + _android/src/RTT.java | 39 +- _android/src/UWB.java | 174 + .../no/nordicsemi/android/ble/BleManager.java | 3650 +++++++++++++++++ .../android/ble/BleManagerCallbacks.java | 206 + .../android/ble/ConnectRequest.java | 246 ++ .../ble/ConnectionPriorityRequest.java | 160 + .../android/ble/DisconnectRequest.java | 80 + .../ble/MainThreadBluetoothGattCallback.java | 201 + .../no/nordicsemi/android/ble/MtuRequest.java | 98 + .../no/nordicsemi/android/ble/Operation.java | 32 + .../no/nordicsemi/android/ble/PhyRequest.java | 165 + .../nordicsemi/android/ble/ReadRequest.java | 249 ++ .../android/ble/ReadRssiRequest.java | 88 + .../android/ble/ReliableWriteRequest.java | 134 + .../no/nordicsemi/android/ble/Request.java | 896 ++++ .../nordicsemi/android/ble/RequestQueue.java | 164 + .../nordicsemi/android/ble/SimpleRequest.java | 106 + .../android/ble/SimpleValueRequest.java | 152 + .../nordicsemi/android/ble/SleepRequest.java | 79 + .../android/ble/TimeoutHandler.java | 13 + .../android/ble/TimeoutableRequest.java | 222 + .../android/ble/TimeoutableValueRequest.java | 238 ++ .../android/ble/ValueChangedCallback.java | 137 + .../ble/WaitForValueChangedRequest.java | 400 ++ .../nordicsemi/android/ble/WriteRequest.java | 282 ++ .../ble/annotation/ConnectionPriority.java | 37 + .../ble/annotation/ConnectionState.java | 39 + .../android/ble/annotation/PhyMask.java | 37 + .../android/ble/annotation/PhyOption.java | 37 + .../android/ble/annotation/PhyValue.java | 37 + .../android/ble/annotation/WriteType.java | 38 + .../android/ble/callback/BeforeCallback.java | 37 + .../callback/ConnectionPriorityCallback.java | 66 + .../ble/callback/DataReceivedCallback.java | 42 + .../ble/callback/DataSentCallback.java | 40 + .../android/ble/callback/FailCallback.java | 50 + .../ble/callback/InvalidRequestCallback.java | 32 + .../android/ble/callback/MtuCallback.java | 41 + .../android/ble/callback/PhyCallback.java | 61 + .../ble/callback/ReadProgressCallback.java | 44 + .../android/ble/callback/RssiCallback.java | 40 + .../android/ble/callback/SuccessCallback.java | 37 + .../ble/callback/WriteProgressCallback.java | 43 + .../callback/profile/ProfileDataCallback.java | 45 + .../callback/profile/ProfileReadResponse.java | 88 + .../no/nordicsemi/android/ble/data/Data.java | 470 +++ .../android/ble/data/DataFilter.java | 37 + .../android/ble/data/DataMerger.java | 42 + .../android/ble/data/DataSplitter.java | 46 + .../android/ble/data/DataStream.java | 75 + .../android/ble/data/DefaultMtuSplitter.java | 49 + .../android/ble/data/MutableData.java | 409 ++ .../android/ble/error/GattError.java | 209 + .../exception/BluetoothDisabledException.java | 26 + .../ble/exception/ConnectionException.java | 27 + .../DeviceDisconnectedException.java | 26 + .../ble/exception/InvalidDataException.java | 39 + .../exception/InvalidRequestException.java | 43 + .../ble/exception/RequestFailedException.java | 55 + .../response/ConnectionPriorityResponse.java | 145 + .../android/ble/response/MtuResult.java | 91 + .../android/ble/response/PhyResult.java | 97 + .../android/ble/response/ReadResponse.java | 93 + .../android/ble/response/RssiResult.java | 86 + .../android/ble/response/WriteResponse.java | 88 + .../nordicsemi/android/ble/utils/ILogger.java | 47 + .../android/ble/utils/ParserUtils.java | 53 + main.qml | 181 +- uwb.h | 99 + 82 files changed, 12323 insertions(+), 137 deletions(-) create mode 100644 _android/build.gradle create mode 100644 _android/gradle.properties create mode 100644 _android/gradle/wrapper/gradle-wrapper.jar create mode 100644 _android/gradle/wrapper/gradle-wrapper.properties create mode 100644 _android/gradlew create mode 100644 _android/gradlew.bat create mode 100644 _android/res/values/libs.xml create mode 100644 _android/src/DecaManager.java create mode 100644 _android/src/DecaManagerCallbacks.java create mode 100644 _android/src/UWB.java create mode 100644 _android/src/no/nordicsemi/android/ble/BleManager.java create mode 100644 _android/src/no/nordicsemi/android/ble/BleManagerCallbacks.java create mode 100644 _android/src/no/nordicsemi/android/ble/ConnectRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/ConnectionPriorityRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/DisconnectRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/MainThreadBluetoothGattCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/MtuRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/Operation.java create mode 100644 _android/src/no/nordicsemi/android/ble/PhyRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/ReadRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/ReadRssiRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/ReliableWriteRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/Request.java create mode 100644 _android/src/no/nordicsemi/android/ble/RequestQueue.java create mode 100644 _android/src/no/nordicsemi/android/ble/SimpleRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/SimpleValueRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/SleepRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/TimeoutHandler.java create mode 100644 _android/src/no/nordicsemi/android/ble/TimeoutableRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/TimeoutableValueRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/ValueChangedCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/WaitForValueChangedRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/WriteRequest.java create mode 100644 _android/src/no/nordicsemi/android/ble/annotation/ConnectionPriority.java create mode 100644 _android/src/no/nordicsemi/android/ble/annotation/ConnectionState.java create mode 100644 _android/src/no/nordicsemi/android/ble/annotation/PhyMask.java create mode 100644 _android/src/no/nordicsemi/android/ble/annotation/PhyOption.java create mode 100644 _android/src/no/nordicsemi/android/ble/annotation/PhyValue.java create mode 100644 _android/src/no/nordicsemi/android/ble/annotation/WriteType.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/BeforeCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/ConnectionPriorityCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/DataReceivedCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/DataSentCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/FailCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/InvalidRequestCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/MtuCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/PhyCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/ReadProgressCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/RssiCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/SuccessCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/WriteProgressCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/profile/ProfileDataCallback.java create mode 100644 _android/src/no/nordicsemi/android/ble/callback/profile/ProfileReadResponse.java create mode 100644 _android/src/no/nordicsemi/android/ble/data/Data.java create mode 100644 _android/src/no/nordicsemi/android/ble/data/DataFilter.java create mode 100644 _android/src/no/nordicsemi/android/ble/data/DataMerger.java create mode 100644 _android/src/no/nordicsemi/android/ble/data/DataSplitter.java create mode 100644 _android/src/no/nordicsemi/android/ble/data/DataStream.java create mode 100644 _android/src/no/nordicsemi/android/ble/data/DefaultMtuSplitter.java create mode 100644 _android/src/no/nordicsemi/android/ble/data/MutableData.java create mode 100644 _android/src/no/nordicsemi/android/ble/error/GattError.java create mode 100644 _android/src/no/nordicsemi/android/ble/exception/BluetoothDisabledException.java create mode 100644 _android/src/no/nordicsemi/android/ble/exception/ConnectionException.java create mode 100644 _android/src/no/nordicsemi/android/ble/exception/DeviceDisconnectedException.java create mode 100644 _android/src/no/nordicsemi/android/ble/exception/InvalidDataException.java create mode 100644 _android/src/no/nordicsemi/android/ble/exception/InvalidRequestException.java create mode 100644 _android/src/no/nordicsemi/android/ble/exception/RequestFailedException.java create mode 100644 _android/src/no/nordicsemi/android/ble/response/ConnectionPriorityResponse.java create mode 100644 _android/src/no/nordicsemi/android/ble/response/MtuResult.java create mode 100644 _android/src/no/nordicsemi/android/ble/response/PhyResult.java create mode 100644 _android/src/no/nordicsemi/android/ble/response/ReadResponse.java create mode 100644 _android/src/no/nordicsemi/android/ble/response/RssiResult.java create mode 100644 _android/src/no/nordicsemi/android/ble/response/WriteResponse.java create mode 100644 _android/src/no/nordicsemi/android/ble/utils/ILogger.java create mode 100644 _android/src/no/nordicsemi/android/ble/utils/ParserUtils.java create mode 100644 uwb.h diff --git a/Manager.cpp b/Manager.cpp index 45d1ce3..775c1c8 100644 --- a/Manager.cpp +++ b/Manager.cpp @@ -9,37 +9,56 @@ #include #include #include +#include > #include #include #include +#include + + +#include "uwb.h" const std::string NUC1 = "38:de:ad:6d:77:25"; const std::string NUC2 = "38:de:ad:6d:60:ff"; const std::string NUC3 = "1c:1b:b5:ef:a2:9a"; const std::string NUC4 = "1c:1b:b5:ec:d1:82"; +static long long startTime = 0; static QString GetCurrentTimeForFileName() { auto time = std::time(nullptr); std::stringstream ss; - ss << std::put_time(std::localtime(&time), "%F_%T"); // ISO 8601 without timezone information. + ss << std::put_time(std::localtime(&time), "%Y%m%d_%H%M%S"); auto s = ss.str(); std::replace(s.begin(), s.end(), ':', '-'); return QString::fromStdString(s); } +static long long nowInMsec() +{ + auto now = std::chrono::system_clock::now(); + auto duration = now.time_since_epoch(); + auto millis = std::chrono::duration_cast(duration).count(); + return millis; +} + Manager::Manager() { } + void Manager::trigger() { - QString folder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + "/ftm/"; + QString folder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation) + "/ftm/"; - dataLogger = std::make_shared(folder+"/ftm_"+GetCurrentTimeForFileName()+".txt"); + QString prefix = GetCurrentTimeForFileName(); + + dataLogger = std::make_shared(folder+"/"+prefix+"_ftm.txt"); + uwbLogger = std::make_shared(folder+"/"+prefix+"_uwb.txt"); + gtLogger = std::make_shared(folder+"/"+prefix+"_gt.txt"); if (!dataLogger->exists()) { // create the folder, if necessary @@ -57,10 +76,24 @@ void Manager::trigger() { dataLogger = nullptr; } + if (!uwbLogger->open(QIODevice::ReadWrite)) { + qWarning() << "Failed to create uwb data logger file" << uwbLogger->fileName(); + uwbLogger = nullptr; + } + + if (!gtLogger->open(QIODevice::ReadWrite)) { + qWarning() << "Failed to create gt data logger file" << gtLogger->fileName(); + gtLogger = nullptr; + } + + startTime = nowInMsec(); + #ifdef ANDROID + QAndroidJniObject::callStaticMethod("android/net/wifi/UWB", "start", "()I"); QAndroidJniObject::callStaticMethod("android/net/wifi/RTT", "start", "()I"); + #else //onData("38:de:ad:6d:77:25;FAILED"); @@ -75,21 +108,39 @@ void Manager::trigger() { void Manager::stop() { #ifdef ANDROID QAndroidJniObject::callStaticMethod("android/net/wifi/RTT", "stop", "()I"); + QAndroidJniObject::callStaticMethod("android/net/wifi/UWB", "stop", "()I"); #endif dataLogger->flush(); dataLogger->close(); + + uwbLogger->flush(); + uwbLogger->close(); + + gtLogger->flush(); + gtLogger->close(); +} + + +void Manager::manualCheckpoint() { + qDebug() << "Manual checkpoint"; + + long long timestamp = nowInMsec(); + timestamp -= startTime; + + QTextStream out(gtLogger.get()); + + out << timestamp << endl; } void Manager::onData(std::string str) { qDebug() << QString(str.c_str()); - if (dataLogger) { - dataLogger->write(str.c_str()); - dataLogger->write("\n"); - } + long long timestamp = nowInMsec(); + timestamp -= startTime; + bool successfullMeas = true; std::stringstream lineStream(str); std::string cell; @@ -99,47 +150,111 @@ void Manager::onData(std::string str) { int i = 0; const float alpha = 0.7f; + QTextStream out(dataLogger.get()); + + out << timestamp << ";"; + while (std::getline(lineStream, cell, ';')) { switch(i) { - case 0: { + case 0: { + // success flag + successfullMeas = (cell == "1"); + out << (successfullMeas ? 1 : 0) << ";"; + break; + } + case 1: { + // timestamp; ignore; break; } - case 1: { + case 2: { if(NUC1 == cell) {distIndex = 0;} if(NUC2 == cell) {distIndex = 1;} if(NUC3 == cell) {distIndex = 2;} if(NUC4 == cell) {distIndex = 3;} + + out << QString(cell.c_str()) << ";"; break; } - case 2: { - if ("FAILED" == cell) { - _dist[distIndex] = 0; + case 3: { + if (successfullMeas) { + _dist[distIndex] = std::stoi(cell) + _offset; + //_dist[distIndex] = _dist[distIndex] * alpha + atoi(cell.c_str()) * (1-alpha); } else { - //_dist[distIndex] = std::stoi(cell); - _dist[distIndex] = _dist[distIndex] * alpha + atoi(cell.c_str()) * (1-alpha); + _dist[distIndex] = 0; } + + out << _dist[distIndex] << ";"; break; } - case 3: { - _stdDev[distIndex] = atoi(cell.c_str()); + case 4: { + if (successfullMeas) { + _stdDev[distIndex] = atoi(cell.c_str()); + } else { + _stdDev[distIndex] = 0; + } + + out << _stdDev[distIndex] << ";"; break; } - case 4: { + case 5: { // RSSI - } + out << cell.c_str() << ";"; + break; + } } ++i; } - emit distChanged(); + out << endl; + +// if (dataLogger && successfullMeas) { +// dataLogger->write(str.c_str()); +// dataLogger->write("\n"); +// } + + emit distChanged(); +} + +void Manager::onUWBData(std::vector data) { + qDebug() << "Received uwb data " << data; + + long long timestamp = nowInMsec(); + timestamp -= startTime; + + UwbResult uwbResult = uwb_parse(data.data()); + + _uwbDist[0] = 0; + _uwbDist[1] = 0; + _uwbDist[2] = 0; + _uwbDist[3] = 0; + + for (UwbDistance& dist : uwbResult.distances) { + qDebug() << dist.nodeID; + + switch (dist.nodeID) { + case 0x1D8C: _uwbDist[0] = static_cast(dist.distance); break; + case 0x47A7: _uwbDist[1] = static_cast(dist.distance); break; + case 0x863B: _uwbDist[2] = static_cast(dist.distance); break; + case 0x58B4: _uwbDist[3] = static_cast(dist.distance); break; + } + + if (uwbLogger) { + + QTextStream out(uwbLogger.get()); + + out << timestamp << ";" << "1;" << "DW" << hex << dist.nodeID << dec << ";" << dist.distance << endl; + } + } + + emit distChanged(); } Manager mgmt; @@ -156,5 +271,16 @@ extern "C" { mgmt.onData(str); } + JNIEXPORT void JNICALL Java_android_net_wifi_UWB_onUWBComplete(JNIEnv* env, jobject jobj, jbyteArray arrayID) { + (void) env; (void) jobj; + jsize length = env->GetArrayLength(arrayID); + jbyte* data = env->GetByteArrayElements(arrayID, 0); + std::vector c_data; + c_data.assign(reinterpret_cast(data), reinterpret_cast(data)+length); + env->ReleaseByteArrayElements(arrayID, data, JNI_ABORT); + + mgmt.onUWBData(c_data); + } + } #endif diff --git a/Manager.h b/Manager.h index c24d1bf..6f29d03 100644 --- a/Manager.h +++ b/Manager.h @@ -12,8 +12,13 @@ private: float _dist[4]; float _stdDev[4]; + float _offset = 500; + + float _uwbDist[4]; std::shared_ptr dataLogger; + std::shared_ptr uwbLogger; + std::shared_ptr gtLogger; public: @@ -27,11 +32,22 @@ public: Q_PROPERTY(float stdDev3 READ getStdDev3() NOTIFY distChanged) Q_PROPERTY(float stdDev4 READ getStdDev4() NOTIFY distChanged) + // FTM offset + Q_PROPERTY(float offset READ getOffset() WRITE setOffset() NOTIFY offsetChanged) + + Q_PROPERTY(float uwbDist1 READ getUwbDist1() NOTIFY distChanged) + Q_PROPERTY(float uwbDist2 READ getUwbDist2() NOTIFY distChanged) + Q_PROPERTY(float uwbDist3 READ getUwbDist3() NOTIFY distChanged) + Q_PROPERTY(float uwbDist4 READ getUwbDist4() NOTIFY distChanged) + Q_INVOKABLE void trigger(); Q_INVOKABLE void stop(); + Q_INVOKABLE void manualCheckpoint(); + void onData(std::string str); + void onUWBData(std::vector data); public: @@ -45,9 +61,18 @@ public: float getStdDev3() {return _stdDev[2];} float getStdDev4() {return _stdDev[3];} + float getOffset() {return _offset;} + void setOffset(float value) { _offset = value; emit offsetChanged(); } + + float getUwbDist1() {return _uwbDist[0];} + float getUwbDist2() {return _uwbDist[1];} + float getUwbDist3() {return _uwbDist[2];} + float getUwbDist4() {return _uwbDist[3];} + signals: void distChanged(); + void offsetChanged(); public: diff --git a/RTT.pro b/RTT.pro index 67670db..ec0684a 100644 --- a/RTT.pro +++ b/RTT.pro @@ -46,4 +46,82 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target HEADERS += \ - Manager.h + Manager.h \ + uwb.h + +DISTFILES += \ + _android/src/UWB.java \ + _android/src/no/nordicsemi/android/ble/annotation/ConnectionPriority.java \ + _android/src/no/nordicsemi/android/ble/annotation/ConnectionState.java \ + _android/src/no/nordicsemi/android/ble/annotation/PhyMask.java \ + _android/src/no/nordicsemi/android/ble/annotation/PhyOption.java \ + _android/src/no/nordicsemi/android/ble/annotation/PhyValue.java \ + _android/src/no/nordicsemi/android/ble/annotation/WriteType.java \ + _android/src/no/nordicsemi/android/ble/callback/profile/ProfileDataCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/profile/ProfileReadResponse.java \ + _android/src/no/nordicsemi/android/ble/callback/BeforeCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/ConnectionPriorityCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/DataReceivedCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/DataSentCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/FailCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/InvalidRequestCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/MtuCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/PhyCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/ReadProgressCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/RssiCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/SuccessCallback.java \ + _android/src/no/nordicsemi/android/ble/callback/WriteProgressCallback.java \ + _android/src/no/nordicsemi/android/ble/data/Data.java \ + _android/src/no/nordicsemi/android/ble/data/DataFilter.java \ + _android/src/no/nordicsemi/android/ble/data/DataMerger.java \ + _android/src/no/nordicsemi/android/ble/data/DataSplitter.java \ + _android/src/no/nordicsemi/android/ble/data/DataStream.java \ + _android/src/no/nordicsemi/android/ble/data/DefaultMtuSplitter.java \ + _android/src/no/nordicsemi/android/ble/data/MutableData.java \ + _android/src/no/nordicsemi/android/ble/error/GattError.java \ + _android/src/no/nordicsemi/android/ble/exception/BluetoothDisabledException.java \ + _android/src/no/nordicsemi/android/ble/exception/ConnectionException.java \ + _android/src/no/nordicsemi/android/ble/exception/DeviceDisconnectedException.java \ + _android/src/no/nordicsemi/android/ble/exception/InvalidDataException.java \ + _android/src/no/nordicsemi/android/ble/exception/InvalidRequestException.java \ + _android/src/no/nordicsemi/android/ble/exception/RequestFailedException.java \ + _android/src/no/nordicsemi/android/ble/response/ConnectionPriorityResponse.java \ + _android/src/no/nordicsemi/android/ble/response/MtuResult.java \ + _android/src/no/nordicsemi/android/ble/response/PhyResult.java \ + _android/src/no/nordicsemi/android/ble/response/ReadResponse.java \ + _android/src/no/nordicsemi/android/ble/response/RssiResult.java \ + _android/src/no/nordicsemi/android/ble/response/WriteResponse.java \ + _android/src/no/nordicsemi/android/ble/utils/ILogger.java \ + _android/src/no/nordicsemi/android/ble/utils/ParserUtils.java \ + _android/src/no/nordicsemi/android/ble/BleManager.java \ + _android/src/no/nordicsemi/android/ble/BleManagerCallbacks.java \ + _android/src/no/nordicsemi/android/ble/ConnectionPriorityRequest.java \ + _android/src/no/nordicsemi/android/ble/ConnectRequest.java \ + _android/src/no/nordicsemi/android/ble/DisconnectRequest.java \ + _android/src/no/nordicsemi/android/ble/MainThreadBluetoothGattCallback.java \ + _android/src/no/nordicsemi/android/ble/MtuRequest.java \ + _android/src/no/nordicsemi/android/ble/Operation.java \ + _android/src/no/nordicsemi/android/ble/PhyRequest.java \ + _android/src/no/nordicsemi/android/ble/ReadRequest.java \ + _android/src/no/nordicsemi/android/ble/ReadRssiRequest.java \ + _android/src/no/nordicsemi/android/ble/ReliableWriteRequest.java \ + _android/src/no/nordicsemi/android/ble/Request.java \ + _android/src/no/nordicsemi/android/ble/RequestQueue.java \ + _android/src/no/nordicsemi/android/ble/SimpleRequest.java \ + _android/src/no/nordicsemi/android/ble/SimpleValueRequest.java \ + _android/src/no/nordicsemi/android/ble/SleepRequest.java \ + _android/src/no/nordicsemi/android/ble/TimeoutableRequest.java \ + _android/src/no/nordicsemi/android/ble/TimeoutableValueRequest.java \ + _android/src/no/nordicsemi/android/ble/TimeoutHandler.java \ + _android/src/no/nordicsemi/android/ble/ValueChangedCallback.java \ + _android/src/no/nordicsemi/android/ble/WaitForValueChangedRequest.java \ + _android/src/no/nordicsemi/android/ble/WriteRequest.java \ + _android/src/DecaManager.java \ + _android/src/DecaManagerCallbacks.java \ + _android/AndroidManifest.xml \ + _android/gradle/wrapper/gradle-wrapper.jar \ + _android/gradlew \ + _android/res/values/libs.xml \ + _android/build.gradle \ + _android/gradle/wrapper/gradle-wrapper.properties \ + _android/gradlew.bat diff --git a/_android/AndroidManifest.xml b/_android/AndroidManifest.xml index 37b01e0..64f977d 100644 --- a/_android/AndroidManifest.xml +++ b/_android/AndroidManifest.xml @@ -1,49 +1,49 @@ - + - - - - - - - - - - + + + + + + + + + + - + - + - + - - - - + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - + diff --git a/_android/build.gradle b/_android/build.gradle new file mode 100644 index 0000000..dbd79d5 --- /dev/null +++ b/_android/build.gradle @@ -0,0 +1,65 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.2.0' + } +} + +repositories { + google() + jcenter() +} + +apply plugin: 'com.android.application' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + implementation 'androidx.annotation:annotation:1.0.0' + implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' +} + +android { + /******************************************************* + * The following variables: + * - androidBuildToolsVersion, + * - androidCompileSdkVersion + * - qt5AndroidDir - holds the path to qt android files + * needed to build any Qt application + * on Android. + * + * are defined in gradle.properties file. This file is + * updated by QtCreator and androiddeployqt tools. + * Changing them manually might break the compilation! + *******************************************************/ + + compileSdkVersion androidCompileSdkVersion.toInteger() + + buildToolsVersion androidBuildToolsVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] + aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] + res.srcDirs = [qt5AndroidDir + '/res', 'res'] + resources.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } + + lintOptions { + abortOnError false + } +} diff --git a/_android/gradle.properties b/_android/gradle.properties new file mode 100644 index 0000000..b3908aa --- /dev/null +++ b/_android/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + + + +android.enableJetifier=true +android.useAndroidX=true \ No newline at end of file diff --git a/_android/gradle/wrapper/gradle-wrapper.jar b/_android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/_android/gradle/wrapper/gradle-wrapper.properties b/_android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bf3de21 --- /dev/null +++ b/_android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/_android/gradlew b/_android/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/_android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/_android/gradlew.bat b/_android/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/_android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/_android/res/values/libs.xml b/_android/res/values/libs.xml new file mode 100644 index 0000000..4009a77 --- /dev/null +++ b/_android/res/values/libs.xml @@ -0,0 +1,25 @@ + + + + https://download.qt.io/ministro/android/qt5/qt-5.9 + + + + + + + + + + + + + + + + + + + + diff --git a/_android/src/DecaManager.java b/_android/src/DecaManager.java new file mode 100644 index 0000000..baaab9f --- /dev/null +++ b/_android/src/DecaManager.java @@ -0,0 +1,107 @@ +package com.example.nrftest; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.Context; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.util.UUID; + +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.BleManager; +import no.nordicsemi.android.ble.callback.profile.ProfileDataCallback; +import no.nordicsemi.android.ble.data.Data; + +public class DecaManager extends BleManager { + private static final String TAG = "DecaManager"; + + /** Decawave Service UUID. */ + public final static UUID LBS_UUID_SERVICE = UUID.fromString("680c21d9-c946-4c1f-9c11-baa1c21329e7"); + /** */ + public final static UUID LBS_UUID_LOCATION_DATA_CHAR = UUID.fromString("003bbdf2-c634-4b3d-ab56-7ec889b89a37"); + + + + private BluetoothGattCharacteristic _locationDataCharacteristic; + + + + public DecaManager(@NonNull final Context context) + { + super(context); + } + + + @NonNull + @Override + protected BleManagerGattCallback getGattCallback() { + return mGattCallback; + } + + + /** + * BluetoothGatt callbacks object. + */ + private final BleManagerGattCallback mGattCallback = new BleManagerGattCallback() { + @Override + protected void initialize() { + + requestMtu(512).enqueue(); + + setNotificationCallback(_locationDataCharacteristic).with(mButtonCallback); +// readCharacteristic(mLedCharacteristic).with(mLedCallback).enqueue(); +// readCharacteristic(mButtonCharacteristic).with(mButtonCallback).enqueue(); + enableNotifications(_locationDataCharacteristic).enqueue(); + } + + @Override + public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { + final BluetoothGattService service = gatt.getService(LBS_UUID_SERVICE); + if (service != null) { + _locationDataCharacteristic = service.getCharacteristic(LBS_UUID_LOCATION_DATA_CHAR); + } + +// boolean writeRequest = false; +// if (mLedCharacteristic != null) { +// final int rxProperties = mLedCharacteristic.getProperties(); +// writeRequest = (rxProperties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0; +// } +// +// mSupported = mButtonCharacteristic != null && mLedCharacteristic != null && writeRequest; +// return mSupported; + + return _locationDataCharacteristic != null; + } + + @Override + protected void onDeviceDisconnected() { + _locationDataCharacteristic = null; + } + }; + + + + private final ProfileDataCallback mButtonCallback = new ProfileDataCallback() { + @Override + public void onDataReceived(@NonNull BluetoothDevice device, @NonNull Data data) { + Log.d(TAG, "onDataReceived: length=" + data.size() + " data=" + data); + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + for (int i = 0; i < data.size(); i++) { + stream.write(data.getByte(i)); + } + + mCallbacks.onTagLocationData(stream.toByteArray()); + } + + @Override + public void onInvalidDataReceived(@NonNull final BluetoothDevice device, + @NonNull final Data data) { + Log.w(TAG, "Invalid data received: " + data); + } + }; +} diff --git a/_android/src/DecaManagerCallbacks.java b/_android/src/DecaManagerCallbacks.java new file mode 100644 index 0000000..9e8a79b --- /dev/null +++ b/_android/src/DecaManagerCallbacks.java @@ -0,0 +1,8 @@ +package com.example.nrftest; + +import no.nordicsemi.android.ble.BleManagerCallbacks; + +public interface DecaManagerCallbacks extends BleManagerCallbacks { + + void onTagLocationData(byte[] dataStr); +} diff --git a/_android/src/RTT.java b/_android/src/RTT.java index 41dcd9c..4d645ed 100644 --- a/_android/src/RTT.java +++ b/_android/src/RTT.java @@ -62,10 +62,8 @@ public class RTT { Log.d("RTT", mac.toString() + " FAILED"); } - RTT.onRTTComplete(serialize(res)); - - } - + RTT.onRTTComplete(serialize(res)); + } } }; @@ -75,28 +73,31 @@ public class RTT { char delim = ';'; boolean success = res.getStatus() == RangingResult.STATUS_SUCCESS; - if (success) { - baos.write(("" + res.getRangingTimestampMillis()).getBytes()); - baos.write(delim); - } else { - baos.write(("" + System.currentTimeMillis()).getBytes()); - baos.write(delim); - } + baos.write(success ? '1' : '0'); + baos.write(delim); + + baos.write(("" + System.currentTimeMillis()).getBytes()); + baos.write(delim); baos.write(res.getMacAddress().toString().getBytes()); baos.write(delim); - if (success) { - baos.write( ("" + res.getDistanceMm()).getBytes() ); - baos.write(delim); - baos.write( ("" + res.getDistanceStdDevMm()).getBytes() ); - baos.write(delim); - baos.write( ("" + res.getRssi()).getBytes() ); + int distValue = 0; + int distStdDev = 0; + int rssi = 0; - } else { - baos.write( "FAILED".getBytes() ); + if (success) { + distValue = res.getDistanceMm(); + distStdDev = res.getDistanceStdDevMm(); + rssi = res.getRssi(); } + baos.write( ("" + distValue).getBytes() ); + baos.write(delim); + baos.write( ("" + distStdDev).getBytes() ); + baos.write(delim); + baos.write( ("" + rssi).getBytes() ); + } catch (final Exception e) {;} return baos.toByteArray(); } diff --git a/_android/src/UWB.java b/_android/src/UWB.java new file mode 100644 index 0000000..2c72b5b --- /dev/null +++ b/_android/src/UWB.java @@ -0,0 +1,174 @@ +package android.net.wifi; + +import android.app.Activity; +import android.util.Log; +import android.content.Context; + +import java.lang.System; + +import java.util.List; +import java.util.ArrayList; + +import androidx.annotation.NonNull; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; + +import java.util.concurrent.Executor; +import android.net.MacAddress; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Array; + +import indoor.java.MyActivity; +import java.io.ByteArrayOutputStream; + +import com.example.nrftest.*; + +public class UWB { + + private static Activity act; + private static Executor mainExecutor; + private static Thread uwbThread; + private static boolean uwbRunning; + + private static BluetoothManager bluetoothManager; + private static BluetoothAdapter bluetoothAdapter; + private static BluetoothDevice bluetoothDevice; + + private static DecaManager uwbManager; + + // called when a UWB is completed successfully + public static native void onUWBComplete(final byte[] result); + + + public static int start() { + + if (uwbRunning) + return 0; + + Log.d("UWB", "start()"); + + MyActivity act = MyActivity.act; + mainExecutor = act.getMainExecutor(); + + bluetoothManager = (BluetoothManager) act.getSystemService(Context.BLUETOOTH_SERVICE); + bluetoothAdapter = bluetoothManager.getAdapter(); + + bluetoothDevice = bluetoothAdapter.getRemoteDevice("D1:6C:7A:99:57:71"); + + if (bluetoothDevice == null) { + return 1; + } + + uwbManager = new DecaManager(act.getApplicationContext()); + uwbManager.setGattCallbacks(decaCallbacks); + + uwbManager.connect(bluetoothDevice) + .retry(3, 100) + .useAutoConnect(false) + .enqueue(); + + uwbRunning = true; + +// uwbThread = new Thread() { +// public void run() { +// while(uwbRunning) { +// try { +// Thread.sleep(200); +// } catch (Exception e) {;} + +// startRangingOnMacs(macs); +// } +// } +// }; + +// uwbThread.start(); + + return 1337; + } + + public static int stop() { + Log.d("UWB", "stop()"); + + if (uwbRunning) { + uwbManager.disconnect().enqueue(); + + uwbRunning = false; + } + + return 1337*2; + } + + + static final DecaManagerCallbacks decaCallbacks = new DecaManagerCallbacks(){ + + @Override + public void onTagLocationData(byte[] data) { + onUWBComplete(data); + } + + @Override + public void onDeviceConnecting(@NonNull BluetoothDevice device) { + Log.d("DecaCallbacks", "onDeviceConnecting"); + } + + @Override + public void onDeviceConnected(@NonNull BluetoothDevice device) { + Log.d("DecaCallbacks", "onDeviceConnected"); + } + + @Override + public void onDeviceDisconnecting(@NonNull BluetoothDevice device) { + Log.d("DecaCallbacks", "onDeviceDisconnecting"); + } + + @Override + public void onDeviceDisconnected(@NonNull BluetoothDevice device) { + Log.d("DecaCallbacks", "onDeviceDisconnected"); + } + + @Override + public void onLinkLossOccurred(@NonNull BluetoothDevice device) { + Log.d("DecaCallbacks", "onLinkLossOccurred"); + } + + @Override + public void onServicesDiscovered(@NonNull BluetoothDevice device, boolean optionalServicesFound) { + Log.d("DecaCallbacks", "onServicesDiscovered"); + } + + @Override + public void onDeviceReady(@NonNull BluetoothDevice device) { + Log.d("DecaCallbacks", "onDeviceReady"); + } + + @Override + public void onBondingRequired(@NonNull BluetoothDevice device) { + Log.d("DecaCallbacks", "onBondingRequired"); + } + + @Override + public void onBonded(@NonNull BluetoothDevice device) { + Log.d("DecaCallbacks", "onBonded"); + } + + @Override + public void onBondingFailed(@NonNull BluetoothDevice device) { + Log.d("DecaCallbacks", "onBondingFailed"); + } + + @Override + public void onError(@NonNull BluetoothDevice device, @NonNull String message, int errorCode) { + Log.d("DecaCallbacks", "onError: " + message); + } + + @Override + public void onDeviceNotSupported(@NonNull BluetoothDevice device) { + Log.d("DecaCallbacks", "onDeviceNotSupported"); + } + + }; +} diff --git a/_android/src/no/nordicsemi/android/ble/BleManager.java b/_android/src/no/nordicsemi/android/ble/BleManager.java new file mode 100644 index 0000000..01c2ccd --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/BleManager.java @@ -0,0 +1,3650 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.ble; + +import android.annotation.TargetApi; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothProfile; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.UUID; + +import androidx.annotation.IntDef; +import androidx.annotation.IntRange; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.StringRes; +import no.nordicsemi.android.ble.annotation.ConnectionPriority; +import no.nordicsemi.android.ble.annotation.ConnectionState; +import no.nordicsemi.android.ble.annotation.PhyMask; +import no.nordicsemi.android.ble.annotation.PhyOption; +import no.nordicsemi.android.ble.annotation.PhyValue; +import no.nordicsemi.android.ble.callback.ConnectionPriorityCallback; +import no.nordicsemi.android.ble.callback.DataReceivedCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.MtuCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; +import no.nordicsemi.android.ble.data.Data; +import no.nordicsemi.android.ble.data.DataMerger; +import no.nordicsemi.android.ble.data.DataSplitter; +import no.nordicsemi.android.ble.error.GattError; +import no.nordicsemi.android.ble.utils.ILogger; +import no.nordicsemi.android.ble.utils.ParserUtils; + +// DO NOT EDIT THIS FILE UNLESS NECESSARY! + +/** + *

+ * The BleManager is responsible for managing the low level communication with a Bluetooth LE device. + * Please see profiles implementation in Android nRF Blinky or Android nRF Toolbox app for an + * example of use. + *

+ * This base manager has been tested against number of devices and samples from Nordic SDK. + *

+ * The manager handles connection events and initializes the device after establishing the connection. + *

    + *
  1. For bonded devices it ensures that the Service Changed indications, if this characteristic + * is present, are enabled. Before Android Marshmallow, Android did not enable them by default, + * leaving this to the developers.
  2. + *
  3. The manager tries to read the Battery Level characteristic. No matter the result of + * this operation (for example the Battery Level characteristic may not have the READ property) + * it tries to enable Battery Level notifications to get battery updates from the device. + * This feature is now deprecated and will not work with the new API. Instead, read or enabledBattery Level + * notifications just like any other.
  4. + *
  5. After connecting and service discovery, the manager initializes the device using given queue + * of commands. See {@link BleManagerGattCallback#initialize()} method for more details.
  6. + *
  7. When initialization complete, the {@link BleManagerCallbacks#onDeviceReady(BluetoothDevice)} + * callback is called.
  8. + *
+ *

+ * The manager also is responsible for parsing the Battery Level values and calling + * {@link BleManagerCallbacks#onBatteryValueReceived(BluetoothDevice, int)} method. + *

+ * To get logs, override the {@link #log(int, String)} method. + *

+ * The BleManager should be overridden in your app and all the 'high level' callbacks should + * be called from there. Keeping this file as is (and {@link BleManagerCallbacks} as well) + * will allow to quickly update it when an update is posted here. + * + * @param The profile callbacks type. + */ +@SuppressWarnings({"WeakerAccess", "unused", "DeprecatedIsStillUsed", "deprecation"}) +public abstract class BleManager extends TimeoutHandler implements ILogger { + private final static String TAG = "BleManager"; + + private final static UUID CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + + private final static UUID BATTERY_SERVICE = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb"); + private final static UUID BATTERY_LEVEL_CHARACTERISTIC = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb"); + + private final static UUID GENERIC_ATTRIBUTE_SERVICE = UUID.fromString("00001801-0000-1000-8000-00805f9b34fb"); + private final static UUID SERVICE_CHANGED_CHARACTERISTIC = UUID.fromString("00002A05-0000-1000-8000-00805f9b34fb"); + + private final Object mLock = new Object(); + private final Context mContext; + final Handler mHandler; + + private BluetoothGatt mBluetoothGatt; + private BluetoothDevice mBluetoothDevice; + private BleManagerGattCallback mGattCallback; + protected E mCallbacks; + /** + * This flag is set to false only when the {@link ConnectRequest#shouldAutoConnect()} method + * returns true and the device got disconnected without calling {@link #disconnect()} method. + * If {@link ConnectRequest#shouldAutoConnect()} returns false (default) this is always + * set to true. + */ + private boolean mUserDisconnected; + /** + * Flag set to true when {@link ConnectRequest#shouldAutoConnect()} method returned true. + * The first connection attempt is done with autoConnect flag set to false + * (to make the first connection quick) but on connection lost the manager will call + * {@link #connect(BluetoothDevice)} again. This time this method will call + * {@link BluetoothGatt#connect()} which always uses autoConnect equal true. + */ + private boolean mInitialConnection; + /** + * Flag set to true when the device is connected. + */ + private boolean mConnected; + /** + * A timestamp when the last connection attempt was made. This is distinguish two situations + * when the 133 error happens during a connection attempt: a timeout (when ~30 sec passed since + * connection was requested), or an error (packet collision, packet missed, etc.) + */ + private long mConnectionTime; + /** + * A time after which receiving 133 error is considered a timeout, instead of a + * different reason. + * A {@link BluetoothDevice#connectGatt(Context, boolean, BluetoothGattCallback)} call will + * fail after 30 seconds if the device won't be found until then. Other errors happen much + * earlier. 20 sec should be OK here. + */ + private final static long CONNECTION_TIMEOUT_THRESHOLD = 20000; // ms + /** + * Flag set to true when the initialization queue is complete. + */ + private boolean mReady; + /** + * Flag set when services were discovered. + */ + private boolean mServicesDiscovered; + private boolean mServiceDiscoveryRequested; + private int mConnectionCount = 0; + /** + * Connection state. One of {@link BluetoothGatt#STATE_CONNECTED}, + * {@link BluetoothGatt#STATE_CONNECTING}, {@link BluetoothGatt#STATE_DISCONNECTED}, + * {@link BluetoothGatt#STATE_DISCONNECTING}. + */ + private int mConnectionState = BluetoothGatt.STATE_DISCONNECTED; + /** + * Last received battery value or -1 if value wasn't received. + * + * @deprecated Battery value should be kept in the profile manager instead. See BatteryManager + * class in Android nRF Toolbox app. + */ + @IntRange(from = -1, to = 100) + @Deprecated + private int mBatteryValue = -1; + /** + * The current MTU (Maximum Transfer Unit). The maximum number of bytes that can be sent in + * a single packet is MTU-3. + */ + private int mMtu = 23; + /** A flag indicating that Reliable Write is in progress. */ + private boolean mReliableWriteInProgress; + /** + * The connect request. This is instantiated in {@link #connect(BluetoothDevice, int)} + * and nullified after the device is ready. + *

+ * This request has a separate reference, as it is notified when the device becomes ready, + * after the initialization requests are done. + */ + private ConnectRequest mConnectRequest; + /** + * Currently performed request or null in idle state. + */ + private Request mRequest; + /** + * Currently performer request set, or null if none. + */ + private RequestQueue mRequestQueue; + /** + * A map of {@link ValueChangedCallback}s for handling notifications and indications. + */ + private final HashMap mNotificationCallbacks = new HashMap<>(); + /** + * An instance of a request that waits for a notification or an indication. + * There may be only a single instance of such request at a time as this is a blocking request. + */ + private WaitForValueChangedRequest mValueChangedRequest; + @Deprecated + private ValueChangedCallback mBatteryLevelNotificationCallback; + + private final BroadcastReceiver mBluetoothStateBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); + final int previousState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.STATE_OFF); + + final String stateString = "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + + ", state changed to " + state2String(state); + log(Log.DEBUG, stateString); + + switch (state) { + case BluetoothAdapter.STATE_TURNING_OFF: + case BluetoothAdapter.STATE_OFF: + if (previousState != BluetoothAdapter.STATE_TURNING_OFF + && previousState != BluetoothAdapter.STATE_OFF) { + final BleManagerGattCallback callback = mGattCallback; + if (callback != null) { + // No more calls are possible + callback.mOperationInProgress = true; + callback.cancelQueue(); + callback.mInitQueue = null; + } + + final BluetoothDevice device = mBluetoothDevice; + if (device != null) { + // Signal the current request, if any + if (mRequest != null && mRequest.type != Request.Type.DISCONNECT) { + mRequest.notifyFail(device, FailCallback.REASON_BLUETOOTH_DISABLED); + mRequest = null; + } + if (mValueChangedRequest != null) { + mValueChangedRequest.notifyFail(device, FailCallback.REASON_BLUETOOTH_DISABLED); + mValueChangedRequest = null; + } + if (mConnectRequest != null) { + mConnectRequest.notifyFail(device, FailCallback.REASON_BLUETOOTH_DISABLED); + mConnectRequest = null; + } + } + + // The connection is killed by the system, no need to disconnect gently. + mUserDisconnected = true; + if (callback != null) { + // Allow new requests when Bluetooth is enabled again. close() doesn't do it. + // See: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/25 + // and: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/41 + callback.mOperationInProgress = false; + // This will call close() + if (device != null) { + callback.notifyDeviceDisconnected(device); + } + } + } else { + // Calling close() will prevent the STATE_OFF event from being logged + // (this receiver will be unregistered). But it doesn't matter. + close(); + } + break; + } + } + + private String state2String(final int state) { + switch (state) { + case BluetoothAdapter.STATE_TURNING_ON: + return "TURNING ON"; + case BluetoothAdapter.STATE_ON: + return "ON"; + case BluetoothAdapter.STATE_TURNING_OFF: + return "TURNING OFF"; + case BluetoothAdapter.STATE_OFF: + return "OFF"; + default: + return "UNKNOWN (" + state + ")"; + } + } + }; + + private BroadcastReceiver mBondingBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1); + final int previousBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1); + + // Skip other devices. + if (mBluetoothDevice == null || !device.getAddress().equals(mBluetoothDevice.getAddress())) + return; + + log(Log.DEBUG, "[Broadcast] Action received: " + BluetoothDevice.ACTION_BOND_STATE_CHANGED + + ", bond state changed to: " + bondStateToString(bondState) + " (" + bondState + ")"); + + switch (bondState) { + case BluetoothDevice.BOND_NONE: + if (previousBondState == BluetoothDevice.BOND_BONDING) { + mCallbacks.onBondingFailed(device); + log(Log.WARN, "Bonding failed"); + if (mRequest != null) { // CREATE_BOND request + mRequest.notifyFail(device, FailCallback.REASON_REQUEST_FAILED); + mRequest = null; + } + } else if (previousBondState == BluetoothDevice.BOND_BONDED) { + if (mRequest != null && mRequest.type == Request.Type.REMOVE_BOND) { + // The device has already disconnected by now. + log(Log.INFO, "Bond information removed"); + mRequest.notifySuccess(device); + mRequest = null; + } + } + break; + case BluetoothDevice.BOND_BONDING: + mCallbacks.onBondingRequired(device); + return; + case BluetoothDevice.BOND_BONDED: + log(Log.INFO, "Device bonded"); + mCallbacks.onBonded(device); + if (mRequest != null && mRequest.type == Request.Type.CREATE_BOND) { + mRequest.notifySuccess(device); + mRequest = null; + break; + } + // If the device started to pair just after the connection was + // established the services were not discovered. + if (!mServicesDiscovered && !mServiceDiscoveryRequested) { + mServiceDiscoveryRequested = true; + mHandler.post(() -> { + log(Log.VERBOSE, "Discovering services..."); + log(Log.DEBUG, "gatt.discoverServices()"); + mBluetoothGatt.discoverServices(); + }); + return; + } + // On older Android versions, after executing a command on secured attribute + // of a device that is not bonded, let's say a write characteristic operation, + // the system will start bonding. The BOND_BONDING and BOND_BONDED events will + // be received, but the command will not be repeated automatically. + // + // Test results: + // Devices that require repeating the last task: + // - Nexus 4 with Android 5.1.1 + // - Samsung S6 with 5.0.1 + // - Samsung S8 with Android 7.0 + // - Nexus 9 with Android 7.1.1 + // Devices that repeat the request automatically: + // - Pixel 2 with Android 8.1.0 + // - Samsung S8 with Android 8.0.0 + // + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + if (mRequest != null && mRequest.type != Request.Type.CREATE_BOND) { + // Repeat the last command in that case. + mGattCallback.enqueueFirst(mRequest); + break; + } + } + // No need to repeat the request. + return; + } + mGattCallback.nextRequest(true); + } + }; + + private final BroadcastReceiver mPairingRequestBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + + // Skip other devices. + if (mBluetoothDevice == null || device == null + || !device.getAddress().equals(mBluetoothDevice.getAddress())) + return; + + // String values are used as the constants are not available for Android 4.3. + final int variant = intent.getIntExtra("android.bluetooth.device.extra.PAIRING_VARIANT"/*BluetoothDevice.EXTRA_PAIRING_VARIANT*/, 0); + log(Log.DEBUG, "[Broadcast] Action received: android.bluetooth.device.action.PAIRING_REQUEST"/*BluetoothDevice.ACTION_PAIRING_REQUEST*/ + + ", pairing variant: " + pairingVariantToString(variant) + " (" + variant + ")"); + + onPairingRequestReceived(device, variant); + } + }; + + /** + * This method will be called if a remote device requires a non-'just works' pairing. + * See PAIRING_* constants for possible options. + * + * @param device the device. + * @param variant pairing variant. + */ + protected void onPairingRequestReceived(@NonNull final BluetoothDevice device, + @PairingVariant final int variant) { + // The API below is available for Android 4.4 or newer. + + // An app may set the PIN here or set pairing confirmation (depending on the variant) using: + // device.setPin(new byte[] { '1', '2', '3', '4', '5', '6' }); + // device.setPairingConfirmation(true); + + // However, setting the PIN here will not prevent from displaying the default pairing + // dialog, which is shown by another application (Bluetooth Settings). + } + + /** + * The manager constructor. + *

+ * After constructing the manager, the callbacks object must be set with + * {@link #setGattCallbacks(BleManagerCallbacks)}. + *

+ * To connect a device, call {@link #connect(BluetoothDevice)}. + * + * @param context the context. + */ + public BleManager(@NonNull final Context context) { + mContext = context.getApplicationContext(); + mHandler = new Handler(Looper.getMainLooper()); + } + + /** + * Sets the manager callback listener. + * + * @param callbacks the callback listener. + */ + public void setGattCallbacks(@NonNull final E callbacks) { + mCallbacks = callbacks; + } + + /** + * This method must return the gatt callback used by the manager. + * This method must not create a new gatt callback each time it is being invoked, but rather + * return a single object. + * + * @return The gatt callback object. + */ + @NonNull + protected abstract BleManagerGattCallback getGattCallback(); + + /** + * Returns the context that the manager was created with. + * + * @return The context. + */ + @NonNull + protected final Context getContext() { + return mContext; + } + + /** + * Returns the Bluetooth device object used in {@link #connect(BluetoothDevice)}. + * + * @return The Bluetooth device or null, if {@link #connect(BluetoothDevice)} wasn't called. + */ + @Nullable + // This method is not final, as some Managers may be created with BluetoothDevice in a + // constructor. Those can return the device object even without calling connect(device). + public BluetoothDevice getBluetoothDevice() { + return mBluetoothDevice; + } + + /** + * This method returns true if the device is connected. Services could have not been + * discovered yet. + */ + public final boolean isConnected() { + return mConnected; + } + + /** + * Returns whether the target device is bonded. The device does not have to be connected, + * but must have been set prior to call this method. + * + * @return True, if the Android has bonds information of the device. This does not mean that + * the target device also has such information, or that the link is in fact encrypted. + */ + protected final boolean isBonded() { + return mBluetoothDevice != null + && mBluetoothDevice.getBondState() == BluetoothDevice.BOND_BONDED; + } + + /** + * Returns true if the device is connected and the initialization has finished, + * that is when {@link BleManagerGattCallback#onDeviceReady()} was called. + */ + public final boolean isReady() { + return mReady; + } + + /** + * Method returns the connection state: + * {@link BluetoothProfile#STATE_CONNECTING STATE_CONNECTING}, + * {@link BluetoothProfile#STATE_CONNECTED STATE_CONNECTED}, + * {@link BluetoothProfile#STATE_DISCONNECTING STATE_DISCONNECTING}, + * {@link BluetoothProfile#STATE_DISCONNECTED STATE_DISCONNECTED} + * + * @return The connection state. + */ + @ConnectionState + public final int getConnectionState() { + return mConnectionState; + } + + /** + * Returns the last received value of Battery Level characteristic, or -1 if such + * does not exist, hasn't been read or notification wasn't received yet. + *

+ * The value returned will be invalid if overridden {@link #readBatteryLevel()} and + * {@link #enableBatteryLevelNotifications()} were used. + * + * @return The last battery level value in percent. + * @deprecated Keep the battery level in your manager instead. + */ + @IntRange(from = -1, to = 100) + @Deprecated + public final int getBatteryValue() { + return mBatteryValue; + } + + @Override + public void log(final int priority, @NonNull final String message) { + // Override to log events. Simple log can use Logcat: + // + // Log.println(priority, TAG, message); + // + // You may also use Timber: + // + // Timber.log(priority, message); + // + // or nRF Logger: + // + // Logger.log(logSession, LogContract.Log.Level.fromPriority(priority), message); + // + // Starting from nRF Logger 2.1.3, you may use log-timber and plant nRFLoggerTree. + // https://github.com/NordicSemiconductor/nRF-Logger-API + } + + @Override + public void log(final int priority, @StringRes final int messageRes, + @Nullable final Object... params) { + final String message = mContext.getString(messageRes, params); + log(priority, message); + } + + /** + * Returns whether to connect to the remote device just once (false) or to add the address to + * white list of devices that will be automatically connect as soon as they become available + * (true). In the latter case, if Bluetooth adapter is enabled, Android scans periodically + * for devices from the white list and if a advertising packet is received from such, it tries + * to connect to it. When the connection is lost, the system will keep trying to reconnect to + * it in. If true is returned, and the connection to the device is lost the + * {@link BleManagerCallbacks#onLinkLossOccurred(BluetoothDevice)} callback is called instead of + * {@link BleManagerCallbacks#onDeviceDisconnected(BluetoothDevice)}. + *

+ * This feature works much better on newer Android phone models and many not work on older + * phones. + *

+ * This method should only be used with bonded devices, as otherwise the device may change + * it's address. It will however work also with non-bonded devices with private static address. + * A connection attempt to a device with private resolvable address will fail. + *

+ * The first connection to a device will always be created with autoConnect flag to false + * (see {@link BluetoothDevice#connectGatt(Context, boolean, BluetoothGattCallback)}). This is + * to make it quick as the user most probably waits for a quick response. + * However, if this method returned true during first connection and the link was lost, + * the manager will try to reconnect to it using {@link BluetoothGatt#connect()} which forces + * autoConnect to true. + * + * @return The AutoConnect flag value. + * @deprecated Use {@link ConnectRequest#useAutoConnect(boolean)} instead. + */ + @Deprecated + protected boolean shouldAutoConnect() { + return false; + } + + /** + * Returns whether the device cache should be cleared after the device disconnected, + * before calling {@link BluetoothGatt#close()}. By default it returns false. + *

+ * If the returned value is true, the next time the Android device will connect to + * this peripheral the services will be discovered again. If false, the services + * will be obtained from the cache. + *

+ * Note, that the {@link BluetoothGatt#refresh()} method is not in the public API and it + * is not recommended to use this. However, as Android is caching services of all devices, + * even if they are not bonded and have Service Changed characteristic, it may necessary to + * clear the cache manually. + *

+ * On older Android versions clearing device cache helped with connection stability. + * It was common to get error 133 on the second and following connections when services were + * obtained from the cache. However, full service discovery takes time and consumes peripheral's + * battery. + * + * @return True, if the device cache should be cleared after the device disconnects or false, + * (default) if the cached value be used. + */ + @SuppressWarnings("JavadocReference") + protected boolean shouldClearCacheWhenDisconnected() { + return false; + } + + /** + * The onConnectionStateChange event is triggered just after the Android connects to a device. + * In case of bonded devices, the encryption is reestablished AFTER this callback is called. + * Moreover, when the device has Service Changed indication enabled, and the list of services + * has changed (e.g. using the DFU), the indication is received few hundred milliseconds later, + * depending on the connection interval. + * When received, Android will start performing a service discovery operation, internally, + * and will NOT notify the app that services has changed. + *

+ * If the gatt.discoverServices() method would be invoked here with no delay, if would return + * cached services, as the SC indication wouldn't be received yet. Therefore, we have to + * postpone the service discovery operation until we are (almost, as there is no such callback) + * sure, that it has been handled. It should be greater than the time from + * LLCP Feature Exchange to ATT Write for Service Change indication. + *

+ * If your device does not use Service Change indication (for example does not have DFU) + * the delay may be 0. + *

+ * Please calculate the proper delay that will work in your solution. + *

+ * For devices that are not bonded, but support paiing, a small delay is required on some + * older Android versions (Nexus 4 with Android 5.1.1) when the device will send pairing + * request just after connection. If so, we want to wait with the service discovery until + * bonding is complete. + *

+ * The default implementation returns 1600 ms for bonded and 300 ms when the device is not + * bonded to be compatible with older versions of the library. + */ + @IntRange(from = 0) + protected int getServiceDiscoveryDelay(final boolean bonded) { + return bonded ? 1600 : 300; + } + + /** + * Creates a Connect request that will try to connect to the given Bluetooth LE device. + * Call {@link ConnectRequest#enqueue()} or {@link ConnectRequest#await()} in order to execute + * the request. + *

+ * This method returns a {@link ConnectRequest} which can be used to set completion + * and failure callbacks. The completion callback (done) will be called after the initialization + * is complete, after {@link BleManagerCallbacks#onDeviceReady(BluetoothDevice)} has been + * called. + *

+ * Calling {@link ConnectRequest#await()} will make this request + * synchronous (the callbacks set will be ignored, instead the synchronous method will + * return or throw an exception). + *

+ * For asynchronous call usage, {@link ConnectRequest#enqueue()} must be called on the returned + * request. + *

+ * The callbacks observer must be set with {@link #setGattCallbacks(BleManagerCallbacks)} + * before calling this method. + * + * @param device a device to connect to. + * @return The connect request. + */ + @SuppressWarnings("ConstantConditions") + @NonNull + public final ConnectRequest connect(@NonNull final BluetoothDevice device) { + if (mCallbacks == null) { + throw new NullPointerException("Set callbacks using setGattCallbacks(E callbacks) before connecting"); + } + if (device == null) { + throw new NullPointerException("Bluetooth device not specified"); + } + return Request.connect(device) + .useAutoConnect(shouldAutoConnect()) + .setManager(this); + } + + /** + * Creates a Connect request that will try to connect to the given Bluetooth LE device using + * preferred PHY. Call {@link ConnectRequest#enqueue()} or {@link ConnectRequest#await()} + * in order to execute the request. + *

+ * This method returns a {@link ConnectRequest} which can be used to set completion + * and failure callbacks. The completion callback will be called after the initialization + * is complete, after {@link BleManagerCallbacks#onDeviceReady(BluetoothDevice)} has been + * called. + *

+ * Calling {@link ConnectRequest#await()} will make this request + * synchronous (the callbacks set will be ignored, instead the synchronous method will + * return or throw an exception). + *

+ * For asynchronous call usage, {@link ConnectRequest#enqueue()} must be called on the returned + * request. + *

+ * The callbacks observer must be set with {@link #setGattCallbacks(BleManagerCallbacks)} + * before calling this method. + * + * @param device a device to connect to. + * @param phy preferred PHY for connections to remote LE device. Bitwise OR of any of + * {@link PhyRequest#PHY_LE_1M_MASK}, {@link PhyRequest#PHY_LE_2M_MASK}, + * and {@link PhyRequest#PHY_LE_CODED_MASK}. This option does not take effect + * if {@code autoConnect} is set to true. PHY 2M and Coded are supported + * on newer devices running Android Oreo or newer. + * @return The connect request. + * @deprecated Use {@link #connect(BluetoothDevice)} instead and set preferred PHY using + * {@link ConnectRequest#usePreferredPhy(int)}. + */ + @SuppressWarnings("ConstantConditions") + @NonNull + @Deprecated + public final ConnectRequest connect(@NonNull final BluetoothDevice device, @PhyMask final int phy) { + if (mCallbacks == null) { + throw new NullPointerException("Set callbacks using setGattCallbacks(E callbacks) before connecting"); + } + if (device == null) { + throw new NullPointerException("Bluetooth device not specified"); + } + return Request.connect(device) + .usePreferredPhy(phy) + .useAutoConnect(shouldAutoConnect()) + .setManager(this); + } + + @MainThread + private boolean internalConnect(@NonNull final BluetoothDevice device, + @Nullable final ConnectRequest connectRequest) { + final boolean bluetoothEnabled = BluetoothAdapter.getDefaultAdapter().isEnabled(); + if (mConnected || !bluetoothEnabled) { + final BluetoothDevice currentDevice = mBluetoothDevice; + if (bluetoothEnabled && currentDevice != null && currentDevice.equals(device)) { + mConnectRequest.notifySuccess(device); + } else { + // We can't return false here, as the request would be notified with + // mBluetoothDevice instance instead, and that may be null or a wrong device. + if (mConnectRequest != null) { + mConnectRequest.notifyFail(device, + bluetoothEnabled ? + FailCallback.REASON_REQUEST_FAILED : + FailCallback.REASON_BLUETOOTH_DISABLED); + } // else, the request was already failed by the Bluetooth state receiver + } + mConnectRequest = null; + if (mGattCallback != null) { + mGattCallback.nextRequest(true); + } + return true; + } + + synchronized (mLock) { + if (mBluetoothGatt != null) { + // There are 2 ways of reconnecting to the same device: + // 1. Reusing the same BluetoothGatt object and calling connect() - this will force + // the autoConnect flag to true + // 2. Closing it and reopening a new instance of BluetoothGatt object. + // The gatt.close() is an asynchronous method. It requires some time before it's + // finished and device.connectGatt(...) can't be called immediately or service + // discovery may never finish on some older devices (Nexus 4, Android 5.0.1). + // If shouldAutoConnect() method returned false we can't call gatt.connect() and + // have to close gatt and open it again. + if (!mInitialConnection) { + log(Log.DEBUG, "gatt.close()"); + try { + mBluetoothGatt.close(); + } catch (final Throwable t) { + // ignore + } + mBluetoothGatt = null; + try { + log(Log.DEBUG, "wait(200)"); + Thread.sleep(200); // Is 200 ms enough? + } catch (final InterruptedException e) { + // Ignore + } + } else { + // Instead, the gatt.connect() method will be used to reconnect to the same device. + // This method forces autoConnect = true even if the gatt was created with this + // flag set to false. + mInitialConnection = false; + mConnectionTime = 0L; // no timeout possible when autoConnect used + mConnectionState = BluetoothGatt.STATE_CONNECTING; + log(Log.VERBOSE, "Connecting..."); + mCallbacks.onDeviceConnecting(device); + log(Log.DEBUG, "gatt.connect()"); + mBluetoothGatt.connect(); + return true; + } + } else { + // Register bonding broadcast receiver + mContext.registerReceiver(mBluetoothStateBroadcastReceiver, + new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); + mContext.registerReceiver(mBondingBroadcastReceiver, + new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); + mContext.registerReceiver(mPairingRequestBroadcastReceiver, + //BluetoothDevice.ACTION_PAIRING_REQUEST + new IntentFilter("android.bluetooth.device.action.PAIRING_REQUEST")); + } + } + + // This should not happen in normal circumstances, but may, when Bluetooth was turned off + // when retrying to create a connection. + if (connectRequest == null) + return false; + final boolean shouldAutoConnect = connectRequest.shouldAutoConnect(); + // We will receive Link Loss events only when the device is connected with autoConnect=true. + mUserDisconnected = !shouldAutoConnect; + // The first connection will always be done with autoConnect = false to make the connection quick. + // If the shouldAutoConnect() method returned true, the manager will automatically try to + // reconnect to this device on link loss. + if (shouldAutoConnect) { + mInitialConnection = true; + } + mBluetoothDevice = device; + mGattCallback.setHandler(mHandler); + log(Log.VERBOSE, connectRequest.isFirstAttempt() ? "Connecting..." : "Retrying..."); + mConnectionState = BluetoothGatt.STATE_CONNECTING; + mCallbacks.onDeviceConnecting(device); + mConnectionTime = SystemClock.elapsedRealtime(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // connectRequest will never be null here. + final int preferredPhy = connectRequest.getPreferredPhy(); + log(Log.DEBUG, "gatt = device.connectGatt(autoConnect = false, TRANSPORT_LE, " + + phyMaskToString(preferredPhy) + ")"); + // A variant of connectGatt with Handled can't be used here. + // Check https://github.com/NordicSemiconductor/Android-BLE-Library/issues/54 + mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback, + BluetoothDevice.TRANSPORT_LE, preferredPhy/*, mHandler*/); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + log(Log.DEBUG, "gatt = device.connectGatt(autoConnect = false, TRANSPORT_LE)"); + mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback, + BluetoothDevice.TRANSPORT_LE); + } else { + log(Log.DEBUG, "gatt = device.connectGatt(autoConnect = false)"); + mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback); + } + return true; + } + + /** + * Disconnects from the device or cancels the pending connection attempt. + * Does nothing if device was not connected. + * + * @return The disconnect request. The completion callback will be called after the device + * has disconnected and the connection was closed. If the device was not connected, + * the completion callback will be called immediately with device parameter set to null. + */ + @NonNull + public final DisconnectRequest disconnect() { + return Request.disconnect().setManager(this); + } + + @MainThread + private boolean internalDisconnect() { + mUserDisconnected = true; + mInitialConnection = false; + mReady = false; + + if (mBluetoothGatt != null) { + mConnectionState = BluetoothGatt.STATE_DISCONNECTING; + log(Log.VERBOSE, mConnected ? "Disconnecting..." : "Cancelling connection..."); + mCallbacks.onDeviceDisconnecting(mBluetoothGatt.getDevice()); + final boolean wasConnected = mConnected; + log(Log.DEBUG, "gatt.disconnect()"); + mBluetoothGatt.disconnect(); + + if (wasConnected) + return true; + + // If the device wasn't connected, there will be no callback after calling + // gatt.disconnect(), the connection attempt will be stopped. + mConnectionState = BluetoothGatt.STATE_DISCONNECTED; + log(Log.INFO, "Disconnected"); + mCallbacks.onDeviceDisconnected(mBluetoothGatt.getDevice()); + } + // mRequest may be of type DISCONNECT or CONNECT (timeout). + // For the latter, it has already been notified with REASON_TIMEOUT. + if (mRequest != null && mRequest.type == Request.Type.DISCONNECT) { + if (mBluetoothDevice != null) + mRequest.notifySuccess(mBluetoothDevice); + else + mRequest.notifyInvalidRequest(); + } + if (mGattCallback != null) { + mGattCallback.nextRequest(true); + } + return true; + } + + /** + * Closes and releases resources. This method will be called automatically after + * calling {@link #disconnect()}. When the device disconnected with link loss and + * {@link #shouldAutoConnect()} returned true you have call this method to close the connection. + */ + public void close() { + try { + mContext.unregisterReceiver(mBluetoothStateBroadcastReceiver); + mContext.unregisterReceiver(mBondingBroadcastReceiver); + mContext.unregisterReceiver(mPairingRequestBroadcastReceiver); + } catch (final Exception e) { + // the receiver must have been not registered or unregistered before. + } + synchronized (mLock) { + if (mBluetoothGatt != null) { + if (shouldClearCacheWhenDisconnected()) { + if (internalRefreshDeviceCache()) { + log(Log.INFO, "Cache refreshed"); + } else { + log(Log.WARN, "Refreshing failed"); + } + } + log(Log.DEBUG, "gatt.close()"); + try { + mBluetoothGatt.close(); + } catch (final Throwable t) { + // ignore + } + mBluetoothGatt = null; + } + mConnected = false; + mInitialConnection = false; + mReliableWriteInProgress = false; + mNotificationCallbacks.clear(); + mConnectionState = BluetoothGatt.STATE_DISCONNECTED; + if (mGattCallback != null) { + // close() is called in notifyDeviceDisconnected, which may enqueue new requests. + // Setting this flag to false would allow to enqueue a new request before the + // current one ends processing. The following line should not be uncommented. + // mGattCallback.mOperationInProgress = false; + mGattCallback.cancelQueue(); + mGattCallback.mInitQueue = null; + } + mGattCallback = null; + mBluetoothDevice = null; + } + } + + /** + * Returns a request to create bond with the device. The device must be first set using + * {@link #connect(BluetoothDevice)} which will try to connect to the device. + * If you need to pair with a device before connecting to it you may do it without + * the use of BleManager object and connect after bond is established. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @return The request. + */ + @NonNull + protected Request createBond() { + return Request.createBond().setManager(this); + } + + @MainThread + private boolean internalCreateBond() { + final BluetoothDevice device = mBluetoothDevice; + if (device == null) + return false; + + log(Log.VERBOSE, "Starting pairing..."); + + if (device.getBondState() == BluetoothDevice.BOND_BONDED) { + log(Log.WARN, "Device already bonded"); + mRequest.notifySuccess(device); + mGattCallback.nextRequest(true); + return true; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + log(Log.DEBUG, "device.createBond()"); + return device.createBond(); + } else { + /* + * There is a createBond() method in BluetoothDevice class but for now it's hidden. + * We will call it using reflections. It has been revealed in KitKat (Api19). + */ + try { + final Method createBond = device.getClass().getMethod("createBond"); + log(Log.DEBUG, "device.createBond() (hidden)"); + return (Boolean) createBond.invoke(device); + } catch (final Exception e) { + Log.w(TAG, "An exception occurred while creating bond", e); + } + } + return false; + } + + /** + * Enqueues removing bond information. When the device was bonded and the bond + * information was successfully removed, the device will disconnect. + * Note, that this will not remove the bond information from the connected device! + *

+ * The success callback will be called after the device get disconnected, + * when the {@link BluetoothDevice#getBondState()} changes to {@link BluetoothDevice#BOND_NONE}. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @return The request. + */ + @NonNull + protected Request removeBond() { + return Request.removeBond().setManager(this); + } + + @MainThread + private boolean internalRemoveBond() { + final BluetoothDevice device = mBluetoothDevice; + if (device == null) + return false; + + log(Log.VERBOSE, "Removing bond information..."); + + if (device.getBondState() == BluetoothDevice.BOND_NONE) { + log(Log.WARN, "Device is not bonded"); + mRequest.notifySuccess(device); + mGattCallback.nextRequest(true); + return true; + } + + /* + * There is a removeBond() method in BluetoothDevice class but for now it's hidden. + * We will call it using reflections. + */ + try { + //noinspection JavaReflectionMemberAccess + final Method removeBond = device.getClass().getMethod("removeBond"); + log(Log.DEBUG, "device.removeBond() (hidden)"); + return (Boolean) removeBond.invoke(device); + } catch (final Exception e) { + Log.w(TAG, "An exception occurred while removing bond", e); + } + return false; + } + + /** + * When the device is bonded and has the Generic Attribute service and the Service Changed + * characteristic this method enables indications on this characteristic. + * In case one of the requirements is not fulfilled this method returns false. + * + * @return true when the request has been sent, false when the device + * is not bonded, does not have the Generic Attribute service, the GA service does not have + * the Service Changed characteristic or this characteristic does not have the CCCD. + */ + private boolean ensureServiceChangedEnabled() { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + // The Service Changed indications have sense only on bonded devices. + final BluetoothDevice device = gatt.getDevice(); + if (device.getBondState() != BluetoothDevice.BOND_BONDED) + return false; + + final BluetoothGattService gaService = gatt.getService(GENERIC_ATTRIBUTE_SERVICE); + if (gaService == null) + return false; + + final BluetoothGattCharacteristic scCharacteristic = + gaService.getCharacteristic(SERVICE_CHANGED_CHARACTERISTIC); + if (scCharacteristic == null) + return false; + + log(Log.INFO, "Service Changed characteristic found on a bonded device"); + return internalEnableIndications(scCharacteristic); + } + + /** + * Returns the callback that is registered for value changes (notifications) of given + * characteristic. After assigning the notifications callback, notifications must be + * enabled using {@link #enableNotifications(BluetoothGattCharacteristic)}. + * This applies also when they were already enabled on the remote side. + *

+ * To remove the callback, disable notifications using + * {@link #disableNotifications(BluetoothGattCharacteristic)}. + * + * @param characteristic characteristic to bind the callback with. If null, the returned + * callback will not be null, but will not be used. + * @return The callback. + */ + @MainThread + @NonNull + protected ValueChangedCallback setNotificationCallback(@Nullable final BluetoothGattCharacteristic characteristic) { + ValueChangedCallback callback = mNotificationCallbacks.get(characteristic); + if (callback == null) { + callback = new ValueChangedCallback(); + if (characteristic != null) { + mNotificationCallbacks.put(characteristic, callback); + } + } + return callback.free(); + } + + /** + * Returns the callback that is registered for value changes (indications) of given + * characteristic. After assigning the notifications callback, indications must be + * enabled using {@link #enableIndications(BluetoothGattCharacteristic)}. + * This applies also when they were already enabled on the remote side. + *

+ * To remove the callback, disable indications using + * {@link #disableIndications(BluetoothGattCharacteristic)}. + * + * @param characteristic characteristic to bind the callback with. If null, the returned + * callback will not be null, but will not be used. + * @return The callback. + */ + @NonNull + protected ValueChangedCallback setIndicationCallback(@Nullable final BluetoothGattCharacteristic characteristic) { + return setNotificationCallback(characteristic); + } + + /** + * Sets a one-time callback that will be notified when the value of the given characteristic + * changes. This is a blocking request, so the next request will be executed after the + * notification was received. + *

+ * If {@link WaitForValueChangedRequest#merge(DataMerger)} was used, the whole message will be + * completed before the callback is notified. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param characteristic the characteristic that value is expect to change. + * @return The callback. + */ + @NonNull + protected WaitForValueChangedRequest waitForNotification(@NonNull final BluetoothGattCharacteristic characteristic) { + return Request.newWaitForNotificationRequest(characteristic).setManager(this); + } + + /** + * Sets a one-time callback that will be notified when the value of the given characteristic + * changes. This is a blocking request, so the next request will be executed after the + * indication was received. + *

+ * If {@link WaitForValueChangedRequest#merge(DataMerger)} was used, the whole message will be + * completed before the callback is notified. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param characteristic the characteristic that value is expect to change. + * @return The callback. + */ + @NonNull + protected WaitForValueChangedRequest waitForIndication(@NonNull final BluetoothGattCharacteristic characteristic) { + return Request.newWaitForIndicationRequest(characteristic).setManager(this); + } + + /** + * Enables notifications on given characteristic. + * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param characteristic the characteristic to be enabled. + * @return The request. + */ + @NonNull + protected WriteRequest enableNotifications( + @Nullable final BluetoothGattCharacteristic characteristic) { + return Request.newEnableNotificationsRequest(characteristic).setManager(this); + } + + @MainThread + private boolean internalEnableNotifications(final BluetoothGattCharacteristic characteristic) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || characteristic == null || !mConnected) + return false; + + final BluetoothGattDescriptor descriptor = getCccd(characteristic, BluetoothGattCharacteristic.PROPERTY_NOTIFY); + if (descriptor != null) { + log(Log.DEBUG, "gatt.setCharacteristicNotification(" + characteristic.getUuid() + ", true)"); + gatt.setCharacteristicNotification(characteristic, true); + + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + log(Log.VERBOSE, "Enabling notifications for " + characteristic.getUuid()); + log(Log.DEBUG, "gatt.writeDescriptor(" + CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID + ", value=0x01-00)"); + return internalWriteDescriptorWorkaround(descriptor); + } + return false; + } + + /** + * Disables notifications on given characteristic. + * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param characteristic the characteristic to be disabled. + * @return The request. + */ + @NonNull + protected WriteRequest disableNotifications(@Nullable final BluetoothGattCharacteristic characteristic) { + return Request.newDisableNotificationsRequest(characteristic).setManager(this); + } + + @MainThread + private boolean internalDisableNotifications(final BluetoothGattCharacteristic characteristic) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || characteristic == null || !mConnected) + return false; + + final BluetoothGattDescriptor descriptor = getCccd(characteristic, BluetoothGattCharacteristic.PROPERTY_NOTIFY); + if (descriptor != null) { + log(Log.DEBUG, "gatt.setCharacteristicNotification(" + characteristic.getUuid() + ", false)"); + gatt.setCharacteristicNotification(characteristic, false); + + descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); + log(Log.VERBOSE, "Disabling notifications and indications for " + characteristic.getUuid()); + log(Log.DEBUG, "gatt.writeDescriptor(" + CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID + ", value=0x00-00)"); + return internalWriteDescriptorWorkaround(descriptor); + } + return false; + } + + /** + * Enables indications on given characteristic. + * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param characteristic the characteristic to be enabled. + * @return The request. + */ + @NonNull + protected WriteRequest enableIndications(@Nullable final BluetoothGattCharacteristic characteristic) { + return Request.newEnableIndicationsRequest(characteristic).setManager(this); + } + + @MainThread + private boolean internalEnableIndications(final BluetoothGattCharacteristic characteristic) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || characteristic == null || !mConnected) + return false; + + final BluetoothGattDescriptor descriptor = getCccd(characteristic, BluetoothGattCharacteristic.PROPERTY_INDICATE); + if (descriptor != null) { + log(Log.DEBUG, "gatt.setCharacteristicNotification(" + characteristic.getUuid() + ", true)"); + gatt.setCharacteristicNotification(characteristic, true); + + descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE); + log(Log.VERBOSE, "Enabling indications for " + characteristic.getUuid()); + log(Log.DEBUG, "gatt.writeDescriptor(" + CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID + ", value=0x02-00)"); + return internalWriteDescriptorWorkaround(descriptor); + } + return false; + } + + /** + * Disables indications on given characteristic. + * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param characteristic the characteristic to be disabled. + * @return The request. + */ + @NonNull + protected WriteRequest disableIndications(@Nullable final BluetoothGattCharacteristic characteristic) { + return Request.newDisableIndicationsRequest(characteristic).setManager(this); + } + + @MainThread + private boolean internalDisableIndications(final BluetoothGattCharacteristic characteristic) { + // This writes exactly the same settings so do not duplicate code. + return internalDisableNotifications(characteristic); + } + + /** + * Returns the Client Characteristic Config Descriptor if the characteristic has the + * required property. It may return null if the CCCD is not there. + * + * @param characteristic the characteristic to look the CCCD in. + * @param requiredProperty the required property: {@link BluetoothGattCharacteristic#PROPERTY_NOTIFY} + * or {@link BluetoothGattCharacteristic#PROPERTY_INDICATE}. + * @return The CCC descriptor or null if characteristic is null, if it doesn't have the + * required property, or if the CCCD is missing. + */ + private static BluetoothGattDescriptor getCccd(@Nullable final BluetoothGattCharacteristic characteristic, + final int requiredProperty) { + if (characteristic == null) + return null; + + // Check characteristic property + final int properties = characteristic.getProperties(); + if ((properties & requiredProperty) == 0) + return null; + + return characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID); + } + + /** + * Sends the read request to the given characteristic. + * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param characteristic the characteristic to read. + * @return The request. + */ + @NonNull + protected ReadRequest readCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic) { + return Request.newReadRequest(characteristic).setManager(this); + } + + @MainThread + private boolean internalReadCharacteristic(final BluetoothGattCharacteristic characteristic) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || characteristic == null || !mConnected) + return false; + + // Check characteristic property. + final int properties = characteristic.getProperties(); + if ((properties & BluetoothGattCharacteristic.PROPERTY_READ) == 0) + return false; + + log(Log.VERBOSE, "Reading characteristic " + characteristic.getUuid()); + log(Log.DEBUG, "gatt.readCharacteristic(" + characteristic.getUuid() + ")"); + return gatt.readCharacteristic(characteristic); + } + + /** + * Writes the given data to the characteristic. The write type is taken from the characteristic. + *

+ * Use {@link WriteRequest#split() split()} or + * {@link WriteRequest#split(DataSplitter) split(DataSplitter)} on the returned + * {@link WriteRequest} if data should be automatically split into multiple packets. + * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param characteristic the characteristic to write to. + * @param data data to be written to the characteristic. + * @return The request. + */ + @NonNull + protected WriteRequest writeCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic, + @Nullable final Data data) { + return Request.newWriteRequest(characteristic, data != null ? data.getValue() : null) + .setManager(this); + } + + /** + * Writes the given data to the characteristic. The write type is taken from the characteristic. + *

+ * Use {@link WriteRequest#split() split()} or + * {@link WriteRequest#split(DataSplitter) split(ValueSplitter)} on the returned + * {@link WriteRequest} if data should be automatically split into multiple packets. + * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param characteristic the characteristic to write to. + * @param data data to be written to the characteristic. + * @return The request. + */ + @NonNull + protected WriteRequest writeCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] data) { + return Request.newWriteRequest(characteristic, data).setManager(this); + } + + /** + * Writes at most length bytes from offset at given data to the characteristic. + * The write type is taken from the characteristic. + *

+ * Use {@link WriteRequest#split() split()} or + * {@link WriteRequest#split(DataSplitter) split(ValueSplitter)} on the returned + * {@link WriteRequest} if data should be automatically split into multiple packets. + * If the characteristic is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param characteristic the characteristic to write to. + * @param data data to be written to the characteristic. + * @param offset index of the first byte to be sent. + * @param length number of bytes to be sent. + * @return The request. + */ + @NonNull + protected WriteRequest writeCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] data, final int offset, final int length) { + return Request.newWriteRequest(characteristic, data, offset, length).setManager(this); + } + + @MainThread + private boolean internalWriteCharacteristic(final BluetoothGattCharacteristic characteristic) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || characteristic == null || !mConnected) + return false; + + // Check characteristic property. + final int properties = characteristic.getProperties(); + if ((properties & (BluetoothGattCharacteristic.PROPERTY_WRITE | + BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) == 0) + return false; + + log(Log.VERBOSE, "Writing characteristic " + characteristic.getUuid() + + " (" + writeTypeToString(characteristic.getWriteType()) + ")"); + log(Log.DEBUG, "gatt.writeCharacteristic(" + characteristic.getUuid() + ")"); + return gatt.writeCharacteristic(characteristic); + } + + /** + * Sends the read request to the given descriptor. + * If the descriptor is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param descriptor the descriptor to read. + * @return The request. + */ + @NonNull + protected ReadRequest readDescriptor(@Nullable final BluetoothGattDescriptor descriptor) { + return Request.newReadRequest(descriptor).setManager(this); + } + + @MainThread + private boolean internalReadDescriptor(final BluetoothGattDescriptor descriptor) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || descriptor == null || !mConnected) + return false; + + log(Log.VERBOSE, "Reading descriptor " + descriptor.getUuid()); + log(Log.DEBUG, "gatt.readDescriptor(" + descriptor.getUuid() + ")"); + return gatt.readDescriptor(descriptor); + } + + /** + * Writes the given data to the descriptor. + *

+ * Use {@link WriteRequest#split() split()} or + * {@link WriteRequest#split(DataSplitter) split(ValueSplitter)} on the returned + * {@link WriteRequest} if data should be automatically split into multiple packets. + * If the descriptor is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param descriptor the descriptor to write to. + * @param data data to be written to the descriptor. + * @return The request. + */ + @NonNull + protected WriteRequest writeDescriptor(@Nullable final BluetoothGattDescriptor descriptor, + @Nullable final Data data) { + return Request.newWriteRequest(descriptor, data != null ? data.getValue() : null) + .setManager(this); + } + + /** + * Writes the given data to the descriptor. + *

+ * Use {@link WriteRequest#split() split()} or + * {@link WriteRequest#split(DataSplitter) split(ValueSplitter)} on the returned + * {@link WriteRequest} if data should be automatically split into multiple packets. + * If the descriptor is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param descriptor the descriptor to write to. + * @param data data to be written to the descriptor. + * @return The request. + */ + @NonNull + protected WriteRequest writeDescriptor(@Nullable final BluetoothGattDescriptor descriptor, + @Nullable final byte[] data) { + return Request.newWriteRequest(descriptor, data).setManager(this); + } + + /** + * Writes at most length bytes from offset at given data to the descriptor. + *

+ * Use {@link WriteRequest#split() split()} or + * {@link WriteRequest#split(DataSplitter) split(ValueSplitter)} on the returned + * {@link WriteRequest} if data should be automatically split into multiple packets. + * If the descriptor is null, the {@link Request#fail(FailCallback) fail(FailCallback)} + * callback will be called. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param descriptor the descriptor to write to. + * @param data data to be written to the descriptor. + * @param offset index of the first byte to be sent. + * @param length number of bytes to be sent. + * @return The request. + */ + @NonNull + protected WriteRequest writeDescriptor(@Nullable final BluetoothGattDescriptor descriptor, + @Nullable final byte[] data, final int offset, + final int length) { + return Request.newWriteRequest(descriptor, data, offset, length).setManager(this); + } + + @MainThread + private boolean internalWriteDescriptor(final BluetoothGattDescriptor descriptor) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || descriptor == null || !mConnected) + return false; + + log(Log.VERBOSE, "Writing descriptor " + descriptor.getUuid()); + log(Log.DEBUG, "gatt.writeDescriptor(" + descriptor.getUuid() + ")"); + return internalWriteDescriptorWorkaround(descriptor); + } + + /** + * Creates an atomic request queue. The requests from the queue will be executed in order. + * This is useful when more then one thread may add requests and you want some of them to + * be executed together. + * + * @return The request. + */ + @NonNull + protected RequestQueue beginAtomicRequestQueue() { + return new RequestQueue().setManager(this); + } + + /** + * Begins the Reliable Write sub-procedure. Requests that need to be performed reliably + * should be enqueued with {@link ReliableWriteRequest#add(Operation)} instead of using + * {@link Request#enqueue()}. The library will verify all Write operations and will + * automatically abort the Reliable Write procedure when the returned data mismatch with the + * data sent. When all requests enqueued in the {@link ReliableWriteRequest} were completed, + * the Reliable Write will be automatically executed. + *

+ * Long Write will not work when Reliable Write is in progress. The library will make sure + * that {@link WriteRequest#split()} was called for all {@link WriteRequest} packets, had + * they not been assigned other splitter. + *

+ * At least one Write operation must be executed before executing or aborting, otherwise the + * {@link GattError#GATT_INVALID_OFFSET} error will be reported. Because of that, enqueueing + * a {@link ReliableWriteRequest} without any operations does nothing. + *

+ * Example of usage: + *

+	 *     beginReliableWrite()
+	 *           .add(writeCharacteristic(someCharacteristic, someData)
+	 *                   .fail(...)
+	 *                   .done(...))
+	 *           // Non-write requests are also possible
+	 *           .add(requestMtu(200))
+	 *           // Data will be written in the same order
+	 *           .add(writeCharacteristic(someCharacteristic, differentData))
+	 *           // This will return the OLD data, not 'differentData', as the RW wasn't executed!
+	 *           .add(readCharacteristic(someCharacteristic).with(callback))
+	 *           // Multiple characteristics may be written during a single RW
+	 *           .add(writeCharacteristic(someOtherCharacteristic, importantData))
+	 *        // Finally, enqueue the Reliable Write request in BleManager
+	 *     	  .enqueue();
+	 * 
+ * + * @return The request. + */ + @NonNull + protected ReliableWriteRequest beginReliableWrite() { + return Request.newReliableWriteRequest().setManager(this); + } + + @MainThread + private boolean internalBeginReliableWrite() { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + // Reliable Write can't be before the old one isn't executed or aborted. + if (mReliableWriteInProgress) + return true; + + log(Log.VERBOSE, "Beginning reliable write..."); + log(Log.DEBUG, "gatt.beginReliableWrite()"); + return mReliableWriteInProgress = gatt.beginReliableWrite(); + } + + @MainThread + private boolean internalExecuteReliableWrite() { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + if (!mReliableWriteInProgress) + return false; + + log(Log.VERBOSE, "Executing reliable write..."); + log(Log.DEBUG, "gatt.executeReliableWrite()"); + return gatt.executeReliableWrite(); + } + + @MainThread + private boolean internalAbortReliableWrite() { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + if (!mReliableWriteInProgress) + return false; + + log(Log.VERBOSE, "Aborting reliable write..."); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + log(Log.DEBUG, "gatt.abortReliableWrite()"); + gatt.abortReliableWrite(); + } else { + log(Log.DEBUG, "gatt.abortReliableWrite(device)"); + gatt.abortReliableWrite(mBluetoothDevice); + } + return true; + } + + /** + * Returns true if {@link BluetoothGatt#beginReliableWrite()} has been called and + * the Reliable Write hasn't been executed nor aborted yet. + */ + protected final boolean isReliableWriteInProgress() { + return mReliableWriteInProgress; + } + + /** + * Reads the battery level from the device. + * + * @deprecated Use {@link #readCharacteristic(BluetoothGattCharacteristic)} instead. + */ + @SuppressWarnings("ConstantConditions") + @Deprecated + protected void readBatteryLevel() { + Request.newReadBatteryLevelRequest().setManager(this) + .with((device, data) -> { + if (data.size() == 1) { + final int batteryLevel = data.getIntValue(Data.FORMAT_UINT8, 0); + log(Log.INFO, "Battery Level received: " + batteryLevel + "%"); + mBatteryValue = batteryLevel; + final BleManagerGattCallback callback = mGattCallback; + if (callback != null) { + callback.onBatteryValueReceived(mBluetoothGatt, batteryLevel); + } + mCallbacks.onBatteryValueReceived(device, batteryLevel); + } + }) + .enqueue(); + } + + @MainThread + @Deprecated + private boolean internalReadBatteryLevel() { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + final BluetoothGattService batteryService = gatt.getService(BATTERY_SERVICE); + if (batteryService == null) + return false; + + final BluetoothGattCharacteristic batteryLevelCharacteristic = + batteryService.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC); + return internalReadCharacteristic(batteryLevelCharacteristic); + } + + /** + * This method enables notifications on the Battery Level characteristic. + * + * @deprecated Use {@link #setNotificationCallback(BluetoothGattCharacteristic)} and + * {@link #enableNotifications(BluetoothGattCharacteristic)} instead. + */ + @SuppressWarnings("ConstantConditions") + @Deprecated + protected void enableBatteryLevelNotifications() { + if (mBatteryLevelNotificationCallback == null) { + mBatteryLevelNotificationCallback = new ValueChangedCallback() + .with((device, data) -> { + if (data.size() == 1) { + final int batteryLevel = data.getIntValue(Data.FORMAT_UINT8, 0); + mBatteryValue = batteryLevel; + final BleManagerGattCallback callback = mGattCallback; + if (callback != null) { + callback.onBatteryValueReceived(mBluetoothGatt, batteryLevel); + } + mCallbacks.onBatteryValueReceived(device, batteryLevel); + } + }); + } + Request.newEnableBatteryLevelNotificationsRequest().setManager(this) + .done(device -> log(Log.INFO, "Battery Level notifications enabled")) + .enqueue(); + } + + /** + * This method disables notifications on the Battery Level characteristic. + * + * @deprecated Use {@link #disableNotifications(BluetoothGattCharacteristic)} instead. + */ + @Deprecated + protected void disableBatteryLevelNotifications() { + Request.newDisableBatteryLevelNotificationsRequest().setManager(this) + .done(device -> log(Log.INFO, "Battery Level notifications disabled")) + .enqueue(); + } + + @MainThread + @Deprecated + private boolean internalSetBatteryNotifications(final boolean enable) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + final BluetoothGattService batteryService = gatt.getService(BATTERY_SERVICE); + if (batteryService == null) + return false; + + final BluetoothGattCharacteristic batteryLevelCharacteristic = batteryService.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC); + if (enable) + return internalEnableNotifications(batteryLevelCharacteristic); + else + return internalDisableNotifications(batteryLevelCharacteristic); + } + + /** + * There was a bug in Android up to 6.0 where the descriptor was written using parent + * characteristic's write type, instead of always Write With Response, as the spec says. + *

+ * See: + * https://android.googlesource.com/platform/frameworks/base/+/942aebc95924ab1e7ea1e92aaf4e7fc45f695a6c%5E%21/#F0 + * + * @param descriptor the descriptor to be written + * @return the result of {@link BluetoothGatt#writeDescriptor(BluetoothGattDescriptor)} + */ + @MainThread + private boolean internalWriteDescriptorWorkaround(final BluetoothGattDescriptor descriptor) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || descriptor == null || !mConnected) + return false; + + final BluetoothGattCharacteristic parentCharacteristic = descriptor.getCharacteristic(); + final int originalWriteType = parentCharacteristic.getWriteType(); + parentCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); + final boolean result = gatt.writeDescriptor(descriptor); + parentCharacteristic.setWriteType(originalWriteType); + return result; + } + + /** + * Requests new MTU. On Android Lollipop or newer it will send the MTU request to the connected + * device. On older versions of Android the + * {@link MtuCallback#onMtuChanged(BluetoothDevice, int)} set with + * {@link MtuRequest#with(MtuCallback)} will be called with current MTU value. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @return The request. + */ + protected MtuRequest requestMtu(@IntRange(from = 23, to = 517) final int mtu) { + return Request.newMtuRequest(mtu).setManager(this); + } + + /** + * Returns the current MTU (Maximum Transfer Unit). MTU specifies the maximum number of bytes + * that can be sent in a single write operation. 3 bytes are used for internal purposes, + * so the maximum size is MTU-3. The value will changed only if requested with + * {@link #requestMtu(int)} and a successful callback is received. If the peripheral requests + * MTU change, the {@link BluetoothGattCallback#onMtuChanged(BluetoothGatt, int, int)} + * callback is not invoked, therefor the returned MTU value will not be correct. + * Use {@link android.bluetooth.BluetoothGattServerCallback#onMtuChanged(BluetoothDevice, int)} + * to get the callback with right value requested from the peripheral side. + * + * @return the current MTU value. Default to 23. + */ + @IntRange(from = 23, to = 517) + protected int getMtu() { + return mMtu; + } + + /** + * This method overrides the MTU value. Use it only when the peripheral has changed MTU and you + * received the + * {@link android.bluetooth.BluetoothGattServerCallback#onMtuChanged(BluetoothDevice, int)} + * callback. If you want to set MTU as a master, use {@link #requestMtu(int)} instead. + * + * @param mtu the MTU value set by the peripheral. + */ + protected void overrideMtu(@IntRange(from = 23, to = 517) final int mtu) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mMtu = mtu; + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @MainThread + private boolean internalRequestMtu(@IntRange(from = 23, to = 517) final int mtu) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + log(Log.VERBOSE, "Requesting new MTU..."); + log(Log.DEBUG, "gatt.requestMtu(" + mtu + ")"); + return gatt.requestMtu(mtu); + } + + /** + * Requests the new connection priority. Acceptable values are: + *

    + *
  1. {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH} + * - Interval: 11.25 -15 ms, latency: 0, supervision timeout: 20 sec,
  2. + *
  3. {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED} + * - Interval: 30 - 50 ms, latency: 0, supervision timeout: 20 sec,
  4. + *
  5. {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER} + * - Interval: 100 - 125 ms, latency: 2, supervision timeout: 20 sec.
  6. + *
+ * Works only on Android Lollipop or newer. On older system versions will cause + * {@link Request#fail(FailCallback)} callback or throw + * {@link no.nordicsemi.android.ble.exception.RequestFailedException} with + * {@link FailCallback#REASON_REQUEST_FAILED} status if called synchronously. + * Starting from Android Oreo you may get a callback with the interval, latency and timeout + * using {@link ConnectionPriorityRequest#with(ConnectionPriorityCallback)}. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param priority one of: {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH}, + * {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED}, + * {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}. + * @return The request. + */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + protected ConnectionPriorityRequest requestConnectionPriority( + @ConnectionPriority final int priority) { + return Request.newConnectionPriorityRequest(priority).setManager(this); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @MainThread + private boolean internalRequestConnectionPriority(@ConnectionPriority final int priority) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + String text, priorityText; + switch (priority) { + case ConnectionPriorityRequest.CONNECTION_PRIORITY_HIGH: + text = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? + "HIGH (11.25–15ms, 0, 20s)" : "HIGH (7.5–10ms, 0, 20s)"; + priorityText = "HIGH"; + break; + case ConnectionPriorityRequest.CONNECTION_PRIORITY_LOW_POWER: + text = "LOW POWER (100–125ms, 2, 20s)"; + priorityText = "LOW POWER"; + break; + default: + case ConnectionPriorityRequest.CONNECTION_PRIORITY_BALANCED: + text = "BALANCED (30–50ms, 0, 20s)"; + priorityText = "BALANCED"; + break; + } + log(Log.VERBOSE, "Requesting connection priority: " + text + "..."); + log(Log.DEBUG, "gatt.requestConnectionPriority(" + priorityText + ")"); + return gatt.requestConnectionPriority(priority); + } + + /** + * Enqueues a request to set the preferred PHY. + *

+ * PHY LE 2M and PHY LE Coded are supported only on Android Oreo or newer. + * You may safely request other PHYs on older platforms, but you will get PHY LE 1M + * as TX and RX PHY in the callback. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param txPhy preferred transmitter PHY. Bitwise OR of any of + * {@link PhyRequest#PHY_LE_1M_MASK}, {@link PhyRequest#PHY_LE_2M_MASK}, + * and {@link PhyRequest#PHY_LE_CODED_MASK}. + * @param rxPhy preferred receiver PHY. Bitwise OR of any of + * {@link PhyRequest#PHY_LE_1M_MASK}, {@link PhyRequest#PHY_LE_2M_MASK}, + * and {@link PhyRequest#PHY_LE_CODED_MASK}. + * @param phyOptions preferred coding to use when transmitting on the LE Coded PHY. Can be one + * of {@link PhyRequest#PHY_OPTION_NO_PREFERRED}, + * {@link PhyRequest#PHY_OPTION_S2} or {@link PhyRequest#PHY_OPTION_S8}. + * @return The request. + */ + protected PhyRequest setPreferredPhy(@PhyMask final int txPhy, @PhyMask final int rxPhy, + @PhyOption final int phyOptions) { + return Request.newSetPreferredPhyRequest(txPhy, rxPhy, phyOptions).setManager(this); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @MainThread + private boolean internalSetPreferredPhy(@PhyMask final int txPhy, @PhyMask final int rxPhy, + @PhyOption final int phyOptions) { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + log(Log.VERBOSE, "Requesting preferred PHYs..."); + log(Log.DEBUG, "gatt.setPreferredPhy(" + phyMaskToString(txPhy) + ", " + + phyMaskToString(rxPhy) + ", coding option = " + + phyCodedOptionToString(phyOptions) + ")"); + gatt.setPreferredPhy(txPhy, rxPhy, phyOptions); + return true; + } + + /** + * Reads the current PHY for this connections. + *

+ * PHY LE 2M and PHY LE Coded are supported only on Android Oreo or newer. + * You may safely read PHY on older platforms, but you will get PHY LE 1M as TX and RX PHY + * in the callback. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @return The request. + */ + protected PhyRequest readPhy() { + return Request.newReadPhyRequest().setManager(this); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @MainThread + private boolean internalReadPhy() { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + log(Log.VERBOSE, "Reading PHY..."); + log(Log.DEBUG, "gatt.readPhy()"); + gatt.readPhy(); + return true; + } + + /** + * Reads the current RSSI (Received Signal Strength Indication). + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @return The request. + */ + protected ReadRssiRequest readRssi() { + return Request.newReadRssiRequest().setManager(this); + } + + @MainThread + private boolean internalReadRssi() { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null || !mConnected) + return false; + + log(Log.VERBOSE, "Reading remote RSSI..."); + log(Log.DEBUG, "gatt.readRemoteRssi()"); + return gatt.readRemoteRssi(); + } + + /** + * Refreshes the device cache. As the {@link BluetoothGatt#refresh()} method is not in the + * public API (it's hidden, and on Android P it is on a light gray list) it is called + * using reflections and may be removed in some future Android release or on some devices. + *

+ * There is no callback indicating when the cache has been cleared. This library assumes + * some time and waits. After the delay, it will start service discovery and clear the + * task queue. When the service discovery finishes, the + * {@link BleManager.BleManagerGattCallback#isRequiredServiceSupported(BluetoothGatt)} and + * {@link BleManager.BleManagerGattCallback#isOptionalServiceSupported(BluetoothGatt)} will + * be called and the initialization will be performed as if the device has just connected. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @return The request. + */ + @SuppressWarnings("JavadocReference") + protected Request refreshDeviceCache() { + return Request.newRefreshCacheRequest().setManager(this); + } + + /** + * Clears the device cache. + */ + @SuppressWarnings("JavaReflectionMemberAccess") + @MainThread + private boolean internalRefreshDeviceCache() { + final BluetoothGatt gatt = mBluetoothGatt; + if (gatt == null) // no need to be connected + return false; + + log(Log.VERBOSE, "Refreshing device cache..."); + log(Log.DEBUG, "gatt.refresh() (hidden)"); + /* + * There is a refresh() method in BluetoothGatt class but for now it's hidden. + * We will call it using reflections. + */ + try { + final Method refresh = gatt.getClass().getMethod("refresh"); + return (Boolean) refresh.invoke(gatt); + } catch (final Exception e) { + Log.w(TAG, "An exception occurred while refreshing device", e); + log(Log.WARN, "gatt.refresh() method not found"); + } + return false; + } + + /** + * Enqueues a sleep operation with given duration. The next request will be performed after + * at least given number of milliseconds. + *

+ * The returned request must be either enqueued using {@link Request#enqueue()} for + * asynchronous use, or awaited using await() in synchronous execution. + * + * @param delay the delay in milliseconds. + * @return The request. + */ + protected SleepRequest sleep(@IntRange(from = 0) final long delay) { + return Request.newSleepRequest(delay).setManager(this); + } + + /** + * Enqueues a new request. + * + * @param request the new request to be added to the end of the queue. + * @deprecated The access modifier of this method will be changed to package only. + */ + @Deprecated + protected final void enqueue(@NonNull final Request request) { + BleManagerGattCallback callback = mGattCallback; + if (callback == null) { + callback = mGattCallback = getGattCallback(); + } + // Add the new task to the end of the queue. + final BleManagerGattCallback finalCallback = callback; + finalCallback.enqueue(request); + runOnUiThread(() -> finalCallback.nextRequest(false)); + } + + /** + * Cancels all the enqueued operations. The one currently executed will be finished. + */ + protected void clearQueue() { + final BleManagerGattCallback callback = mGattCallback; + if (callback != null) { + callback.cancelQueue(); + } + } + + private void runOnUiThread(final Runnable runnable) { + if (Looper.myLooper() != Looper.getMainLooper()) { + mHandler.post(runnable); + } else { + runnable.run(); + } + } + + @MainThread + @Override + void onRequestTimeout(@NonNull final TimeoutableRequest request) { + mRequest = null; + mValueChangedRequest = null; + if (request.type == Request.Type.CONNECT) { + mConnectRequest = null; + internalDisconnect(); + // The method above will call mGattCallback.nextRequest(true) so we have to return here. + return; + } + if (request.type == Request.Type.DISCONNECT) { + close(); + return; + } + final BleManagerGattCallback callback = mGattCallback; + if (callback != null) { + callback.nextRequest(true); + } + } + + @SuppressWarnings({"WeakerAccess", "DeprecatedIsStillUsed"}) + protected abstract class BleManagerGattCallback extends MainThreadBluetoothGattCallback { + private final static String ERROR_CONNECTION_STATE_CHANGE = "Error on connection state change"; + private final static String ERROR_DISCOVERY_SERVICE = "Error on discovering services"; + private final static String ERROR_AUTH_ERROR_WHILE_BONDED = "Phone has lost bonding information"; + private final static String ERROR_READ_CHARACTERISTIC = "Error on reading characteristic"; + private final static String ERROR_WRITE_CHARACTERISTIC = "Error on writing characteristic"; + private final static String ERROR_READ_DESCRIPTOR = "Error on reading descriptor"; + private final static String ERROR_WRITE_DESCRIPTOR = "Error on writing descriptor"; + private final static String ERROR_MTU_REQUEST = "Error on mtu request"; + private final static String ERROR_CONNECTION_PRIORITY_REQUEST = "Error on connection priority request"; + private final static String ERROR_READ_RSSI = "Error on RSSI read"; + private final static String ERROR_READ_PHY = "Error on PHY read"; + private final static String ERROR_PHY_UPDATE = "Error on PHY update"; + private final static String ERROR_RELIABLE_WRITE = "Error on Execute Reliable Write"; + + private final Deque mTaskQueue = new LinkedList<>(); + private Deque mInitQueue; + private boolean mInitInProgress; + private boolean mOperationInProgress; + + /** + * This flag is required to resume operations after the connection priority request was made. + * It is used only on Android Oreo and newer, as only there there is onConnectionUpdated + * callback. However, as this callback is triggered every time the connection parameters + * change, even when such request wasn't made, this flag ensures the nextRequest() method + * won't be called during another operation. + */ + private boolean mConnectionPriorityOperationInProgress = false; + + /** + * This method should return true when the gatt device supports the + * required services. + * + * @param gatt the gatt device with services discovered + * @return True when the device has the required service. + */ + protected abstract boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt); + + /** + * This method should return true when the gatt device supports the + * optional services. The default implementation returns false. + * + * @param gatt the gatt device with services discovered + * @return True when the device has the optional service. + */ + protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) { + return false; + } + + /** + * This method should return a list of requests needed to initialize the profile. + * Enabling Service Change indications for bonded devices and reading the Battery Level + * value and enabling Battery Level notifications is handled before executing this queue. + * The queue should not have requests that are not available, e.g. should not read an + * optional service when it is not supported by the connected device. + *

+ * This method is called when the services has been discovered and the device is supported + * (has required service). + * + * @param gatt the gatt device with services discovered + * @return The queue of requests. + * @deprecated Use {@link #initialize()} instead. + */ + @Deprecated + protected Deque initGatt(@NonNull final BluetoothGatt gatt) { + return null; + } + + /** + * This method should set up the request queue needed to initialize the profile. + * Enabling Service Change indications for bonded devices is handled before executing this + * queue. The queue may have requests that are not available, e.g. read an optional + * service when it is not supported by the connected device. Such call will trigger + * {@link Request#fail(FailCallback)}. + *

+ * This method is called from the main thread when the services has been discovered and + * the device is supported (has required service). + *

+ * Remember to call {@link Request#enqueue()} for each request. + *

+ * A sample initialization should look like this: + *

+		 * @Override
+		 * protected void initialize() {
+		 *    requestMtu(MTU)
+		 *       .with((device, mtu) -> {
+		 *           ...
+		 *       })
+		 *       .enqueue();
+		 *    setNotificationCallback(characteristic)
+		 *       .with((device, data) -> {
+		 *           ...
+		 *       });
+		 *    enableNotifications(characteristic)
+		 *       .done(device -> {
+		 *           ...
+		 *       })
+		 *       .fail((device, status) -> {
+		 *           ...
+		 *       })
+		 *       .enqueue();
+		 * }
+		 * 
+ */ + protected void initialize() { + // empty initialization queue + } + + /** + * Called then the initialization queue is complete. + */ + protected void onDeviceReady() { + mCallbacks.onDeviceReady(mBluetoothGatt.getDevice()); + } + + /** + * Called each time the task queue gets cleared. + */ + protected void onManagerReady() { + // empty + } + + /** + * This method should nullify all services and characteristics of the device. + * It's called when the device is no longer connected, either due to user action + * or a link loss. + */ + protected abstract void onDeviceDisconnected(); + + private void notifyDeviceDisconnected(@NonNull final BluetoothDevice device) { + final boolean wasConnected = mConnected; + mConnected = false; + mServicesDiscovered = false; + mServiceDiscoveryRequested = false; + mInitInProgress = false; + mConnectionState = BluetoothGatt.STATE_DISCONNECTED; + if (!wasConnected) { + log(Log.WARN, "Connection attempt timed out"); + close(); + mCallbacks.onDeviceDisconnected(device); + // ConnectRequest was already notified + } else if (mUserDisconnected) { + log(Log.INFO, "Disconnected"); + close(); + mCallbacks.onDeviceDisconnected(device); + final Request request = mRequest; + if (request != null && request.type == Request.Type.DISCONNECT) { + request.notifySuccess(device); + } + } else { + log(Log.WARN, "Connection lost"); + mCallbacks.onLinkLossOccurred(device); + // We are not closing the connection here as the device should try to reconnect + // automatically. + // This may be only called when the shouldAutoConnect() method returned true. + } + onDeviceDisconnected(); + } + + /** + * Callback reporting the result of a characteristic read operation. + * + * @param gatt GATT client + * @param characteristic Characteristic that was read from the associated remote device. + * @deprecated Use {@link ReadRequest#with(DataReceivedCallback)} instead. + */ + @Deprecated + protected void onCharacteristicRead(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic) { + // do nothing + } + + /** + * Callback indicating the result of a characteristic write operation. + *

If this callback is invoked while a reliable write transaction is + * in progress, the value of the characteristic represents the value + * reported by the remote device. An application should compare this + * value to the desired value to be written. If the values don't match, + * the application must abort the reliable write transaction. + * + * @param gatt GATT client + * @param characteristic Characteristic that was written to the associated remote device. + * @deprecated Use {@link WriteRequest#done(SuccessCallback)} instead. + */ + @Deprecated + protected void onCharacteristicWrite(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic) { + // do nothing + } + + /** + * Callback reporting the result of a descriptor read operation. + * + * @param gatt GATT client + * @param descriptor Descriptor that was read from the associated remote device. + * @deprecated Use {@link ReadRequest#with(DataReceivedCallback)} instead. + */ + @Deprecated + protected void onDescriptorRead(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattDescriptor descriptor) { + // do nothing + } + + /** + * Callback indicating the result of a descriptor write operation. + *

If this callback is invoked while a reliable write transaction is in progress, + * the value of the characteristic represents the value reported by the remote device. + * An application should compare this value to the desired value to be written. + * If the values don't match, the application must abort the reliable write transaction. + * + * @param gatt GATT client + * @param descriptor Descriptor that was written to the associated remote device. + * @deprecated Use {@link WriteRequest} and {@link SuccessCallback} instead. + */ + @Deprecated + protected void onDescriptorWrite(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattDescriptor descriptor) { + // do nothing + } + + /** + * Callback reporting the value of Battery Level characteristic which could have + * been received by Read or Notify operations. + *

+ * This method will not be called if {@link #readBatteryLevel()} and + * {@link #enableBatteryLevelNotifications()} were overridden. + *

+ * + * @param gatt GATT client + * @param value the battery value in percent + * @deprecated Use {@link ReadRequest#with(DataReceivedCallback)} and + * BatteryLevelDataCallback from BLE-Common-Library instead. + */ + @Deprecated + protected void onBatteryValueReceived(@NonNull final BluetoothGatt gatt, + @IntRange(from = 0, to = 100) final int value) { + // do nothing + } + + /** + * Callback indicating a notification has been received. + * + * @param gatt GATT client + * @param characteristic Characteristic from which the notification came. + * @deprecated Use {@link ReadRequest#with(DataReceivedCallback)} instead. + */ + @Deprecated + protected void onCharacteristicNotified(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic) { + // do nothing + } + + /** + * Callback indicating an indication has been received. + * + * @param gatt GATT client + * @param characteristic Characteristic from which the indication came. + * @deprecated Use {@link ReadRequest#with(DataReceivedCallback)} instead. + */ + @Deprecated + protected void onCharacteristicIndicated(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic) { + // do nothing + } + + /** + * Method called when the MTU request has finished with success. The MTU value may + * be different than requested one. + * + * @param gatt GATT client + * @param mtu the new MTU (Maximum Transfer Unit) + * @deprecated Use {@link MtuRequest#with(MtuCallback)} instead. + */ + @Deprecated + protected void onMtuChanged(@NonNull final BluetoothGatt gatt, + @IntRange(from = 23, to = 517) final int mtu) { + // do nothing + } + + /** + * Callback indicating the connection parameters were updated. Works on Android 8+. + * + * @param gatt GATT client. + * @param interval Connection interval used on this connection, 1.25ms unit. + * Valid range is from 6 (7.5ms) to 3200 (4000ms). + * @param latency Slave latency for the connection in number of connection events. + * Valid range is from 0 to 499. + * @param timeout Supervision timeout for this connection, in 10ms unit. + * Valid range is from 10 (0.1s) to 3200 (32s). + * @deprecated Use {@link ConnectionPriorityRequest#with(ConnectionPriorityCallback)} instead. + */ + @Deprecated + @TargetApi(Build.VERSION_CODES.O) + protected void onConnectionUpdated(@NonNull final BluetoothGatt gatt, + @IntRange(from = 6, to = 3200) final int interval, + @IntRange(from = 0, to = 499) final int latency, + @IntRange(from = 10, to = 3200) final int timeout) { + // do nothing + } + + private void onError(final BluetoothDevice device, final String message, final int errorCode) { + log(Log.ERROR, "Error (0x" + Integer.toHexString(errorCode) + "): " + + GattError.parse(errorCode)); + mCallbacks.onError(device, message, errorCode); + } + + @Override + final void onConnectionStateChangeSafe(@NonNull final BluetoothGatt gatt, final int status, final int newState) { + log(Log.DEBUG, "[Callback] Connection state changed with status: " + + status + " and new state: " + newState + " (" + stateToString(newState) + ")"); + + if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) { + // Sometimes, when a notification/indication is received after the device got + // disconnected, the Android calls onConnectionStateChanged again, with state + // STATE_CONNECTED. + // See: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/43 + if (mBluetoothDevice == null) { + Log.e(TAG, "Device received notification after disconnection."); + log(Log.DEBUG, "gatt.close()"); + try { + gatt.close(); + } catch (final Throwable t) { + // ignore + } + return; + } + + // Notify the parent activity/service. + log(Log.INFO, "Connected to " + gatt.getDevice().getAddress()); + mConnected = true; + mConnectionTime = 0L; + mConnectionState = BluetoothGatt.STATE_CONNECTED; + mCallbacks.onDeviceConnected(gatt.getDevice()); + + if (!mServiceDiscoveryRequested) { + final boolean bonded = gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED; + final int delay = getServiceDiscoveryDelay(bonded); + if (delay > 0) + log(Log.DEBUG, "wait(" + delay + ")"); + + final int connectionCount = ++mConnectionCount; + mHandler.postDelayed(() -> { + if (connectionCount != mConnectionCount) { + // Ensure that we will not try to discover services for a lost connection. + return; + } + // Some proximity tags (e.g. nRF PROXIMITY Pebble) initialize bonding + // automatically when connected. Wait with the discovery until bonding is + // complete. It will be initiated again in the bond state broadcast receiver + // on the top of this file. + if (mConnected && + gatt.getDevice().getBondState() != BluetoothDevice.BOND_BONDING) { + mServiceDiscoveryRequested = true; + log(Log.VERBOSE, "Discovering services..."); + log(Log.DEBUG, "gatt.discoverServices()"); + gatt.discoverServices(); + } + }, delay); + } + } else { + if (newState == BluetoothProfile.STATE_DISCONNECTED) { + final long now = SystemClock.elapsedRealtime(); + final boolean canTimeout = mConnectionTime > 0; + final boolean timeout = canTimeout && now > mConnectionTime + CONNECTION_TIMEOUT_THRESHOLD; + + if (status != BluetoothGatt.GATT_SUCCESS) + log(Log.WARN, "Error: (0x" + Integer.toHexString(status) + "): " + + GattError.parseConnectionError(status)); + + // In case of a connection error, retry if required. + if (status != BluetoothGatt.GATT_SUCCESS && canTimeout && !timeout + && mConnectRequest != null && mConnectRequest.canRetry()) { + final int delay = mConnectRequest.getRetryDelay(); + if (delay > 0) + log(Log.DEBUG, "wait(" + delay + ")"); + mHandler.postDelayed(() -> internalConnect(gatt.getDevice(), mConnectRequest), delay); + return; + } + + mOperationInProgress = true; // no more calls are possible + cancelQueue(); + mInitQueue = null; + mReady = false; + + // Store the current value of the mConnected flag... + final boolean wasConnected = mConnected; + // ...because this method sets the mConnected flag to false. + notifyDeviceDisconnected(gatt.getDevice()); // this may call close() + + // Signal the current request, if any. + if (mRequest != null) { + if (mRequest.type != Request.Type.DISCONNECT && mRequest.type != Request.Type.REMOVE_BOND) { + // The CONNECT request is notified below. + // The DISCONNECT request is notified below in + // notifyDeviceDisconnected(BluetoothDevice). + // The REMOVE_BOND request will be notified when the bond state changes + // to BOND_NONE in the broadcast received on the top of this file. + mRequest.notifyFail(gatt.getDevice(), + status == BluetoothGatt.GATT_SUCCESS ? + FailCallback.REASON_DEVICE_DISCONNECTED : status); + mRequest = null; + } + } + if (mValueChangedRequest != null) { + mValueChangedRequest.notifyFail(mBluetoothDevice, FailCallback.REASON_DEVICE_DISCONNECTED); + mValueChangedRequest = null; + } + if (mConnectRequest != null) { + int reason; + if (mServicesDiscovered) + reason = FailCallback.REASON_DEVICE_NOT_SUPPORTED; + else if (status == BluetoothGatt.GATT_SUCCESS) + reason = FailCallback.REASON_DEVICE_DISCONNECTED; + else if (status == GattError.GATT_ERROR && timeout) + reason = FailCallback.REASON_TIMEOUT; + else + reason = status; + mConnectRequest.notifyFail(gatt.getDevice(), reason); + mConnectRequest = null; + } + + // Reset flag, so the next Connect could be enqueued. + mOperationInProgress = false; + // Try to reconnect if the initial connection was lost because of a link loss, + // and shouldAutoConnect() returned true during connection attempt. + // This time it will set the autoConnect flag to true (gatt.connect() forces + // autoConnect true). + if (wasConnected && mInitialConnection) { + internalConnect(gatt.getDevice(), null); + } else { + mInitialConnection = false; + nextRequest(false); + } + + if (wasConnected || status == BluetoothGatt.GATT_SUCCESS) + return; + } else { + if (status != BluetoothGatt.GATT_SUCCESS) + log(Log.ERROR, "Error (0x" + Integer.toHexString(status) + "): " + + GattError.parseConnectionError(status)); + } + mCallbacks.onError(gatt.getDevice(), ERROR_CONNECTION_STATE_CHANGE, status); + } + } + + @Override + final void onServicesDiscoveredSafe(@NonNull final BluetoothGatt gatt, final int status) { + mServiceDiscoveryRequested = false; + if (status == BluetoothGatt.GATT_SUCCESS) { + log(Log.INFO, "Services discovered"); + mServicesDiscovered = true; + if (isRequiredServiceSupported(gatt)) { + log(Log.VERBOSE, "Primary service found"); + final boolean optionalServicesFound = isOptionalServiceSupported(gatt); + if (optionalServicesFound) + log(Log.VERBOSE, "Secondary service found"); + + // Notify the parent activity. + mCallbacks.onServicesDiscovered(gatt.getDevice(), optionalServicesFound); + + // Obtain the queue of initialization requests. + // First, let's call the deprecated initGatt(...). + mInitInProgress = true; + mOperationInProgress = true; + mInitQueue = initGatt(gatt); + + final boolean deprecatedApiUsed = mInitQueue != null; + if (deprecatedApiUsed) { + for (final Request request : mInitQueue) { + request.enqueued = true; + } + } + + if (mInitQueue == null) + mInitQueue = new LinkedList<>(); + + // Before we start executing the initialization queue some other tasks + // need to be done. + // Note, that operations are added in reverse order to the front of the queue. + + // 1. On devices running Android 4.3-5.x, 8.x and 9.0 the Service Changed + // characteristic needs to be enabled by the app (for bonded devices). + // The request will be ignored if there is no Service Changed characteristic. + // This "fix" broke this in Android 8: + // https://android-review.googlesource.com/c/platform/system/bt/+/239970 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || Build.VERSION.SDK_INT == Build.VERSION_CODES.O + || Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1 + || Build.VERSION.SDK_INT == Build.VERSION_CODES.P) + enqueueFirst(Request.newEnableServiceChangedIndicationsRequest() + .setManager(BleManager.this)); + + // Deprecated: + if (deprecatedApiUsed) { + // All Battery Service handling will be removed from BleManager in the future. + // If you want to read/enable notifications on Battery Level characteristic + // do this in initialize(...). + + // 2. Read Battery Level characteristic (if such does not exist, this will + // be skipped) + readBatteryLevel(); + // 3. Enable Battery Level notifications if required (if this char. does not + // exist, this operation will be skipped) + if (mCallbacks.shouldEnableBatteryLevelNotifications(gatt.getDevice())) + enableBatteryLevelNotifications(); + } + // End + + initialize(); + mInitInProgress = false; + nextRequest(true); + } else { + log(Log.WARN, "Device is not supported"); + mCallbacks.onDeviceNotSupported(gatt.getDevice()); + internalDisconnect(); + } + } else { + Log.e(TAG, "onServicesDiscovered error " + status); + onError(gatt.getDevice(), ERROR_DISCOVERY_SERVICE, status); + if (mConnectRequest != null) { + mConnectRequest.notifyFail(gatt.getDevice(), FailCallback.REASON_REQUEST_FAILED); + mConnectRequest = null; + } + internalDisconnect(); + } + } + + @Override + final void onCharacteristicReadSafe(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] data, final int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + log(Log.INFO, "Read Response received from " + characteristic.getUuid() + + ", value: " + ParserUtils.parse(data)); + + onCharacteristicRead(gatt, characteristic); + if (mRequest instanceof ReadRequest) { + final ReadRequest request = (ReadRequest) mRequest; + final boolean matches = request.matches(data); + if (matches) { + request.notifyValueChanged(gatt.getDevice(), data); + } + if (!matches || request.hasMore()) { + enqueueFirst(request); + } else { + request.notifySuccess(gatt.getDevice()); + } + } + } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION + || status == 8 /* GATT INSUF AUTHORIZATION */ + || status == 137 /* GATT AUTH FAIL */) { + log(Log.WARN, "Authentication required (" + status + ")"); + if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) { + // This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352 + Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED); + mCallbacks.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status); + } + // The request will be repeated when the bond state changes to BONDED. + return; + } else { + Log.e(TAG, "onCharacteristicRead error " + status); + if (mRequest instanceof ReadRequest) { + mRequest.notifyFail(gatt.getDevice(), status); + } + mValueChangedRequest = null; + onError(gatt.getDevice(), ERROR_READ_CHARACTERISTIC, status); + } + nextRequest(true); + } + + @Override + final void onCharacteristicWriteSafe(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] data, final int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + log(Log.INFO, "Data written to " + characteristic.getUuid() + + ", value: " + ParserUtils.parse(data)); + + onCharacteristicWrite(gatt, characteristic); + if (mRequest instanceof WriteRequest) { + final WriteRequest request = (WriteRequest) mRequest; + final boolean valid = request.notifyPacketSent(gatt.getDevice(), data); + if (!valid && mRequestQueue instanceof ReliableWriteRequest) { + request.notifyFail(gatt.getDevice(), FailCallback.REASON_VALIDATION); + mRequestQueue.cancelQueue(); + } else if (request.hasMore()) { + enqueueFirst(request); + } else { + request.notifySuccess(gatt.getDevice()); + } + } + } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION + || status == 8 /* GATT INSUF AUTHORIZATION */ + || status == 137 /* GATT AUTH FAIL */) { + log(Log.WARN, "Authentication required (" + status + ")"); + if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) { + // This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352 + Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED); + mCallbacks.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status); + } + // The request will be repeated when the bond state changes to BONDED. + return; + } else { + Log.e(TAG, "onCharacteristicWrite error " + status); + if (mRequest instanceof WriteRequest) { + mRequest.notifyFail(gatt.getDevice(), status); + // Automatically abort Reliable Write when write error happen + if (mRequestQueue instanceof ReliableWriteRequest) + mRequestQueue.cancelQueue(); + } + mValueChangedRequest = null; + onError(gatt.getDevice(), ERROR_WRITE_CHARACTERISTIC, status); + } + nextRequest(true); + } + + @Override + final void onReliableWriteCompletedSafe(@NonNull final BluetoothGatt gatt, + final int status) { + final boolean execute = mRequest.type == Request.Type.EXECUTE_RELIABLE_WRITE; + mReliableWriteInProgress = false; + if (status == BluetoothGatt.GATT_SUCCESS) { + if (execute) { + log(Log.INFO, "Reliable Write executed"); + mRequest.notifySuccess(gatt.getDevice()); + } else { + log(Log.WARN, "Reliable Write aborted"); + mRequest.notifySuccess(gatt.getDevice()); + mRequestQueue.notifyFail(gatt.getDevice(), FailCallback.REASON_REQUEST_FAILED); + } + } else { + Log.e(TAG, "onReliableWriteCompleted execute " + execute + ", error " + status); + mRequest.notifyFail(gatt.getDevice(), status); + onError(gatt.getDevice(), ERROR_RELIABLE_WRITE, status); + } + nextRequest(true); + } + + @Override + void onDescriptorReadSafe(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattDescriptor descriptor, + @Nullable final byte[] data, final int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + log(Log.INFO, "Read Response received from descr. " + descriptor.getUuid() + + ", value: " + ParserUtils.parse(data)); + + onDescriptorRead(gatt, descriptor); + if (mRequest instanceof ReadRequest) { + final ReadRequest request = (ReadRequest) mRequest; + request.notifyValueChanged(gatt.getDevice(), data); + if (request.hasMore()) { + enqueueFirst(request); + } else { + request.notifySuccess(gatt.getDevice()); + } + } + } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION + || status == 8 /* GATT INSUF AUTHORIZATION */ + || status == 137 /* GATT AUTH FAIL */) { + log(Log.WARN, "Authentication required (" + status + ")"); + if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) { + // This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352 + Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED); + mCallbacks.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status); + } + // The request will be repeated when the bond state changes to BONDED. + return; + } else { + Log.e(TAG, "onDescriptorRead error " + status); + if (mRequest instanceof ReadRequest) { + mRequest.notifyFail(gatt.getDevice(), status); + } + mValueChangedRequest = null; + onError(gatt.getDevice(), ERROR_READ_DESCRIPTOR, status); + } + nextRequest(true); + } + + @Override + final void onDescriptorWriteSafe(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattDescriptor descriptor, + @Nullable final byte[] data, final int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + log(Log.INFO, "Data written to descr. " + descriptor.getUuid() + + ", value: " + ParserUtils.parse(data)); + + if (isServiceChangedCCCD(descriptor)) { + log(Log.INFO, "Service Changed notifications enabled"); + } else if (isCCCD(descriptor)) { + if (data != null && data.length == 2 && data[1] == 0x00) { + switch (data[0]) { + case 0x00: + mNotificationCallbacks.remove(descriptor.getCharacteristic()); + log(Log.INFO, "Notifications and indications disabled"); + break; + case 0x01: + log(Log.INFO, "Notifications enabled"); + break; + case 0x02: + log(Log.INFO, "Indications enabled"); + break; + } + onDescriptorWrite(gatt, descriptor); + } + } else { + onDescriptorWrite(gatt, descriptor); + } + if (mRequest instanceof WriteRequest) { + final WriteRequest request = (WriteRequest) mRequest; + final boolean valid = request.notifyPacketSent(gatt.getDevice(), data); + if (!valid && mRequestQueue instanceof ReliableWriteRequest) { + request.notifyFail(gatt.getDevice(), FailCallback.REASON_VALIDATION); + mRequestQueue.cancelQueue(); + } else if (request.hasMore()) { + enqueueFirst(request); + } else { + request.notifySuccess(gatt.getDevice()); + } + } + } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION + || status == 8 /* GATT INSUF AUTHORIZATION */ + || status == 137 /* GATT AUTH FAIL */) { + log(Log.WARN, "Authentication required (" + status + ")"); + if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) { + // This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352 + Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED); + mCallbacks.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status); + } + // The request will be repeated when the bond state changes to BONDED. + return; + } else { + Log.e(TAG, "onDescriptorWrite error " + status); + if (mRequest instanceof WriteRequest) { + mRequest.notifyFail(gatt.getDevice(), status); + // Automatically abort Reliable Write when write error happen + if (mRequestQueue instanceof ReliableWriteRequest) + mRequestQueue.cancelQueue(); + } + mValueChangedRequest = null; + onError(gatt.getDevice(), ERROR_WRITE_DESCRIPTOR, status); + } + nextRequest(true); + } + + @Override + final void onCharacteristicChangedSafe(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] data) { + if (isServiceChangedCharacteristic(characteristic)) { + // TODO this should be tested. Should services be invalidated? + // Forbid enqueuing more operations. + mOperationInProgress = true; + // Clear queues, services are no longer valid. + cancelQueue(); + mInitQueue = null; + log(Log.INFO, "Service Changed indication received"); + log(Log.VERBOSE, "Discovering Services..."); + log(Log.DEBUG, "gatt.discoverServices()"); + gatt.discoverServices(); + } else { + final BluetoothGattDescriptor cccd = + characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID); + final boolean notifications = cccd == null || cccd.getValue() == null || + cccd.getValue().length != 2 || cccd.getValue()[0] == 0x01; + + final String dataString = ParserUtils.parse(data); + if (notifications) { + log(Log.INFO, "Notification received from " + + characteristic.getUuid() + ", value: " + dataString); + onCharacteristicNotified(gatt, characteristic); + } else { // indications + log(Log.INFO, "Indication received from " + + characteristic.getUuid() + ", value: " + dataString); + onCharacteristicIndicated(gatt, characteristic); + } + if (mBatteryLevelNotificationCallback != null && isBatteryLevelCharacteristic(characteristic)) { + mBatteryLevelNotificationCallback.notifyValueChanged(gatt.getDevice(), data); + } + // Notify the notification registered listener, if set + final ValueChangedCallback request = mNotificationCallbacks.get(characteristic); + if (request != null && request.matches(data)) { + request.notifyValueChanged(gatt.getDevice(), data); + } + // If there is a value change request, + final WaitForValueChangedRequest valueChangedRequest = mValueChangedRequest; + if (valueChangedRequest != null + // registered for this characteristic + && valueChangedRequest.characteristic == characteristic + // and didn't have a trigger, or the trigger was started + // (not necessarily completed) + && !valueChangedRequest.isTriggerPending() + // and the data matches the filter (if set) + && valueChangedRequest.matches(data)) { + // notify that new data was received. + valueChangedRequest.notifyValueChanged(gatt.getDevice(), data); + + // If no more data are expected + if (!valueChangedRequest.hasMore()) { + // notify success, + valueChangedRequest.notifySuccess(gatt.getDevice()); + // and proceed to the next request only if the trigger has completed. + // Otherwise, the next request will be started when the request's callback + // will be received. + mValueChangedRequest = null; + if (valueChangedRequest.isTriggerCompleteOrNull()) { + nextRequest(true); + } + } + } + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + final void onMtuChangedSafe(@NonNull final BluetoothGatt gatt, + @IntRange(from = 23, to = 517) final int mtu, + final int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + log(Log.INFO, "MTU changed to: " + mtu); + mMtu = mtu; + onMtuChanged(gatt, mtu); + if (mRequest instanceof MtuRequest) { + ((MtuRequest) mRequest).notifyMtuChanged(gatt.getDevice(), mtu); + mRequest.notifySuccess(gatt.getDevice()); + } + } else { + Log.e(TAG, "onMtuChanged error: " + status + ", mtu: " + mtu); + if (mRequest instanceof MtuRequest) { + mRequest.notifyFail(gatt.getDevice(), status); + mValueChangedRequest = null; + } + onError(gatt.getDevice(), ERROR_MTU_REQUEST, status); + } + nextRequest(true); + } + + /** + * Callback indicating the connection parameters were updated. Works on Android 8+. + * + * @param gatt GATT client involved. + * @param interval Connection interval used on this connection, 1.25ms unit. + * Valid range is from 6 (7.5ms) to 3200 (4000ms). + * @param latency Slave latency for the connection in number of connection events. + * Valid range is from 0 to 499. + * @param timeout Supervision timeout for this connection, in 10ms unit. + * Valid range is from 10 (0.1s) to 3200 (32s) + * @param status {@link BluetoothGatt#GATT_SUCCESS} if the connection has been updated + * successfully. + */ + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + final void onConnectionUpdatedSafe(@NonNull final BluetoothGatt gatt, + @IntRange(from = 6, to = 3200) final int interval, + @IntRange(from = 0, to = 499) final int latency, + @IntRange(from = 10, to = 3200) final int timeout, + final int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + log(Log.INFO, "Connection parameters updated " + + "(interval: " + (interval * 1.25) + "ms," + + " latency: " + latency + ", timeout: " + (timeout * 10) + "ms)"); + onConnectionUpdated(gatt, interval, latency, timeout); + + // This callback may be called af any time, also when some other request is executed + if (mRequest instanceof ConnectionPriorityRequest) { + ((ConnectionPriorityRequest) mRequest) + .notifyConnectionPriorityChanged(gatt.getDevice(), interval, latency, timeout); + mRequest.notifySuccess(gatt.getDevice()); + } + } else if (status == 0x3b) { // HCI_ERR_UNACCEPT_CONN_INTERVAL + Log.e(TAG, "onConnectionUpdated received status: Unacceptable connection interval, " + + "interval: " + interval + ", latency: " + latency + ", timeout: " + timeout); + log(Log.WARN, "Connection parameters update failed with status: " + + "UNACCEPT CONN INTERVAL (0x3b) (interval: " + (interval * 1.25) + "ms, " + + "latency: " + latency + ", timeout: " + (timeout * 10) + "ms)"); + + // This callback may be called af any time, also when some other request is executed + if (mRequest instanceof ConnectionPriorityRequest) { + mRequest.notifyFail(gatt.getDevice(), status); + mValueChangedRequest = null; + } + } else { + Log.e(TAG, "onConnectionUpdated received status: " + status + ", " + + "interval: " + interval + ", latency: " + latency + ", timeout: " + timeout); + log(Log.WARN, "Connection parameters update failed with " + + "status " + status + " (interval: " + (interval * 1.25) + "ms, " + + "latency: " + latency + ", timeout: " + (timeout * 10) + "ms)"); + + // This callback may be called af any time, also when some other request is executed + if (mRequest instanceof ConnectionPriorityRequest) { + mRequest.notifyFail(gatt.getDevice(), status); + mValueChangedRequest = null; + } + mCallbacks.onError(gatt.getDevice(), ERROR_CONNECTION_PRIORITY_REQUEST, status); + } + if (mConnectionPriorityOperationInProgress) { + mConnectionPriorityOperationInProgress = false; + nextRequest(true); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + final void onPhyUpdateSafe(@NonNull final BluetoothGatt gatt, + @PhyValue final int txPhy, @PhyValue final int rxPhy, + final int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + log(Log.INFO, "PHY updated (TX: " + phyToString(txPhy) + + ", RX: " + phyToString(rxPhy) + ")"); + if (mRequest instanceof PhyRequest) { + ((PhyRequest) mRequest).notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy); + mRequest.notifySuccess(gatt.getDevice()); + } + } else { + log(Log.WARN, "PHY updated failed with status " + status); + if (mRequest instanceof PhyRequest) { + mRequest.notifyFail(gatt.getDevice(), status); + mValueChangedRequest = null; + } + mCallbacks.onError(gatt.getDevice(), ERROR_PHY_UPDATE, status); + } + // PHY update may be requested by the other side, or the Android, without explicitly + // requesting it. Proceed with the queue only when update was requested. + if (mRequest instanceof PhyRequest) { + nextRequest(true); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + final void onPhyReadSafe(@NonNull final BluetoothGatt gatt, + @PhyValue final int txPhy, @PhyValue final int rxPhy, + final int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + log(Log.INFO, "PHY read (TX: " + phyToString(txPhy) + + ", RX: " + phyToString(rxPhy) + ")"); + if (mRequest instanceof PhyRequest) { + ((PhyRequest) mRequest).notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy); + mRequest.notifySuccess(gatt.getDevice()); + } + } else { + log(Log.WARN, "PHY read failed with status " + status); + if (mRequest instanceof PhyRequest) { + mRequest.notifyFail(gatt.getDevice(), status); + } + mValueChangedRequest = null; + mCallbacks.onError(gatt.getDevice(), ERROR_READ_PHY, status); + } + nextRequest(true); + } + + @Override + final void onReadRemoteRssiSafe(@NonNull final BluetoothGatt gatt, + @IntRange(from = -128, to = 20) final int rssi, + final int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + log(Log.INFO, "Remote RSSI received: " + rssi + " dBm"); + if (mRequest instanceof ReadRssiRequest) { + ((ReadRssiRequest) mRequest).notifyRssiRead(gatt.getDevice(), rssi); + mRequest.notifySuccess(gatt.getDevice()); + } + } else { + log(Log.WARN, "Reading remote RSSI failed with status " + status); + if (mRequest instanceof ReadRssiRequest) { + mRequest.notifyFail(gatt.getDevice(), status); + } + mValueChangedRequest = null; + mCallbacks.onError(gatt.getDevice(), ERROR_READ_RSSI, status); + } + nextRequest(true); + } + + /** + * Enqueues the given request at the front of the the init or task queue, depending + * on whether the initialization is in progress, or not. + * + * @param request the request to be added. + */ + private void enqueueFirst(@NonNull final Request request) { + final Deque queue = mInitInProgress ? mInitQueue : mTaskQueue; + queue.addFirst(request); + request.enqueued = true; + } + + /** + * Enqueues the given request at the end of the the init or task queue, depending + * on whether the initialization is in progress, or not. + * + * @param request the request to be added. + */ + private void enqueue(@NonNull final Request request) { + final Deque queue = mInitInProgress ? mInitQueue : mTaskQueue; + queue.add(request); + request.enqueued = true; + } + + /** + * Removes all enqueued requests from the queue. + */ + protected void cancelQueue() { + mTaskQueue.clear(); + } + + /** + * Executes the next request. If the last element from the initialization queue has + * been executed the {@link #onDeviceReady()} callback is called. + */ + @SuppressWarnings("ConstantConditions") + @MainThread + private synchronized void nextRequest(final boolean force) { + // Is the manager closed()? + if (mGattCallback == null) { + return; + } + + if (force) { + mOperationInProgress = mValueChangedRequest != null; + } + + if (mOperationInProgress) { + return; + } + + // Get the first request from the init queue + Request request = null; + try { + // If Request set is present, try taking next request from it + if (mRequestQueue != null) { + if (mRequestQueue.hasMore()) { + request = mRequestQueue.getNext().setManager(BleManager.this); + } else { + // Set is completed + mRequestQueue.notifySuccess(mBluetoothDevice); + mRequestQueue = null; + } + } + // Request wasn't obtained from the request set? Take next one from the queue. + if (request == null) { + request = mInitQueue != null ? mInitQueue.poll() : null; + } + } catch (final Exception e) { + // On older Android versions poll() may in some cases throw NoSuchElementException, + // as it's using removeFirst() internally. + // See: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/37 + request = null; + } + + // Are we done with initializing? + if (request == null) { + if (mInitQueue != null) { + mInitQueue = null; // release the queue + + // Set the 'operation in progress' flag, so any request made in onDeviceReady() + // will not start new nextRequest() call. + mOperationInProgress = true; + mReady = true; + onDeviceReady(); + if (mConnectRequest != null) { + mConnectRequest.notifySuccess(mConnectRequest.getDevice()); + mConnectRequest = null; + } + } + // If so, we can continue with the task queue + try { + request = mTaskQueue.remove(); + } catch (final Exception e) { + // No more tasks to perform + mOperationInProgress = false; + mRequest = null; + onManagerReady(); + return; + } + } + + boolean result = false; + mOperationInProgress = true; + mRequest = request; + + // The WAIT_FOR_* request types may override the request with a trigger. + // This is to ensure that the trigger is done after the mValueChangedRequest was set. + int requiredProperty = BluetoothGattCharacteristic.PROPERTY_NOTIFY; + switch (request.type) { + case WAIT_FOR_INDICATION: + requiredProperty = BluetoothGattCharacteristic.PROPERTY_INDICATE; + // fall-through intended + case WAIT_FOR_NOTIFICATION: { + final WaitForValueChangedRequest r = (WaitForValueChangedRequest) request; + result = mConnected && mBluetoothDevice != null + && getCccd(r.characteristic, requiredProperty) != null; + if (result) { + mValueChangedRequest = r; + // Call notifyStarted for the WaitForValueChangedRequest. + r.notifyStarted(mBluetoothDevice); + + if (r.getTrigger() == null) + break; + + // If the request has another request set as a trigger, update the + // request with the trigger. + request = r.getTrigger(); + mRequest = request; + } + // fall-through intended + } + default: { + // Call notifyStarted on the request before it's executed. + if (request.type == Request.Type.CONNECT) { + // When the Connect Request is started, the mBluetoothDevice is not set yet. + // It may also be a connect request to a different device, which is an error + // that is handled in internalConnect() + final ConnectRequest cr = (ConnectRequest) request; + cr.notifyStarted(cr.getDevice()); + } else { + if (mBluetoothDevice != null) { + request.notifyStarted(mBluetoothDevice); + } else { + // The device wasn't connected before. Target is unknown. + request.notifyInvalidRequest(); + + mValueChangedRequest = null; + nextRequest(true); + return; + } + } + } + } + + switch (request.type) { + case CONNECT: { + final ConnectRequest cr = (ConnectRequest) request; + mConnectRequest = cr; + mRequest = null; + result = internalConnect(cr.getDevice(), cr); + break; + } + case DISCONNECT: { + result = internalDisconnect(); + break; + } + case CREATE_BOND: { + result = internalCreateBond(); + break; + } + case REMOVE_BOND: { + result = internalRemoveBond(); + break; + } + case SET: { + mRequestQueue = (RequestQueue) request; + nextRequest(true); + return; + } + case READ: { + result = internalReadCharacteristic(request.characteristic); + break; + } + case WRITE: { + final WriteRequest wr = (WriteRequest) request; + final BluetoothGattCharacteristic characteristic = request.characteristic; + if (characteristic != null) { + characteristic.setValue(wr.getData(mMtu)); + characteristic.setWriteType(wr.getWriteType()); + } + result = internalWriteCharacteristic(characteristic); + break; + } + case READ_DESCRIPTOR: { + result = internalReadDescriptor(request.descriptor); + break; + } + case WRITE_DESCRIPTOR: { + final WriteRequest wr = (WriteRequest) request; + final BluetoothGattDescriptor descriptor = request.descriptor; + if (descriptor != null) { + descriptor.setValue(wr.getData(mMtu)); + } + result = internalWriteDescriptor(descriptor); + break; + } + case BEGIN_RELIABLE_WRITE: { + result = internalBeginReliableWrite(); + // There is no callback for begin reliable write request. + // Notify success and start next request immediately. + if (result) { + mRequest.notifySuccess(mBluetoothDevice); + nextRequest(true); + return; + } + break; + } + case EXECUTE_RELIABLE_WRITE: { + result = internalExecuteReliableWrite(); + break; + } + case ABORT_RELIABLE_WRITE: { + result = internalAbortReliableWrite(); + break; + } + case ENABLE_NOTIFICATIONS: { + result = internalEnableNotifications(request.characteristic); + break; + } + case ENABLE_INDICATIONS: { + result = internalEnableIndications(request.characteristic); + break; + } + case DISABLE_NOTIFICATIONS: { + result = internalDisableNotifications(request.characteristic); + break; + } + case DISABLE_INDICATIONS: { + result = internalDisableIndications(request.characteristic); + break; + } + case READ_BATTERY_LEVEL: { + result = internalReadBatteryLevel(); + break; + } + case ENABLE_BATTERY_LEVEL_NOTIFICATIONS: { + result = internalSetBatteryNotifications(true); + break; + } + case DISABLE_BATTERY_LEVEL_NOTIFICATIONS: { + result = internalSetBatteryNotifications(false); + break; + } + case ENABLE_SERVICE_CHANGED_INDICATIONS: { + result = ensureServiceChangedEnabled(); + break; + } + case REQUEST_MTU: { + final MtuRequest mr = (MtuRequest) request; + if (mMtu != mr.getRequiredMtu() + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + result = internalRequestMtu(mr.getRequiredMtu()); + } else { + result = mConnected; + if (result) { + mr.notifyMtuChanged(mBluetoothDevice, mMtu); + mr.notifySuccess(mBluetoothDevice); + nextRequest(true); + return; + } + } + break; + } + case REQUEST_CONNECTION_PRIORITY: { + final ConnectionPriorityRequest cpr = (ConnectionPriorityRequest) request; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mConnectionPriorityOperationInProgress = true; + result = internalRequestConnectionPriority(cpr.getRequiredPriority()); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + result = internalRequestConnectionPriority(cpr.getRequiredPriority()); + // There is no callback for requestConnectionPriority(...) before Android Oreo. + // Let's give it some time to finish as the request is an asynchronous operation. + if (result) { + final BluetoothDevice device = mBluetoothDevice; + mHandler.postDelayed(() -> { + cpr.notifySuccess(device); + nextRequest(true); + }, 100); + } + } + break; + } + case SET_PREFERRED_PHY: { + final PhyRequest pr = (PhyRequest) request; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + result = internalSetPreferredPhy(pr.getPreferredTxPhy(), + pr.getPreferredRxPhy(), pr.getPreferredPhyOptions()); + } else { + result = mConnected; + if (result) { + pr.notifyLegacyPhy(mBluetoothDevice); + pr.notifySuccess(mBluetoothDevice); + nextRequest(true); + return; + } + } + break; + } + case READ_PHY: { + final PhyRequest pr = (PhyRequest) request; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + result = internalReadPhy(); + } else { + result = mConnected; + if (result) { + pr.notifyLegacyPhy(mBluetoothDevice); + pr.notifySuccess(mBluetoothDevice); + nextRequest(true); + return; + } + } + break; + } + case READ_RSSI: { + result = internalReadRssi(); + break; + } + case REFRESH_CACHE: { + result = internalRefreshDeviceCache(); + if (result) { + final BluetoothDevice device = mBluetoothDevice; + mHandler.postDelayed(() -> { + log(Log.INFO, "Cache refreshed"); + mRequest.notifySuccess(device); + mRequest = null; + if (mValueChangedRequest != null) { + mValueChangedRequest.notifyFail(device, FailCallback.REASON_NULL_ATTRIBUTE); + mValueChangedRequest = null; + } + cancelQueue(); + mInitQueue = null; + if (mConnected) { + // Invalidate all services and characteristics + onDeviceDisconnected(); + // And discover services again + log(Log.VERBOSE, "Discovering Services..."); + log(Log.DEBUG, "gatt.discoverServices()"); + mBluetoothGatt.discoverServices(); + } + }, 200); + } + break; + } + case SLEEP: { + final BluetoothDevice device = mBluetoothDevice; + if (device != null) { + final SleepRequest sr = (SleepRequest) request; + log(Log.DEBUG, "sleep(" + sr.getDelay() + ")"); + mHandler.postDelayed(() -> { + sr.notifySuccess(device); + nextRequest(true); + }, sr.getDelay()); + result = true; + } + break; + } + case WAIT_FOR_NOTIFICATION: + case WAIT_FOR_INDICATION: + // Those were handled before. + break; + } + // The result may be false if given characteristic or descriptor were not found + // on the device, or the feature is not supported on the Android. + // In that case, proceed with next operation and ignore the one that failed. + if (!result) { + mRequest.notifyFail(mBluetoothDevice, + mConnected ? + FailCallback.REASON_NULL_ATTRIBUTE : + BluetoothAdapter.getDefaultAdapter().isEnabled() ? + FailCallback.REASON_DEVICE_DISCONNECTED : + FailCallback.REASON_BLUETOOTH_DISABLED); + mValueChangedRequest = null; + mConnectionPriorityOperationInProgress = false; + nextRequest(true); + } + } + + /** + * Returns true if this descriptor is from the Service Changed characteristic. + * + * @param descriptor the descriptor to be checked + * @return true if the descriptor belongs to the Service Changed characteristic + */ + private boolean isServiceChangedCCCD(@Nullable final BluetoothGattDescriptor descriptor) { + return descriptor != null && + SERVICE_CHANGED_CHARACTERISTIC.equals(descriptor.getCharacteristic().getUuid()); + } + + /** + * Returns true if this is the Service Changed characteristic. + * + * @param characteristic the characteristic to be checked + * @return true if it is the Service Changed characteristic + */ + private boolean isServiceChangedCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic) { + return characteristic != null && + SERVICE_CHANGED_CHARACTERISTIC.equals(characteristic.getUuid()); + } + + /** + * Returns true if the characteristic is the Battery Level characteristic. + * + * @param characteristic the characteristic to be checked + * @return true if the characteristic is the Battery Level characteristic. + */ + @Deprecated + private boolean isBatteryLevelCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic) { + return characteristic != null && + BATTERY_LEVEL_CHARACTERISTIC.equals(characteristic.getUuid()); + } + + /** + * Returns true if this descriptor is a Client Characteristic Configuration descriptor (CCCD). + * + * @param descriptor the descriptor to be checked + * @return true if the descriptor is a CCCD + */ + private boolean isCCCD(final BluetoothGattDescriptor descriptor) { + return descriptor != null && + CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID.equals(descriptor.getUuid()); + } + } + + protected static final int PAIRING_VARIANT_PIN = 0; + protected static final int PAIRING_VARIANT_PASSKEY = 1; + protected static final int PAIRING_VARIANT_PASSKEY_CONFIRMATION = 2; + protected static final int PAIRING_VARIANT_CONSENT = 3; + protected static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4; + protected static final int PAIRING_VARIANT_DISPLAY_PIN = 5; + protected static final int PAIRING_VARIANT_OOB_CONSENT = 6; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + PAIRING_VARIANT_PIN, + PAIRING_VARIANT_PASSKEY, + PAIRING_VARIANT_PASSKEY_CONFIRMATION, + PAIRING_VARIANT_CONSENT, + PAIRING_VARIANT_DISPLAY_PASSKEY, + PAIRING_VARIANT_DISPLAY_PIN, + PAIRING_VARIANT_OOB_CONSENT + }) + public @interface PairingVariant {} + + protected String pairingVariantToString(@PairingVariant final int variant) { + switch (variant) { + case PAIRING_VARIANT_PIN: + return "PAIRING_VARIANT_PIN"; + case PAIRING_VARIANT_PASSKEY: + return "PAIRING_VARIANT_PASSKEY"; + case PAIRING_VARIANT_PASSKEY_CONFIRMATION: + return "PAIRING_VARIANT_PASSKEY_CONFIRMATION"; + case PAIRING_VARIANT_CONSENT: + return "PAIRING_VARIANT_CONSENT"; + case PAIRING_VARIANT_DISPLAY_PASSKEY: + return "PAIRING_VARIANT_DISPLAY_PASSKEY"; + case PAIRING_VARIANT_DISPLAY_PIN: + return "PAIRING_VARIANT_DISPLAY_PIN"; + case PAIRING_VARIANT_OOB_CONSENT: + return "PAIRING_VARIANT_OOB_CONSENT"; + default: + return "UNKNOWN"; + } + } + + protected String bondStateToString(final int state) { + switch (state) { + case BluetoothDevice.BOND_NONE: + return "BOND_NONE"; + case BluetoothDevice.BOND_BONDING: + return "BOND_BONDING"; + case BluetoothDevice.BOND_BONDED: + return "BOND_BONDED"; + default: + return "UNKNOWN"; + } + } + + protected String writeTypeToString(final int type) { + switch (type) { + case BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT: + return "WRITE REQUEST"; + case BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE: + return "WRITE COMMAND"; + case BluetoothGattCharacteristic.WRITE_TYPE_SIGNED: + return "WRITE SIGNED"; + default: + return "UNKNOWN: " + type; + } + } + + /** + * Converts the connection state to String value. + * + * @param state the connection state + * @return state as String + */ + protected String stateToString(final int state) { + switch (state) { + case BluetoothProfile.STATE_CONNECTED: + return "CONNECTED"; + case BluetoothProfile.STATE_CONNECTING: + return "CONNECTING"; + case BluetoothProfile.STATE_DISCONNECTING: + return "DISCONNECTING"; + default: + return "DISCONNECTED"; + } + } + + /** + * Converts the PHY number to String value. + * + * @param phy phy value + * @return phy as String + */ + @RequiresApi(value = Build.VERSION_CODES.O) + private String phyToString(final int phy) { + switch (phy) { + case BluetoothDevice.PHY_LE_1M: + return "LE 1M"; + case BluetoothDevice.PHY_LE_2M: + return "LE 2M"; + case BluetoothDevice.PHY_LE_CODED: + return "LE Coded"; + default: + return "UNKNOWN (" + phy + ")"; + } + } + + @RequiresApi(value = Build.VERSION_CODES.O) + private String phyMaskToString(final int mask) { + switch (mask) { + case BluetoothDevice.PHY_LE_1M_MASK: + return "LE 1M"; + case BluetoothDevice.PHY_LE_2M_MASK: + return "LE 2M"; + case BluetoothDevice.PHY_LE_CODED_MASK: + return "LE Coded"; + case BluetoothDevice.PHY_LE_1M_MASK | BluetoothDevice.PHY_LE_2M_MASK: + return "LE 1M or LE 2M"; + case BluetoothDevice.PHY_LE_1M_MASK | BluetoothDevice.PHY_LE_CODED_MASK: + return "LE 1M or LE Coded"; + case BluetoothDevice.PHY_LE_2M_MASK | BluetoothDevice.PHY_LE_CODED_MASK: + return "LE 2M or LE Coded"; + case BluetoothDevice.PHY_LE_1M_MASK | BluetoothDevice.PHY_LE_2M_MASK + | BluetoothDevice.PHY_LE_CODED_MASK: + return "LE 1M, LE 2M or LE Coded"; + default: + return "UNKNOWN (" + mask + ")"; + } + } + + @RequiresApi(value = Build.VERSION_CODES.O) + private String phyCodedOptionToString(final int option) { + switch (option) { + case BluetoothDevice.PHY_OPTION_NO_PREFERRED: + return "No preferred"; + case BluetoothDevice.PHY_OPTION_S2: + return "S2"; + case BluetoothDevice.PHY_OPTION_S8: + return "S8"; + default: + return "UNKNOWN (" + option + ")"; + } + } +} diff --git a/_android/src/no/nordicsemi/android/ble/BleManagerCallbacks.java b/_android/src/no/nordicsemi/android/ble/BleManagerCallbacks.java new file mode 100644 index 0000000..e98035a --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/BleManagerCallbacks.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +// DO NOT EDIT THIS FILE UNLESS NECESSARY! +/** + * The BleManagerCallbacks should be overridden in your app and all the 'high level' callbacks + * should be added there. See examples in Android nRF Blinky or Android nRF Toolbox. + *

+ * Keeping this file as is (and {@link BleManager} as well) will allow to quickly update it when + * an update is posted here. + */ +@SuppressWarnings({"DeprecatedIsStillUsed", "unused"}) +public interface BleManagerCallbacks { + + /** + * Called when the Android device started connecting to given device. + * The {@link #onDeviceConnected(BluetoothDevice)} will be called when the device is connected, + * or {@link #onError(BluetoothDevice, String, int)} in case of error. + * + * @param device the device that got connected. + */ + void onDeviceConnecting(@NonNull final BluetoothDevice device); + + /** + * Called when the device has been connected. This does not mean that the application may start + * communication. + * A service discovery will be handled automatically after this call. Service discovery + * may ends up with calling {@link #onServicesDiscovered(BluetoothDevice, boolean)} or + * {@link #onDeviceNotSupported(BluetoothDevice)} if required services have not been found. + * + * @param device the device that got connected. + */ + void onDeviceConnected(@NonNull final BluetoothDevice device); + + /** + * Called when user initialized disconnection. + * + * @param device the device that gets disconnecting. + */ + void onDeviceDisconnecting(@NonNull final BluetoothDevice device); + + /** + * Called when the device has disconnected (when the callback returned + * {@link BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int)} with state + * DISCONNECTED), but ONLY if the {@link BleManager#shouldAutoConnect()} method returned false + * for this device when it was connecting. + * Otherwise the {@link #onLinkLossOccurred(BluetoothDevice)} method will be called instead. + * + * @param device the device that got disconnected. + */ + void onDeviceDisconnected(@NonNull final BluetoothDevice device); + + /** + * This callback is invoked when the Ble Manager lost connection to a device that has been + * connected with autoConnect option (see {@link BleManager#shouldAutoConnect()}. + * Otherwise a {@link #onDeviceDisconnected(BluetoothDevice)} method will be called on such + * event. + * + * @param device the device that got disconnected due to a link loss. + */ + void onLinkLossOccurred(@NonNull final BluetoothDevice device); + + /** + * Called when service discovery has finished and primary services has been found. + * This method is not called if the primary, mandatory services were not found during service + * discovery. For example in the Blood Pressure Monitor, a Blood Pressure service is a + * primary service and Intermediate Cuff Pressure service is a optional secondary service. + * Existence of battery service is not notified by this call. + *

+ * After successful service discovery the service will initialize all services. + * The {@link #onDeviceReady(BluetoothDevice)} method will be called when the initialization + * is complete. + * + * @param device the device which services got disconnected. + * @param optionalServicesFound if true the secondary services were also found + * on the device. + */ + void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound); + + /** + * Method called when all initialization requests has been completed. + * + * @param device the device that get ready. + */ + void onDeviceReady(@NonNull final BluetoothDevice device); + + /** + * This method should return true if Battery Level notifications should be enabled on the + * target device. If there is no Battery Service, or the Battery Level characteristic does + * not have NOTIFY property, this method will not be called for this device. + *

+ * This method may return true only if an activity is bound to the service (to display the + * information to the user), always (e.g. if critical battery level is reported using + * notifications) or never, if such information is not important or the manager wants to + * control Battery Level notifications on its own. + * + * @param device the target device. + * @return True to enabled battery level notifications after connecting to the device, + * false otherwise. + * @deprecated Use + *

{@code
+	 * setNotificationCallback(batteryLevelCharacteristic)
+	 *       .with(new BatteryLevelDataCallback() {
+	 *           onBatteryLevelChanged(int batteryLevel) {
+	 *                ...
+	 *           }
+	 *       });
+	 * }
+ * in the {@link BleManager.BleManagerGattCallback#initialize() initialize(BluetoothDevice)} + * instead. + */ + @Deprecated + default boolean shouldEnableBatteryLevelNotifications(@NonNull final BluetoothDevice device) { + return false; + } + + /** + * Called when battery value has been received from the device. + * + * @param device the device from which the battery value has changed. + * @param value the battery value in percent. + * @deprecated Use + *
{@code
+	 * setNotificationCallback(batteryLevelCharacteristic)
+	 *       .with(new BatteryLevelDataCallback() {
+	 *           onBatteryLevelChanged(int batteryLevel) {
+	 *                ...
+	 *           }
+	 *       });
+	 * }
+ * in the {@link BleManager.BleManagerGattCallback#initialize() initialize(BluetoothDevice)} + * instead. + */ + @Deprecated + default void onBatteryValueReceived(@NonNull final BluetoothDevice device, + @IntRange(from = 0, to = 100) final int value) { + // do nothing + } + + /** + * Called when an {@link BluetoothGatt#GATT_INSUFFICIENT_AUTHENTICATION} error occurred and the + * device bond state is {@link BluetoothDevice#BOND_NONE}. + * + * @param device the device that requires bonding. + */ + void onBondingRequired(@NonNull final BluetoothDevice device); + + /** + * Called when the device has been successfully bonded. + * + * @param device the device that got bonded. + */ + void onBonded(@NonNull final BluetoothDevice device); + + /** + * Called when the bond state has changed from {@link BluetoothDevice#BOND_BONDING} to + * {@link BluetoothDevice#BOND_NONE}. + * + * @param device the device that failed to bond. + */ + void onBondingFailed(@NonNull final BluetoothDevice device); + + /** + * Called when a BLE error has occurred + * + * @param message the error message. + * @param errorCode the error code. + * @param device the device that caused an error. + */ + void onError(@NonNull final BluetoothDevice device, + @NonNull final String message, final int errorCode); + + /** + * Called when service discovery has finished but the main services were not found on the device. + * + * @param device the device that failed to connect due to lack of required services. + */ + void onDeviceNotSupported(@NonNull final BluetoothDevice device); +} diff --git a/_android/src/no/nordicsemi/android/ble/ConnectRequest.java b/_android/src/no/nordicsemi/android/ble/ConnectRequest.java new file mode 100644 index 0000000..567acae --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/ConnectRequest.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.content.Context; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.annotation.PhyMask; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; + +/** + * The connect request is used to connect to a Bluetooth LE device. The request will end when + * the device gets connected, the connection timeouts, or an error occurs. + *

+ * The {@link #done(SuccessCallback)} callback will be called after the device is ready, that is + * when it is connected, the services were discovered, the required services were found and the + * initialization queue set in {@link BleManager.BleManagerGattCallback#initialize()} is complete + * (without or with errors). + */ +@SuppressWarnings({"WeakerAccess", "unused", "deprecation"}) +public class ConnectRequest extends TimeoutableRequest { + @NonNull + private BluetoothDevice device; + @PhyMask + private int preferredPhy; + @IntRange(from = 0) + private int attempt = 0, retries = 0; + @IntRange(from = 0) + private int delay = 0; + private boolean autoConnect = false; + + ConnectRequest(@NonNull final Type type, @NonNull final BluetoothDevice device) { + super(type); + this.device = device; + this.preferredPhy = PhyRequest.PHY_LE_1M_MASK; + } + + @NonNull + @Override + ConnectRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @NonNull + @Override + public ConnectRequest timeout(@IntRange(from = 0) final long timeout) { + super.timeout(timeout); + return this; + } + + /** + * Use to set a completion callback. The callback will be invoked when the operation has + * finished successfully unless {@link #await()} or its variant was used, in which case this + * callback will be ignored. + *

+ * The done callback will also be called when one or more of initialization requests has + * failed due to a reason other than disconnect event. This is because + * {@link BleManagerCallbacks#onDeviceReady(BluetoothDevice)} is called no matter + * if the requests succeeded, or not. Set failure callbacks to initialization requests + * to get information about failures. + * + * @param callback the callback. + * @return The request. + */ + @NonNull + @Override + public ConnectRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @NonNull + @Override + public ConnectRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public ConnectRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public ConnectRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + /** + * Sets an optional retry count. The BleManager will do that many attempts to connect to the + * device in case of an error. The library will NOT retry if the device is not reachable, + * that is when the 30 sec. timeout occurs. In that case the app should scan before + * connecting, to make sure the target is in range. + * + * @param count how many times should the BleManager retry to connect. + * @return The request. + * @see #retry(int, int) + */ + public ConnectRequest retry(@IntRange(from = 0) final int count) { + this.retries = count; + this.delay = 0; + return this; + } + + /** + * Sets an optional retry count and a delay that the process will wait before each connection + * attempt. The library will NOT retry if the device is not reachable, that is when the 30 sec. + * timeout occurs. In that case the app should scan before connecting, to make sure the + * target is in range. + * + * @param count how many times should the BleManager retry to connect. + * @param delay the delay between each connection attempt, in milliseconds. + * The real delay will be 200 ms longer than specified, as + * {@link BluetoothGatt#clone()} is estimated to last + * {@link BleManager#internalConnect(BluetoothDevice, ConnectRequest) 200 ms}. + * @return The request. + * @see #retry(int) + */ + public ConnectRequest retry(@IntRange(from = 0) final int count, + @IntRange(from = 0) final int delay) { + this.retries = count; + this.delay = delay; + return this; + } + + /** + * This method replaces the {@link BleManager#shouldAutoConnect()} method. + *

+ * Sets whether to connect to the remote device just once (false) or to add the address to + * white list of devices that will be automatically connect as soon as they become available + * (true). In the latter case, if Bluetooth adapter is enabled, Android scans periodically + * for devices from the white list and, if an advertising packet is received from such, it tries + * to connect to it. When the connection is lost, the system will keep trying to reconnect to + * it. If method is called with parameter set to true, and the connection to the device is + * lost, the {@link BleManagerCallbacks#onLinkLossOccurred(BluetoothDevice)} callback is + * called instead of {@link BleManagerCallbacks#onDeviceDisconnected(BluetoothDevice)}. + *

+ * This feature works much better on newer Android phone models and may have issues on older + * phones. + *

+ * This method should only be used with bonded devices, as otherwise the device may change + * it's address. It will however work also with non-bonded devices with private static address. + * A connection attempt to a non-bonded device with private resolvable address will fail. + *

+ * The first connection to a device will always be created with autoConnect flag to false + * (see {@link BluetoothDevice#connectGatt(Context, boolean, BluetoothGattCallback)}). This is + * to make it quick as the user most probably waits for a quick response. If autoConnect is + * used (true), the following connections will be done using {@link BluetoothGatt#connect()}, + * which forces the autoConnect parameter to true. + * + * @param autoConnect true to use autoConnect feature on the second and following connections. + * The first connection is always done with autoConnect parameter equal to + * false, to make it faster and allow to timeout it the device is unreachable. + * Default value is false. + * @return The request. + */ + public ConnectRequest useAutoConnect(final boolean autoConnect) { + this.autoConnect = autoConnect; + return this; + } + + /** + * Sets the preferred PHY used for connection. Th value should be a bitmask composed of + * {@link PhyRequest#PHY_LE_1M_MASK}, {@link PhyRequest#PHY_LE_2M_MASK} or + * {@link PhyRequest#PHY_LE_CODED_MASK}. + *

+ * Different PHYs are available only on more recent devices with Android 8+. + * Check {@link BluetoothAdapter#isLe2MPhySupported()} and + * {@link BluetoothAdapter#isLeCodedPhySupported()} if required PHYs are supported by this + * Android device. The default PHY is {@link PhyRequest#PHY_LE_1M_MASK}. + * + * @param phy preferred PHY for connections to remote LE device. Bitwise OR of any of + * {@link PhyRequest#PHY_LE_1M_MASK}, {@link PhyRequest#PHY_LE_2M_MASK}, + * and {@link PhyRequest#PHY_LE_CODED_MASK}. This option does not take effect + * if {@code autoConnect} is set to true. + * @return The request. + */ + public ConnectRequest usePreferredPhy(@PhyMask final int phy) { + this.preferredPhy = phy; + return this; + } + + @NonNull + public BluetoothDevice getDevice() { + return device; + } + + @PhyMask + int getPreferredPhy() { + return preferredPhy; + } + + boolean canRetry() { + if (retries > 0) { + retries -= 1; + return true; + } + return false; + } + + boolean isFirstAttempt() { + return attempt++ == 0; + } + + @IntRange(from = 0) + int getRetryDelay() { + return delay; + } + + boolean shouldAutoConnect() { + return autoConnect; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/ConnectionPriorityRequest.java b/_android/src/no/nordicsemi/android/ble/ConnectionPriorityRequest.java new file mode 100644 index 0000000..9dabbd0 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/ConnectionPriorityRequest.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; +import android.os.Build; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import no.nordicsemi.android.ble.annotation.ConnectionPriority; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.ConnectionPriorityCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; +import no.nordicsemi.android.ble.exception.BluetoothDisabledException; +import no.nordicsemi.android.ble.exception.DeviceDisconnectedException; +import no.nordicsemi.android.ble.exception.InvalidRequestException; +import no.nordicsemi.android.ble.exception.RequestFailedException; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class ConnectionPriorityRequest extends SimpleValueRequest + implements Operation { + + /** + * Connection parameter update - Use the connection parameters recommended by the + * Bluetooth SIG. This is the default value if no connection parameter update + * is requested. + *

+ * Interval: 30 - 50 ms, latency: 0, supervision timeout: 20 sec. + */ + public static final int CONNECTION_PRIORITY_BALANCED = 0; + + /** + * Connection parameter update - Request a high priority, low latency connection. + * An application should only request high priority connection parameters to transfer + * large amounts of data over LE quickly. Once the transfer is complete, the application + * should request {@link #CONNECTION_PRIORITY_BALANCED} connection parameters + * to reduce energy use. + *

+ * Interval: 11.25 - 15 ms (Android 6+) or 7.5 - 10 ms (Android 4.3 - 5.1), + * latency: 0, supervision timeout: 20 sec. + */ + public static final int CONNECTION_PRIORITY_HIGH = 1; + + /** + * Connection parameter update - Request low power, reduced data rate connection parameters. + *

+ * Interval: 100 - 125 ms, latency: 2, supervision timeout: 20 sec. + */ + public static final int CONNECTION_PRIORITY_LOW_POWER = 2; + + private final int value; + + ConnectionPriorityRequest(@NonNull final Type type, @ConnectionPriority int priority) { + super(type); + if (priority < 0 || priority > 2) + priority = CONNECTION_PRIORITY_BALANCED; + this.value = priority; + } + + @NonNull + @Override + ConnectionPriorityRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @Override + @NonNull + public ConnectionPriorityRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @Override + @NonNull + public ConnectionPriorityRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public ConnectionPriorityRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public ConnectionPriorityRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + @RequiresApi(value = Build.VERSION_CODES.O) + @Override + @NonNull + public ConnectionPriorityRequest with(@NonNull final ConnectionPriorityCallback callback) { + // The BluetoothGattCallback#onConnectionUpdated callback was introduced in Android Oreo. + super.with(callback); + return this; + } + + @RequiresApi(value = Build.VERSION_CODES.O) + @NonNull + @Override + public E await(@NonNull final Class responseClass) + throws RequestFailedException, DeviceDisconnectedException, BluetoothDisabledException, + InvalidRequestException { + // The BluetoothGattCallback#onConnectionUpdated callback was introduced in Android Oreo. + return super.await(responseClass); + } + + @RequiresApi(value = Build.VERSION_CODES.O) + @NonNull + @Override + public E await(@NonNull final E response) + throws RequestFailedException, DeviceDisconnectedException, BluetoothDisabledException, + InvalidRequestException { + // The BluetoothGattCallback#onConnectionUpdated callback was introduced in Android Oreo. + return super.await(response); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + void notifyConnectionPriorityChanged(@NonNull final BluetoothDevice device, + @IntRange(from = 6, to = 3200) final int interval, + @IntRange(from = 0, to = 499) final int latency, + @IntRange(from = 10, to = 3200) final int timeout) { + if (valueCallback != null) + valueCallback.onConnectionUpdated(device, interval, latency, timeout); + } + + @ConnectionPriority + int getRequiredPriority() { + return value; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/DisconnectRequest.java b/_android/src/no/nordicsemi/android/ble/DisconnectRequest.java new file mode 100644 index 0000000..5e57914 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/DisconnectRequest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; + +@SuppressWarnings({"WeakerAccess", "unused", "deprecation"}) +public class DisconnectRequest extends TimeoutableRequest { + + DisconnectRequest(@NonNull final Type type) { + super(type); + } + + @NonNull + @Override + DisconnectRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @NonNull + @Override + public DisconnectRequest timeout(@IntRange(from = 0) final long timeout) { + super.timeout(timeout); + return this; + } + + @NonNull + @Override + public DisconnectRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @NonNull + @Override + public DisconnectRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public DisconnectRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public DisconnectRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/MainThreadBluetoothGattCallback.java b/_android/src/no/nordicsemi/android/ble/MainThreadBluetoothGattCallback.java new file mode 100644 index 0000000..be589e7 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/MainThreadBluetoothGattCallback.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.IntRange; +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import no.nordicsemi.android.ble.annotation.ConnectionState; +import no.nordicsemi.android.ble.annotation.PhyValue; + +/** + * This class ensures that the BLE callbacks will be called on the main (UI) thread. + * Handler parameter was added to {@link android.bluetooth.BluetoothDevice + * #connectGatt(Context, boolean, BluetoothGattCallback, int, int, Handler)} + * in Android Oreo, before that the behavior was undefined. + */ +abstract class MainThreadBluetoothGattCallback extends BluetoothGattCallback { + private Handler mHandler; + + void setHandler(@NonNull final Handler handler) { + mHandler = handler; + } + + private void runOnUiThread(@NonNull final Runnable runnable) { + if (Looper.myLooper() != Looper.getMainLooper()) { + mHandler.post(runnable); + } else { + runnable.run(); + } + } + + abstract void onConnectionStateChangeSafe(@NonNull final BluetoothGatt gatt, final int status, + final int newState); + abstract void onServicesDiscoveredSafe(@NonNull final BluetoothGatt gatt, final int status); + abstract void onCharacteristicReadSafe(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] data, + final int status); + abstract void onCharacteristicWriteSafe(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] data, + final int status); + abstract void onCharacteristicChangedSafe(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] data); + abstract void onDescriptorReadSafe(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattDescriptor descriptor, + @Nullable final byte[] data, + final int status); + abstract void onDescriptorWriteSafe(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattDescriptor descriptor, + @Nullable final byte[] data, + final int status); + abstract void onReadRemoteRssiSafe(@NonNull final BluetoothGatt gatt, + @IntRange(from = -128, to = 20) final int rssi, + final int status); + abstract void onReliableWriteCompletedSafe(@NonNull final BluetoothGatt gatt, final int status); + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + abstract void onMtuChangedSafe(@NonNull final BluetoothGatt gatt, + @IntRange(from = 23, to = 517) final int mtu, final int status); + @RequiresApi(api = Build.VERSION_CODES.O) + abstract void onPhyReadSafe(@NonNull final BluetoothGatt gatt, + @PhyValue final int txPhy, @PhyValue final int rxPhy, final int status); + @RequiresApi(api = Build.VERSION_CODES.O) + abstract void onPhyUpdateSafe(@NonNull final BluetoothGatt gatt, + @PhyValue final int txPhy, @PhyValue final int rxPhy, final int status); + @RequiresApi(api = Build.VERSION_CODES.O) + abstract void onConnectionUpdatedSafe(@NonNull final BluetoothGatt gatt, + @IntRange(from = 6, to = 3200) final int interval, + @IntRange(from = 0, to = 499) final int latency, + @IntRange(from = 10, to = 3200) final int timeout, + final int status); + + @Override + public final void onConnectionStateChange(@NonNull final BluetoothGatt gatt, final int status, + @ConnectionState final int newState) { + runOnUiThread(() -> onConnectionStateChangeSafe(gatt, status, newState)); + } + + @Override + public final void onServicesDiscovered(@NonNull final BluetoothGatt gatt, final int status) { + runOnUiThread(() -> onServicesDiscoveredSafe(gatt, status)); + } + + @Override + public final void onCharacteristicRead(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic, + final int status) { + final byte[] data = characteristic.getValue(); + runOnUiThread(() -> onCharacteristicReadSafe(gatt, characteristic, data, status)); + } + + @Override + public final void onCharacteristicWrite(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic, + final int status) { + final byte[] data = characteristic.getValue(); + runOnUiThread(() -> onCharacteristicWriteSafe(gatt, characteristic, data, status)); + } + + @Override + public final void onCharacteristicChanged(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic) { + final byte[] data = characteristic.getValue(); + runOnUiThread(() -> onCharacteristicChangedSafe(gatt, characteristic, data)); + } + + @Override + public final void onDescriptorRead(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattDescriptor descriptor, + final int status) { + final byte[] data = descriptor.getValue(); + runOnUiThread(() -> onDescriptorReadSafe(gatt, descriptor, data, status)); + } + + @Override + public final void onDescriptorWrite(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattDescriptor descriptor, + final int status) { + final byte[] data = descriptor.getValue(); + runOnUiThread(() -> onDescriptorWriteSafe(gatt, descriptor, data, status)); + } + + @Override + public final void onReadRemoteRssi(@NonNull final BluetoothGatt gatt, + @IntRange(from = -128, to = 20) final int rssi, + final int status) { + runOnUiThread(() -> onReadRemoteRssiSafe(gatt, rssi, status)); + } + + @Override + public final void onReliableWriteCompleted(@NonNull final BluetoothGatt gatt, final int status) { + runOnUiThread(() -> onReliableWriteCompletedSafe(gatt, status)); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public final void onMtuChanged(@NonNull final BluetoothGatt gatt, + @IntRange(from = 23, to = 517) final int mtu, final int status) { + runOnUiThread(() -> onMtuChangedSafe(gatt, mtu, status)); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public final void onPhyRead(@NonNull final BluetoothGatt gatt, + @PhyValue final int txPhy, @PhyValue final int rxPhy, + final int status) { + runOnUiThread(() -> onPhyReadSafe(gatt, txPhy, rxPhy, status)); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public final void onPhyUpdate(@NonNull final BluetoothGatt gatt, + @PhyValue final int txPhy, @PhyValue final int rxPhy, + final int status) { + runOnUiThread(() -> onPhyUpdateSafe(gatt, txPhy, rxPhy, status)); + } + + // This method is hidden in Android Oreo and Pie + // @Override + @SuppressWarnings("unused") + @RequiresApi(api = Build.VERSION_CODES.O) + @Keep + public final void onConnectionUpdated(@NonNull final BluetoothGatt gatt, + @IntRange(from = 6, to = 3200) final int interval, + @IntRange(from = 0, to = 499) final int latency, + @IntRange(from = 10, to = 3200) final int timeout, + final int status) { + runOnUiThread(() -> onConnectionUpdatedSafe(gatt, interval, latency, timeout, status)); + } +} diff --git a/_android/src/no/nordicsemi/android/ble/MtuRequest.java b/_android/src/no/nordicsemi/android/ble/MtuRequest.java new file mode 100644 index 0000000..f67ccdd --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/MtuRequest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.MtuCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; + +public final class MtuRequest extends SimpleValueRequestimplements Operation { + private final int value; + + MtuRequest(@NonNull final Type type, @IntRange(from = 23, to = 517) int mtu) { + super(type); + if (mtu < 23) + mtu = 23; + if (mtu > 517) + mtu = 517; + this.value = mtu; + } + + @NonNull + @Override + MtuRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @Override + @NonNull + public MtuRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @Override + @NonNull + public MtuRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public MtuRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public MtuRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + @Override + @NonNull + public MtuRequest with(@NonNull final MtuCallback callback) { + super.with(callback); + return this; + } + + void notifyMtuChanged(@NonNull final BluetoothDevice device, + @IntRange(from = 23, to = 517) final int mtu) { + if (valueCallback != null) + valueCallback.onMtuChanged(device, mtu); + } + + int getRequiredMtu() { + return value; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/Operation.java b/_android/src/no/nordicsemi/android/ble/Operation.java new file mode 100644 index 0000000..a701401 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/Operation.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +/** + * An operation is a request that can be executed during a pending connection without + * changing connection state. Such requests may be added to {@link RequestQueue}. + */ +@SuppressWarnings("WeakerAccess") +public interface Operation { + // empty +} diff --git a/_android/src/no/nordicsemi/android/ble/PhyRequest.java b/_android/src/no/nordicsemi/android/ble/PhyRequest.java new file mode 100644 index 0000000..0a9e8c1 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/PhyRequest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.annotation.PhyMask; +import no.nordicsemi.android.ble.annotation.PhyOption; +import no.nordicsemi.android.ble.annotation.PhyValue; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.PhyCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; + +@SuppressWarnings({"WeakerAccess", "unused"}) +public final class PhyRequest extends SimpleValueRequest implements Operation { + + /** + * Bluetooth LE 1M PHY mask. Used to specify LE 1M Physical Channel as one of many available + * options in a bitmask. + */ + public static final int PHY_LE_1M_MASK = 1; + + /** + * Bluetooth LE 2M PHY mask. Used to specify LE 2M Physical Channel as one of many available + * options in a bitmask. + */ + public static final int PHY_LE_2M_MASK = 2; + + /** + * Bluetooth LE Coded PHY mask. Used to specify LE Coded Physical Channel as one of many + * available options in a bitmask. + */ + public static final int PHY_LE_CODED_MASK = 4; + + /** + * No preferred coding when transmitting on the LE Coded PHY. + */ + public static final int PHY_OPTION_NO_PREFERRED = 0; + + /** + * Prefer the S=2 coding to be used when transmitting on the LE Coded PHY. + */ + public static final int PHY_OPTION_S2 = 1; + + /** + * Prefer the S=8 coding to be used when transmitting on the LE Coded PHY. + */ + public static final int PHY_OPTION_S8 = 2; + + private final int txPhy; + private final int rxPhy; + private final int phyOptions; + + PhyRequest(@NonNull final Type type) { + super(type); + this.txPhy = 0; + this.rxPhy = 0; + this.phyOptions = 0; + } + + PhyRequest(@NonNull final Type type, + @PhyMask int txPhy, @PhyMask int rxPhy, @PhyOption int phyOptions) { + super(type); + if ((txPhy & ~(PHY_LE_1M_MASK | PHY_LE_2M_MASK | PHY_LE_CODED_MASK)) > 0) + txPhy = PHY_LE_1M_MASK; + if ((rxPhy & ~(PHY_LE_1M_MASK | PHY_LE_2M_MASK | PHY_LE_CODED_MASK)) > 0) + rxPhy = PHY_LE_1M_MASK; + if (phyOptions < PHY_OPTION_NO_PREFERRED || phyOptions > PHY_OPTION_S8) + phyOptions = PHY_OPTION_NO_PREFERRED; + this.txPhy = txPhy; + this.rxPhy = rxPhy; + this.phyOptions = phyOptions; + } + + @NonNull + @Override + PhyRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @Override + @NonNull + public PhyRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @Override + @NonNull + public PhyRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public PhyRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public PhyRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + @Override + @NonNull + public PhyRequest with(@NonNull final PhyCallback callback) { + super.with(callback); + return this; + } + + void notifyPhyChanged(@NonNull final BluetoothDevice device, + @PhyValue final int txPhy, @PhyValue final int rxPhy) { + if (valueCallback != null) + valueCallback.onPhyChanged(device, txPhy, rxPhy); + } + + void notifyLegacyPhy(@NonNull final BluetoothDevice device) { + if (valueCallback != null) + valueCallback.onPhyChanged(device, PhyCallback.PHY_LE_1M, PhyCallback.PHY_LE_1M); + } + + @PhyMask + int getPreferredTxPhy() { + return txPhy; + } + + @PhyMask + int getPreferredRxPhy() { + return rxPhy; + } + + @PhyOption + int getPreferredPhyOptions() { + return phyOptions; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/ReadRequest.java b/_android/src/no/nordicsemi/android/ble/ReadRequest.java new file mode 100644 index 0000000..476ab12 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/ReadRequest.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.DataReceivedCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.ReadProgressCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; +import no.nordicsemi.android.ble.callback.profile.ProfileReadResponse; +import no.nordicsemi.android.ble.data.Data; +import no.nordicsemi.android.ble.data.DataFilter; +import no.nordicsemi.android.ble.data.DataMerger; +import no.nordicsemi.android.ble.data.DataStream; +import no.nordicsemi.android.ble.exception.BluetoothDisabledException; +import no.nordicsemi.android.ble.exception.DeviceDisconnectedException; +import no.nordicsemi.android.ble.exception.InvalidDataException; +import no.nordicsemi.android.ble.exception.InvalidRequestException; +import no.nordicsemi.android.ble.exception.RequestFailedException; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class ReadRequest extends SimpleValueRequest implements Operation { + private ReadProgressCallback progressCallback; + private DataMerger dataMerger; + private DataStream buffer; + private DataFilter filter; + private int count = 0; + + ReadRequest(@NonNull final Type type) { + super(type); + } + + ReadRequest(@NonNull final Type type, @Nullable final BluetoothGattCharacteristic characteristic) { + super(type, characteristic); + } + + ReadRequest(@NonNull final Type type, @Nullable final BluetoothGattDescriptor descriptor) { + super(type, descriptor); + } + + @NonNull + @Override + ReadRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @Override + @NonNull + public ReadRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @Override + @NonNull + public ReadRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public ReadRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public ReadRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + @Override + @NonNull + public ReadRequest with(@NonNull final DataReceivedCallback callback) { + super.with(callback); + return this; + } + + /** + * Sets a filter which allows to skip some incoming data. + * + * @param filter the data filter. + * @return The request. + */ + @NonNull + public ReadRequest filter(@NonNull final DataFilter filter) { + this.filter = filter; + return this; + } + + /** + * Adds a merger that will be used to merge multiple packets into a single Data. + * The merger may modify each packet if necessary. + * + * @return The request. + */ + @NonNull + public ReadRequest merge(@NonNull final DataMerger merger) { + this.dataMerger = merger; + this.progressCallback = null; + return this; + } + + /** + * Adds a merger that will be used to merge multiple packets into a single Data. + * The merger may modify each packet if necessary. + * + * @return The request. + */ + @NonNull + public ReadRequest merge(@NonNull final DataMerger merger, + @NonNull final ReadProgressCallback callback) { + this.dataMerger = merger; + this.progressCallback = callback; + return this; + } + + /** + * Same as {@link #await(Class)}, but if the response class extends + * {@link ProfileReadResponse} and the received response is not valid + * ({@link ProfileReadResponse#isValid()} returns false), this method will + * throw an exception. + * + * @param responseClass the response class. This class will be instantiate, therefore it + * has to have a default constructor. + * @return The object with the response. + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link BluetoothGatt#GATT_SUCCESS}. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread. + * @throws IllegalArgumentException thrown when the response class could not be instantiated. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter has been disabled. + * @throws InvalidDataException thrown when the received data were not valid (that is when + * {@link ProfileReadResponse#onDataReceived(BluetoothDevice, Data)} + * failed to parse the data correctly and called + * {@link ProfileReadResponse#onInvalidDataReceived(BluetoothDevice, Data)}). + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + */ + @NonNull + public E awaitValid(@NonNull final Class responseClass) + throws RequestFailedException, InvalidDataException, DeviceDisconnectedException, + BluetoothDisabledException, InvalidRequestException { + final E response = await(responseClass); + if (!response.isValid()) { + throw new InvalidDataException(response); + } + return response; + } + + /** + * Same as {@link #await(Object)}, but if the response class extends + * {@link ProfileReadResponse} and the received response is not valid + * ({@link ProfileReadResponse#isValid()} returns false), this method will + * throw an exception. + * + * @param response the response object. + * @return The object with the response. + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link BluetoothGatt#GATT_SUCCESS}. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter has been disabled. + * @throws InvalidDataException thrown when the received data were not valid (that is when + * {@link ProfileReadResponse#onDataReceived(BluetoothDevice, Data)} + * failed to parse the data correctly and called + * {@link ProfileReadResponse#onInvalidDataReceived(BluetoothDevice, Data)}). + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + */ + @NonNull + public E awaitValid(@NonNull final E response) + throws RequestFailedException, InvalidDataException, DeviceDisconnectedException, + BluetoothDisabledException, InvalidRequestException { + await(response); + if (!response.isValid()) { + throw new InvalidDataException(response); + } + return response; + } + + boolean matches(final byte[] packet) { + return filter == null || filter.filter(packet); + } + + void notifyValueChanged(@NonNull final BluetoothDevice device, @Nullable final byte[] value) { + // Keep a reference to the value callback, as it may change during execution + final DataReceivedCallback valueCallback = this.valueCallback; + + // With no value callback there is no need for any merging + if (valueCallback == null) + return; + + if (dataMerger == null) { + valueCallback.onDataReceived(device, new Data(value)); + } else { + if (progressCallback != null) + progressCallback.onPacketReceived(device, value, count); + if (buffer == null) + buffer = new DataStream(); + if (dataMerger.merge(buffer, value, count++)) { + valueCallback.onDataReceived(device, buffer.toData()); + buffer = null; + count = 0; + } // else + // wait for more packets to be merged + } + } + + boolean hasMore() { + return count > 0; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/ReadRssiRequest.java b/_android/src/no/nordicsemi/android/ble/ReadRssiRequest.java new file mode 100644 index 0000000..a189e93 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/ReadRssiRequest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.RssiCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; + +public final class ReadRssiRequest extends SimpleValueRequest implements Operation { + + ReadRssiRequest(@NonNull final Type type) { + super(type); + } + + @NonNull + @Override + ReadRssiRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @Override + @NonNull + public ReadRssiRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @Override + @NonNull + public ReadRssiRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public ReadRssiRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public ReadRssiRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + @Override + @NonNull + public ReadRssiRequest with(@NonNull final RssiCallback callback) { + super.with(callback); + return this; + } + + void notifyRssiRead(@NonNull final BluetoothDevice device, + @IntRange(from = -128, to = 20) final int rssi) { + if (valueCallback != null) + valueCallback.onRssiRead(device, rssi); + } +} diff --git a/_android/src/no/nordicsemi/android/ble/ReliableWriteRequest.java b/_android/src/no/nordicsemi/android/ble/ReliableWriteRequest.java new file mode 100644 index 0000000..e2bc384 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/ReliableWriteRequest.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; + +@SuppressWarnings("unused") +public final class ReliableWriteRequest extends RequestQueue { + private boolean initialized; + private boolean closed; + private boolean cancelled; + + @NonNull + @Override + ReliableWriteRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @Override + @NonNull + public ReliableWriteRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @Override + @NonNull + public ReliableWriteRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public ReliableWriteRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public ReliableWriteRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + @NonNull + @Override + public ReliableWriteRequest add(@NonNull final Operation operation) { + super.add(operation); + // Make sure the write request uses splitting, as Long Write is not supported + // in Reliable Write sub-procedure. + if (operation instanceof WriteRequest) { + ((WriteRequest) operation).forceSplit(); + } + return this; + } + + @Override + public void cancelQueue() { + cancelled = true; + super.cancelQueue(); + } + + /** + * Alias for {@link #cancelQueue()}. + */ + public void abort() { + cancelQueue(); + } + + @Override + public int size() { + int size = super.size(); + + // Add Begin Reliable Write + if (!initialized) + size += 1; + + // Add Execute or Abort Reliable Write + if (!closed) + size += 1; + return size; + } + + @Override + Request getNext() { + if (!initialized) { + initialized = true; + return newBeginReliableWriteRequest(); + } + if (super.isEmpty()) { + closed = true; + + if (cancelled) + return newAbortReliableWriteRequest(); + return newExecuteReliableWriteRequest(); + } + return super.getNext(); + } + + @Override + boolean hasMore() { + // If no operations were added, consider the RW request empty, no requests will be executed. + if (!initialized) + return super.hasMore(); + return !closed; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/Request.java b/_android/src/no/nordicsemi/android/ble/Request.java new file mode 100644 index 0000000..38a3118 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/Request.java @@ -0,0 +1,896 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.os.ConditionVariable; +import android.os.Looper; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.annotation.ConnectionPriority; +import no.nordicsemi.android.ble.annotation.PhyMask; +import no.nordicsemi.android.ble.annotation.PhyOption; +import no.nordicsemi.android.ble.annotation.WriteType; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; +import no.nordicsemi.android.ble.data.Data; + +/** + * On Android, when multiple BLE operations needs to be done, it is required to wait for a proper + * {@link BluetoothGattCallback} callback before calling another operation. + * In order to make BLE operations easier the BleManager allows to enqueue a request containing all + * data necessary for a given operation. Requests are performed one after another until the queue + * is empty. + */ +@SuppressWarnings({"unused", "WeakerAccess", "deprecation", "DeprecatedIsStillUsed"}) +public abstract class Request { + + enum Type { + SET, + CONNECT, + DISCONNECT, + CREATE_BOND, + REMOVE_BOND, + WRITE, + READ, + WRITE_DESCRIPTOR, + READ_DESCRIPTOR, + BEGIN_RELIABLE_WRITE, + EXECUTE_RELIABLE_WRITE, + ABORT_RELIABLE_WRITE, + ENABLE_NOTIFICATIONS, + ENABLE_INDICATIONS, + DISABLE_NOTIFICATIONS, + DISABLE_INDICATIONS, + WAIT_FOR_NOTIFICATION, + WAIT_FOR_INDICATION, + @Deprecated + READ_BATTERY_LEVEL, + @Deprecated + ENABLE_BATTERY_LEVEL_NOTIFICATIONS, + @Deprecated + DISABLE_BATTERY_LEVEL_NOTIFICATIONS, + ENABLE_SERVICE_CHANGED_INDICATIONS, + REQUEST_MTU, + REQUEST_CONNECTION_PRIORITY, + SET_PREFERRED_PHY, + READ_PHY, + READ_RSSI, + REFRESH_CACHE, + SLEEP, + } + + private BleManager manager; + + final ConditionVariable syncLock; + final Type type; + final BluetoothGattCharacteristic characteristic; + final BluetoothGattDescriptor descriptor; + BeforeCallback beforeCallback; + SuccessCallback successCallback; + FailCallback failCallback; + InvalidRequestCallback invalidRequestCallback; + BeforeCallback internalBeforeCallback; + SuccessCallback internalSuccessCallback; + FailCallback internalFailCallback; + boolean enqueued; + boolean finished; + + Request(@NonNull final Type type) { + this.type = type; + this.characteristic = null; + this.descriptor = null; + this.syncLock = new ConditionVariable(true); + } + + Request(@NonNull final Type type, @Nullable final BluetoothGattCharacteristic characteristic) { + this.type = type; + this.characteristic = characteristic; + this.descriptor = null; + this.syncLock = new ConditionVariable(true); + } + + Request(@NonNull final Type type, @Nullable final BluetoothGattDescriptor descriptor) { + this.type = type; + this.characteristic = null; + this.descriptor = descriptor; + this.syncLock = new ConditionVariable(true); + } + + /** + * Sets the {@link BleManager} instance. + * + * @param manager the manager in which the request will be executed. + */ + @NonNull + Request setManager(@NonNull final BleManager manager) { + this.manager = manager; + return this; + } + + /** + * Creates a new connect request. This allows to set a callback to the connect event, + * just like any other request. + * + * @param device the device to connect to. + * @return The new connect request. + */ + @NonNull + static ConnectRequest connect(@NonNull final BluetoothDevice device) { + return new ConnectRequest(Type.CONNECT, device); + } + + /** + * Creates a new disconnect request. This allows to set a callback to a disconnect event, + * just like any other request. + * + * @return The new disconnect request. + */ + @NonNull + static DisconnectRequest disconnect() { + return new DisconnectRequest(Type.DISCONNECT); + } + + /** + * Creates a new request that will start pairing with the device. + * + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#createBond()} instead. + */ + @Deprecated + @NonNull + public static SimpleRequest createBond() { + return new SimpleRequest(Type.CREATE_BOND); + } + + /** + * Creates a new request that will remove the bonding information from the Android device. + * This is done using reflections and may not work on all devices. + *

+ * The device will disconnect after calling this method. The success callback will be called + * after the device got disconnected, when the {@link BluetoothDevice#getBondState()} changes + * to {@link BluetoothDevice#BOND_NONE}. + * + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#removeBond()} instead. + */ + @Deprecated + @NonNull + public static SimpleRequest removeBond() { + return new SimpleRequest(Type.REMOVE_BOND); + } + + /** + * Creates new Read Characteristic request. The request will not be executed if given + * characteristic is null or does not have READ property. + * After the operation is complete a proper callback will be invoked. + * + * @param characteristic characteristic to be read. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#readCharacteristic(BluetoothGattCharacteristic)} instead. + */ + @Deprecated + @NonNull + public static ReadRequest newReadRequest( + @Nullable final BluetoothGattCharacteristic characteristic) { + return new ReadRequest(Type.READ, characteristic); + } + + /** + * Creates new Write Characteristic request. The request will not be executed if given + * characteristic is null or does not have WRITE property. + * After the operation is complete a proper callback will be invoked. + * + * @param characteristic characteristic to be written. + * @param value value to be written. The array is copied into another buffer so it's + * safe to reuse the array again. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#writeCharacteristic(BluetoothGattCharacteristic, byte[])} instead. + */ + @Deprecated + @NonNull + public static WriteRequest newWriteRequest( + @Nullable final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] value) { + return new WriteRequest(Type.WRITE, characteristic, value, 0, + value != null ? value.length : 0, + characteristic != null ? + characteristic.getWriteType() : + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); + } + + /** + * Creates new Write Characteristic request. The request will not be executed if given + * characteristic is null or does not have WRITE property. + * After the operation is complete a proper callback will be invoked. + * + * @param characteristic characteristic to be written. + * @param value value to be written. The array is copied into another buffer so it's + * safe to reuse the array again. + * @param writeType write type to be used, one of + * {@link BluetoothGattCharacteristic#WRITE_TYPE_DEFAULT}, + * {@link BluetoothGattCharacteristic#WRITE_TYPE_NO_RESPONSE}. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#writeCharacteristic(BluetoothGattCharacteristic, Data)} instead. + */ + @Deprecated + @NonNull + public static WriteRequest newWriteRequest( + @Nullable final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] value, @WriteType final int writeType) { + return new WriteRequest(Type.WRITE, characteristic, value, 0, + value != null ? value.length : 0, writeType); + } + + /** + * Creates new Write Characteristic request. The request will not be executed if given + * characteristic is null or does not have WRITE property. + * After the operation is complete a proper callback will be invoked. + * + * @param characteristic characteristic to be written. + * @param value value to be written. The array is copied into another buffer so it's + * safe to reuse the array again. + * @param offset the offset from which value has to be copied. + * @param length number of bytes to be copied from the value buffer. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#writeCharacteristic(BluetoothGattCharacteristic, byte[], int, int)} + * instead. + */ + @Deprecated + @NonNull + public static WriteRequest newWriteRequest( + @Nullable final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] value, + @IntRange(from = 0) final int offset, @IntRange(from = 0) final int length) { + return new WriteRequest(Type.WRITE, characteristic, value, offset, length, + characteristic != null ? + characteristic.getWriteType() : + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); + } + + /** + * Creates new Write Characteristic request. The request will not be executed if given + * characteristic is null or does not have WRITE property. + * After the operation is complete a proper callback will be invoked. + * + * @param characteristic characteristic to be written. + * @param value value to be written. The array is copied into another buffer so it's + * safe to reuse the array again. + * @param offset the offset from which value has to be copied. + * @param length number of bytes to be copied from the value buffer. + * @param writeType write type to be used, one of + * {@link BluetoothGattCharacteristic#WRITE_TYPE_DEFAULT}, + * {@link BluetoothGattCharacteristic#WRITE_TYPE_NO_RESPONSE} or + * {@link BluetoothGattCharacteristic#WRITE_TYPE_SIGNED}. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#writeCharacteristic(BluetoothGattCharacteristic, byte[], int, int)} + * instead. + */ + @Deprecated + @NonNull + public static WriteRequest newWriteRequest( + @Nullable final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] value, + @IntRange(from = 0) final int offset, @IntRange(from = 0) final int length, + @WriteType final int writeType) { + return new WriteRequest(Type.WRITE, characteristic, value, offset, length, writeType); + } + + /** + * Creates new Read Descriptor request. The request will not be executed if given descriptor + * is null. After the operation is complete a proper callback will be invoked. + * + * @param descriptor descriptor to be read. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#readDescriptor(BluetoothGattDescriptor)} instead. + */ + @Deprecated + @NonNull + public static ReadRequest newReadRequest(@Nullable final BluetoothGattDescriptor descriptor) { + return new ReadRequest(Type.READ_DESCRIPTOR, descriptor); + } + + /** + * Creates new Write Descriptor request. The request will not be executed if given descriptor + * is null. After the operation is complete a proper callback will be invoked. + * + * @param descriptor descriptor to be written. + * @param value value to be written. The array is copied into another buffer so it's safe + * to reuse the array again. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#writeDescriptor(BluetoothGattDescriptor, byte[])} instead. + */ + @Deprecated + @NonNull + public static WriteRequest newWriteRequest(@Nullable final BluetoothGattDescriptor descriptor, + @Nullable final byte[] value) { + return new WriteRequest(Type.WRITE_DESCRIPTOR, descriptor, value, 0, + value != null ? value.length : 0); + } + + /** + * Creates new Write Descriptor request. The request will not be executed if given descriptor + * is null. After the operation is complete a proper callback will be invoked. + * + * @param descriptor descriptor to be written. + * @param value value to be written. The array is copied into another buffer so it's safe + * to reuse the array again. + * @param offset the offset from which value has to be copied. + * @param length number of bytes to be copied from the value buffer. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#writeDescriptor(BluetoothGattDescriptor, byte[], int, int)} instead. + */ + @Deprecated + @NonNull + public static WriteRequest newWriteRequest( + @Nullable final BluetoothGattDescriptor descriptor, + @Nullable final byte[] value, + @IntRange(from = 0) final int offset, @IntRange(from = 0) final int length) { + return new WriteRequest(Type.WRITE_DESCRIPTOR, descriptor, value, offset, length); + } + + /** + * Creates new Reliable Write request. All operations that need to be executed + * reliably should be enqueued inside the returned request before enqueuing it in the + * BleManager. The library will automatically verify the data sent + * + * @return The new request. + */ + @NonNull + static ReliableWriteRequest newReliableWriteRequest() { + return new ReliableWriteRequest(); + } + + /** + * Creates new Begin Reliable Write request. + * + * @return The new request. + */ + @NonNull + static SimpleRequest newBeginReliableWriteRequest() { + return new SimpleRequest(Type.BEGIN_RELIABLE_WRITE); + } + + /** + * Executes Reliable Write sub-procedure. At lease one Write Request must be performed + * before the Reliable Write is to be executed, otherwise error + * {@link no.nordicsemi.android.ble.error.GattError#GATT_INVALID_OFFSET} will be returned. + * + * @return The new request. + */ + @NonNull + static SimpleRequest newExecuteReliableWriteRequest() { + return new SimpleRequest(Type.EXECUTE_RELIABLE_WRITE); + } + + /** + * Aborts Reliable Write sub-procedure. All write requests performed during Reliable Write will + * be cancelled. At lease one Write Request must be performed before the Reliable Write + * is to be executed, otherwise error + * {@link no.nordicsemi.android.ble.error.GattError#GATT_INVALID_OFFSET} will be returned. + * + * @return The new request. + */ + @NonNull + static SimpleRequest newAbortReliableWriteRequest() { + return new SimpleRequest(Type.ABORT_RELIABLE_WRITE); + } + + /** + * Creates new Enable Notification request. The request will not be executed if given + * characteristic is null, does not have NOTIFY property or the CCCD. + * After the operation is complete a proper callback will be invoked. + * + * @param characteristic characteristic to have notifications enabled. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#enableNotifications(BluetoothGattCharacteristic)} instead. + */ + @Deprecated + @NonNull + public static WriteRequest newEnableNotificationsRequest( + @Nullable final BluetoothGattCharacteristic characteristic) { + return new WriteRequest(Type.ENABLE_NOTIFICATIONS, characteristic); + } + + /** + * Creates new Disable Notification request. The request will not be executed if given + * characteristic is null, does not have NOTIFY property or the CCCD. + * After the operation is complete a proper callback will be invoked. + * + * @param characteristic characteristic to have notifications disabled. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#disableNotifications(BluetoothGattCharacteristic)} instead. + */ + @Deprecated + @NonNull + public static WriteRequest newDisableNotificationsRequest( + @Nullable final BluetoothGattCharacteristic characteristic) { + return new WriteRequest(Type.DISABLE_NOTIFICATIONS, characteristic); + } + + /** + * Creates new Enable Indications request. The request will not be executed if given + * characteristic is null, does not have INDICATE property or the CCCD. + * After the operation is complete a proper callback will be invoked. + * + * @param characteristic characteristic to have indications enabled. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#enableIndications(BluetoothGattCharacteristic)} instead. + */ + @Deprecated + @NonNull + public static WriteRequest newEnableIndicationsRequest( + @Nullable final BluetoothGattCharacteristic characteristic) { + return new WriteRequest(Type.ENABLE_INDICATIONS, characteristic); + } + + /** + * Creates new Disable Indications request. The request will not be executed if given + * characteristic is null, does not have INDICATE property or the CCCD. + * After the operation is complete a proper callback will be invoked. + * + * @param characteristic characteristic to have indications disabled. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#disableNotifications(BluetoothGattCharacteristic)} instead. + */ + @Deprecated + @NonNull + public static WriteRequest newDisableIndicationsRequest( + @Nullable final BluetoothGattCharacteristic characteristic) { + return new WriteRequest(Type.DISABLE_INDICATIONS, characteristic); + } + + /** + * Creates new Wait For Notification request. The request will not be executed if given + * characteristic is null, does not have NOTIFY property or the CCCD. + * After the operation is complete a proper callback will be invoked. + *

+ * If the notification should be triggered by another operation (for example writing an + * op code), set it with {@link WaitForValueChangedRequest#trigger(Operation)}. + * + * @param characteristic characteristic from which a notification should be received. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#waitForNotification(BluetoothGattCharacteristic)} instead. + */ + @Deprecated + @NonNull + public static WaitForValueChangedRequest newWaitForNotificationRequest( + @Nullable final BluetoothGattCharacteristic characteristic) { + return new WaitForValueChangedRequest(Type.WAIT_FOR_NOTIFICATION, characteristic); + } + + /** + * Creates new Wait For Indication request. The request will not be executed if given + * characteristic is null, does not have INDICATE property or the CCCD. + * After the operation is complete a proper callback will be invoked. + *

+ * If the indication should be triggered by another operation (for example writing an + * op code), set it with {@link WaitForValueChangedRequest#trigger(Operation)}. + * + * @param characteristic characteristic from which a notification should be received. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#waitForIndication(BluetoothGattCharacteristic)} instead. + */ + @Deprecated + @NonNull + public static WaitForValueChangedRequest newWaitForIndicationRequest( + @Nullable final BluetoothGattCharacteristic characteristic) { + return new WaitForValueChangedRequest(Type.WAIT_FOR_INDICATION, characteristic); + } + + /** + * Creates new Read Battery Level request. The first found Battery Level characteristic value + * from the first found Battery Service. If any of them is not found, or the characteristic + * does not have the READ property this operation will not execute. + * + * @return The new request. + * @deprecated Use {@link #newReadRequest(BluetoothGattCharacteristic)} with + * BatteryLevelDataCallback from Android BLE Common Library instead. + */ + @NonNull + @Deprecated + public static ReadRequest newReadBatteryLevelRequest() { + return new ReadRequest(Type.READ_BATTERY_LEVEL); + } + + /** + * Creates new Enable Notifications on the first found Battery Level characteristic from the + * first found Battery Service. If any of them is not found, or the characteristic does not + * have the NOTIFY property this operation will not execute. + * + * @return The new request. + * @deprecated Use {@link #newEnableNotificationsRequest(BluetoothGattCharacteristic)} with + * BatteryLevelDataCallback from Android BLE Common Library instead. + */ + @NonNull + @Deprecated + public static WriteRequest newEnableBatteryLevelNotificationsRequest() { + return new WriteRequest(Type.ENABLE_BATTERY_LEVEL_NOTIFICATIONS); + } + + /** + * Creates new Disable Notifications on the first found Battery Level characteristic from the + * first found Battery Service. If any of them is not found, or the characteristic does not + * have the NOTIFY property this operation will not execute. + * + * @return The new request. + * @deprecated Use {@link #newDisableNotificationsRequest(BluetoothGattCharacteristic)} instead. + */ + @NonNull + @Deprecated + public static WriteRequest newDisableBatteryLevelNotificationsRequest() { + return new WriteRequest(Type.DISABLE_BATTERY_LEVEL_NOTIFICATIONS); + } + + /** + * Creates new Enable Indications on Service Changed characteristic. It is a NOOP if such + * characteristic does not exist in the Generic Attribute service. + * It is required to enable those notifications on bonded devices on older Android versions to + * be informed about attributes changes. + * Android 7+ (or 6+) handles this automatically and no action is required. + * + * @return The new request. + */ + @NonNull + static WriteRequest newEnableServiceChangedIndicationsRequest() { + return new WriteRequest(Type.ENABLE_SERVICE_CHANGED_INDICATIONS); + } + + /** + * Requests new MTU (Maximum Transfer Unit). This is only supported on Android Lollipop or newer. + * On older platforms the request will enqueue, but will fail to execute and + * {@link #fail(FailCallback)} callback will be called. + * The target device may reject requested value and set a smaller MTU. + * + * @param mtu the new MTU. Acceptable values are <23, 517>. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#requestMtu(int)} instead. + */ + @Deprecated + @NonNull + public static MtuRequest newMtuRequest(@IntRange(from = 23, to = 517) final int mtu) { + return new MtuRequest(Type.REQUEST_MTU, mtu); + } + + /** + * Requests the new connection priority. Acceptable values are: + *

    + *
  1. {@link ConnectionPriorityRequest#CONNECTION_PRIORITY_HIGH} + * - Interval: 11.25 -15 ms (Android 6+) and 7.5 - 10 ms (older), latency: 0, + * supervision timeout: 20 sec,
  2. + *
  3. {@link ConnectionPriorityRequest#CONNECTION_PRIORITY_BALANCED} + * - Interval: 30 - 50 ms, latency: 0, supervision timeout: 20 sec,
  4. + *
  5. {@link ConnectionPriorityRequest#CONNECTION_PRIORITY_LOW_POWER} + * - Interval: 100 - 125 ms, latency: 2, supervision timeout: 20 sec.
  6. + *
+ * Requesting connection priority is available on Android Lollipop or newer. On older + * platforms the request will enqueue, but will fail to execute and {@link #fail(FailCallback)} + * callback will be called. + * + * @param priority one of: {@link ConnectionPriorityRequest#CONNECTION_PRIORITY_HIGH}, + * {@link ConnectionPriorityRequest#CONNECTION_PRIORITY_BALANCED}, + * {@link ConnectionPriorityRequest#CONNECTION_PRIORITY_LOW_POWER}. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#requestConnectionPriority(int)} instead. + */ + @Deprecated + @NonNull + public static ConnectionPriorityRequest newConnectionPriorityRequest( + @ConnectionPriority final int priority) { + return new ConnectionPriorityRequest(Type.REQUEST_CONNECTION_PRIORITY, priority); + } + + /** + * Requests the change of preferred PHY for this connections. + *

+ * PHY LE 2M and PHY LE Coded are supported only on Android Oreo or newer. + * You may safely request other PHYs on older platforms, but the request will not be executed + * and you will get PHY LE 1M as TX and RX PHY in the callback. + * + * @param txPhy preferred transmitter PHY. Bitwise OR of any of + * {@link PhyRequest#PHY_LE_1M_MASK}, {@link PhyRequest#PHY_LE_2M_MASK}, + * and {@link PhyRequest#PHY_LE_CODED_MASK}. + * @param rxPhy preferred receiver PHY. Bitwise OR of any of + * {@link PhyRequest#PHY_LE_1M_MASK}, {@link PhyRequest#PHY_LE_2M_MASK}, + * and {@link PhyRequest#PHY_LE_CODED_MASK}. + * @param phyOptions preferred coding to use when transmitting on the LE Coded PHY. Can be one + * of {@link PhyRequest#PHY_OPTION_NO_PREFERRED}, + * {@link PhyRequest#PHY_OPTION_S2} or {@link PhyRequest#PHY_OPTION_S8}. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#setPreferredPhy(int, int, int)} instead. + */ + @Deprecated + @NonNull + public static PhyRequest newSetPreferredPhyRequest(@PhyMask final int txPhy, + @PhyMask final int rxPhy, + @PhyOption final int phyOptions) { + return new PhyRequest(Type.SET_PREFERRED_PHY, txPhy, rxPhy, phyOptions); + } + + /** + * Reads the current PHY for this connections. + *

+ * PHY LE 2M and PHY LE Coded are supported only on Android Oreo or newer. + * You may safely read PHY on older platforms, but the request will not be executed + * and you will get PHY LE 1M as TX and RX PHY in the callback. + * + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#readPhy()} instead. + */ + @Deprecated + @NonNull + public static PhyRequest newReadPhyRequest() { + return new PhyRequest(Type.READ_PHY); + } + + /** + * Reads the current RSSI (Received Signal Strength Indication). + * + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#readRssi()} instead. + */ + @Deprecated + @NonNull + public static ReadRssiRequest newReadRssiRequest() { + return new ReadRssiRequest(Type.READ_RSSI); + } + + /** + * Refreshes the device cache. As the {@link BluetoothGatt#refresh()} method is not in the + * public API (it's hidden, and on Android P it is on a light gray list) it is called + * using reflections and may be removed in some future Android release or on some devices. + *

+ * There is no callback indicating when the cache has been cleared. This library assumes + * some time and waits. After the delay, it will start service discovery and clear the + * task queue. When the service discovery finishes, the + * {@link BleManager.BleManagerGattCallback#isRequiredServiceSupported(BluetoothGatt)} and + * {@link BleManager.BleManagerGattCallback#isOptionalServiceSupported(BluetoothGatt)} will + * be called and the initialization will be performed as if the device has just connected. + * + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#refreshDeviceCache()} instead. + */ + @Deprecated + @SuppressWarnings("JavadocReference") + @NonNull + public static SimpleRequest newRefreshCacheRequest() { + return new SimpleRequest(Type.REFRESH_CACHE); + } + + /** + * Creates new Sleep request that will postpone next request for given number of milliseconds. + * + * @param delay the delay in milliseconds. + * @return The new request. + * @deprecated Access to this method will change to package-only. + * Use {@link BleManager#sleep(long)} instead. + */ + @Deprecated + @NonNull + public static SleepRequest newSleepRequest(@IntRange(from = 0) final long delay) { + return new SleepRequest(Type.SLEEP, delay); + } + + /** + * Use to set a completion callback. The callback will be invoked when the operation has + * finished successfully unless the request was executed synchronously, in which case this + * callback will be ignored. + * + * @param callback the callback. + * @return The request. + */ + @NonNull + public Request done(@NonNull final SuccessCallback callback) { + this.successCallback = callback; + return this; + } + + /** + * Use to set a callback that will be called in case the request has failed. + * If the target device wasn't set before executing this request + * ({@link BleManager#connect(BluetoothDevice)} was never called), the + * {@link #invalid(InvalidRequestCallback)} will be used instead, as the + * {@link BluetoothDevice} is not known. + *

+ * This callback will be ignored if request was executed synchronously, in which case + * the error will be returned as an exception. + * + * @param callback the callback. + * @return The request. + */ + @NonNull + public Request fail(@NonNull final FailCallback callback) { + this.failCallback = callback; + return this; + } + + /** + * Used to set internal callback what will be executed before the request is executed. + * + * @param callback the callback. + */ + void internalBefore(@NonNull final BeforeCallback callback) { + this.internalBeforeCallback = callback; + } + + /** + * Used to set internal success callback. The callback will be notified in case the request + * has completed. + * + * @param callback the callback. + */ + void internalSuccess(@NonNull final SuccessCallback callback) { + this.internalSuccessCallback = callback; + } + + + /** + * Used to set internal fail callback. The callback will be notified in case the request + * has failed. + * + * @param callback the callback. + */ + void internalFail(@NonNull final FailCallback callback) { + this.internalFailCallback = callback; + } + + /** + * Use to set a callback that will be called in case the request was invalid, for example + * called before the device was connected. + * This callback will be ignored if request was executed synchronously, in which case + * the error will be returned as an exception. + * + * @param callback the callback. + * @return The request. + */ + @NonNull + public Request invalid(@NonNull final InvalidRequestCallback callback) { + this.invalidRequestCallback = callback; + return this; + } + + /** + * Sets a callback that will be executed before the execution of this operation starts. + * + * @param callback the callback. + * @return The request. + */ + @NonNull + public Request before(@NonNull final BeforeCallback callback) { + this.beforeCallback = callback; + return this; + } + + /** + * Enqueues the request for asynchronous execution. + */ + public void enqueue() { + manager.enqueue(this); + } + + void notifyStarted(@NonNull final BluetoothDevice device) { + if (beforeCallback != null) + beforeCallback.onRequestStarted(device); + if (internalBeforeCallback != null) + internalBeforeCallback.onRequestStarted(device); + } + + void notifySuccess(@NonNull final BluetoothDevice device) { + if (!finished) { + finished = true; + + if (successCallback != null) + successCallback.onRequestCompleted(device); + if (internalSuccessCallback != null) + internalSuccessCallback.onRequestCompleted(device); + } + } + + void notifyFail(@NonNull final BluetoothDevice device, final int status) { + if (!finished) { + finished = true; + + if (failCallback != null) + failCallback.onRequestFailed(device, status); + if (internalFailCallback != null) + internalFailCallback.onRequestFailed(device, status); + } + } + + void notifyInvalidRequest() { + if (!finished) { + finished = true; + + if (invalidRequestCallback != null) + invalidRequestCallback.onInvalidRequest(); + } + } + + /** + * Asserts that the synchronous method was not called from the UI thread. + * + * @throws IllegalStateException when called from a UI thread. + */ + static void assertNotMainThread() throws IllegalStateException { + if (Looper.myLooper() == Looper.getMainLooper()) { + throw new IllegalStateException("Cannot execute synchronous operation from the UI thread."); + } + } + + final class RequestCallback implements SuccessCallback, FailCallback, InvalidRequestCallback { + final static int REASON_REQUEST_INVALID = -1000000; + int status = BluetoothGatt.GATT_SUCCESS; + + @Override + public void onRequestCompleted(@NonNull final BluetoothDevice device) { + syncLock.open(); + } + + @Override + public void onRequestFailed(@NonNull final BluetoothDevice device, final int status) { + this.status = status; + syncLock.open(); + } + + @Override + public void onInvalidRequest() { + this.status = REASON_REQUEST_INVALID; + syncLock.open(); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + boolean isSuccess() { + return this.status == BluetoothGatt.GATT_SUCCESS; + } + } +} diff --git a/_android/src/no/nordicsemi/android/ble/RequestQueue.java b/_android/src/no/nordicsemi/android/ble/RequestQueue.java new file mode 100644 index 0000000..6d97ab2 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/RequestQueue.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import java.util.LinkedList; +import java.util.Queue; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; + +@SuppressWarnings("WeakerAccess") +public class RequestQueue extends SimpleRequest { + /** + * A list of operations that will be executed together. + */ + @NonNull + private final Queue requests; + + RequestQueue() { + super(Type.SET); + requests = new LinkedList<>(); + } + + @NonNull + @Override + RequestQueue setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @Override + @NonNull + public RequestQueue done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @Override + @NonNull + public RequestQueue fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public RequestQueue invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public RequestQueue before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + /** + * Enqueues a new operation. All operations will be executed sequentially in order they were + * added. + * + * @param operation the new operation to be enqueued. + * @throws IllegalStateException if the operation was enqueued before. + * @throws IllegalArgumentException if the operation is not a {@link Request}. + */ + @NonNull + public RequestQueue add(@NonNull final Operation operation) { + if (operation instanceof Request) { + final Request request = (Request) operation; + // Validate + if (request.enqueued) + throw new IllegalStateException("Request already enqueued"); + // Add + requests.add(request); + // Mark + request.enqueued = true; + return this; + } else { + throw new IllegalArgumentException("Operation does not extend Request"); + } + } + + /** + * Returns number of enqueued operations. + * + * @return the size of the internal operations list. + */ + @IntRange(from = 0) + public int size() { + return requests.size(); + } + + /** + * Returns whether the set is empty, or not. + * + * @return true if the set is empty. Set gets emptied while it all enqueued operations + * are being executed. + */ + public boolean isEmpty() { + return requests.isEmpty(); + } + + /** + * Cancels all the enqueued operations that were not executed yet. + * The one currently executed will be finished. + *

+ * It is safe to call this method in {@link Request#done(SuccessCallback)} or + * {@link Request#fail(FailCallback)} callback; + */ + public void cancelQueue() { + requests.clear(); + } + + /** + * Returns the next {@link Request} to be enqueued. + * + * @return the next request. + */ + @Nullable + Request getNext() { + try { + return requests.remove(); + // poll() may also throw an exception + // See: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/37 + } catch (final Exception e) { + return null; + } + } + + /** + * Returns whether there are more operations to be executed. + * + * @return true, if not all operations were completed. + */ + boolean hasMore() { + return !requests.isEmpty(); + } +} diff --git a/_android/src/no/nordicsemi/android/ble/SimpleRequest.java b/_android/src/no/nordicsemi/android/ble/SimpleRequest.java new file mode 100644 index 0000000..23359d7 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/SimpleRequest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; +import no.nordicsemi.android.ble.exception.BluetoothDisabledException; +import no.nordicsemi.android.ble.exception.DeviceDisconnectedException; +import no.nordicsemi.android.ble.exception.InvalidRequestException; +import no.nordicsemi.android.ble.exception.RequestFailedException; + +/** + * A request that requires a {@link android.bluetooth.BluetoothGattCallback callback} or can't + * have timeout for any other reason. This class defines the {@link #await()} method. + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public class SimpleRequest extends Request { + + SimpleRequest(@NonNull final Type type) { + super(type); + } + + SimpleRequest(@NonNull final Type type, + @Nullable final BluetoothGattCharacteristic characteristic) { + super(type, characteristic); + } + + SimpleRequest(@NonNull final Type type, + @Nullable final BluetoothGattDescriptor descriptor) { + super(type, descriptor); + } + + /** + * Synchronously waits until the request is done. + *

+ * Callbacks set using {@link #done(SuccessCallback)} and {@link #fail(FailCallback)} + * will be ignored. + *

+ * This method may not be called from the main (UI) thread. + * + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link BluetoothGatt#GATT_SUCCESS}. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter has been disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + */ + public final void await() throws RequestFailedException, DeviceDisconnectedException, + BluetoothDisabledException, InvalidRequestException { + assertNotMainThread(); + + final SuccessCallback sc = successCallback; + final FailCallback fc = failCallback; + try { + syncLock.close(); + final RequestCallback callback = new RequestCallback(); + done(callback).fail(callback).invalid(callback).enqueue(); + + syncLock.block(); + if (!callback.isSuccess()) { + if (callback.status == FailCallback.REASON_DEVICE_DISCONNECTED) { + throw new DeviceDisconnectedException(); + } + if (callback.status == FailCallback.REASON_BLUETOOTH_DISABLED) { + throw new BluetoothDisabledException(); + } + if (callback.status == RequestCallback.REASON_REQUEST_INVALID) { + throw new InvalidRequestException(this); + } + throw new RequestFailedException(this, callback.status); + } + } finally { + successCallback = sc; + failCallback = fc; + } + } +} diff --git a/_android/src/no/nordicsemi/android/ble/SimpleValueRequest.java b/_android/src/no/nordicsemi/android/ble/SimpleValueRequest.java new file mode 100644 index 0000000..7115a67 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/SimpleValueRequest.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; +import no.nordicsemi.android.ble.exception.BluetoothDisabledException; +import no.nordicsemi.android.ble.exception.DeviceDisconnectedException; +import no.nordicsemi.android.ble.exception.InvalidRequestException; +import no.nordicsemi.android.ble.exception.RequestFailedException; + +/** + * A value request that requires a {@link android.bluetooth.BluetoothGattCallback callback} or + * can't have timeout for any other reason. This class defines the {@link #await()} methods. + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class SimpleValueRequest extends SimpleRequest { + T valueCallback; + + SimpleValueRequest(@NonNull final Type type) { + super(type); + } + + SimpleValueRequest(@NonNull final Type type, + @Nullable final BluetoothGattCharacteristic characteristic) { + super(type, characteristic); + } + + SimpleValueRequest(@NonNull final Type type, + @Nullable final BluetoothGattDescriptor descriptor) { + super(type, descriptor); + } + + /** + * Sets the value callback. When the request is invoked synchronously, this callback will + * be ignored and the received value will be returned by the await(...) method; + * + * @param callback the callback. + * @return The request. + */ + @NonNull + public SimpleValueRequest with(@NonNull final T callback) { + this.valueCallback = callback; + return this; + } + + /** + * Synchronously waits until the request is done. The given response object will be filled + * with the request response. + *

+ * Callbacks set using {@link #done(SuccessCallback)} and {@link #fail(FailCallback)} and + * {@link #with(T)} will be ignored. + *

+ * This method may not be called from the main (UI) thread. + * + * @param response the response object. + * @param a response class. + * @return The response with a response. + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter is disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @see #await(Class) + */ + @NonNull + public E await(@NonNull final E response) + throws RequestFailedException, DeviceDisconnectedException, BluetoothDisabledException, + InvalidRequestException { + assertNotMainThread(); + + final T vc = valueCallback; + try { + with(response).await(); + return response; + } finally { + valueCallback = vc; + } + } + + /** + * Synchronously waits until the request is done. + *

+ * Callbacks set using {@link #done(SuccessCallback)} and {@link #fail(FailCallback)} and + * {@link #with(T)} will be ignored. + *

+ * This method may not be called from the main (UI) thread. + * + * @param responseClass the response class. This class will be instantiate, therefore it has + * to have a default constructor. + * @return The response with a response. + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread. + * @throws IllegalArgumentException thrown when the response class could not be instantiated. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter is disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @see #await(Object) + */ + @NonNull + public E await(@NonNull final Class responseClass) + throws RequestFailedException, DeviceDisconnectedException, BluetoothDisabledException, + InvalidRequestException { + assertNotMainThread(); + + try { + final E response = responseClass.newInstance(); + return await(response); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Couldn't instantiate " + + responseClass.getCanonicalName() + + " class. Is the default constructor accessible?"); + } catch (InstantiationException e) { + throw new IllegalArgumentException("Couldn't instantiate " + + responseClass.getCanonicalName() + + " class. Does it have a default constructor with no arguments?"); + } + } +} diff --git a/_android/src/no/nordicsemi/android/ble/SleepRequest.java b/_android/src/no/nordicsemi/android/ble/SleepRequest.java new file mode 100644 index 0000000..ab10d4e --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/SleepRequest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class SleepRequest extends SimpleRequest implements Operation { + private long delay; + + SleepRequest(@NonNull final Type type, @IntRange(from = 0) final long delay) { + super(type); + this.delay = delay; + } + + @NonNull + @Override + SleepRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @NonNull + @Override + public SleepRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @NonNull + @Override + public SleepRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public SleepRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @NonNull + @Override + public SleepRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + long getDelay() { + return delay; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/TimeoutHandler.java b/_android/src/no/nordicsemi/android/ble/TimeoutHandler.java new file mode 100644 index 0000000..54b4aa4 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/TimeoutHandler.java @@ -0,0 +1,13 @@ +package no.nordicsemi.android.ble; + +import androidx.annotation.NonNull; + +abstract class TimeoutHandler { + + /** + * Method called when the request timed out. + * + * @param request the request that timed out. + */ + abstract void onRequestTimeout(@NonNull final TimeoutableRequest request); +} diff --git a/_android/src/no/nordicsemi/android/ble/TimeoutableRequest.java b/_android/src/no/nordicsemi/android/ble/TimeoutableRequest.java new file mode 100644 index 0000000..9e69a14 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/TimeoutableRequest.java @@ -0,0 +1,222 @@ +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.os.Handler; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; +import no.nordicsemi.android.ble.exception.BluetoothDisabledException; +import no.nordicsemi.android.ble.exception.DeviceDisconnectedException; +import no.nordicsemi.android.ble.exception.InvalidRequestException; +import no.nordicsemi.android.ble.exception.RequestFailedException; + +@SuppressWarnings("WeakerAccess") +public abstract class TimeoutableRequest extends Request { + private TimeoutHandler timeoutHandler; + private Runnable timeoutCallback; + private Handler handler; + protected long timeout; + + TimeoutableRequest(@NonNull final Type type) { + super(type); + } + + TimeoutableRequest(@NonNull final Type type, @Nullable final BluetoothGattCharacteristic characteristic) { + super(type, characteristic); + } + + TimeoutableRequest(@NonNull final Type type, @Nullable final BluetoothGattDescriptor descriptor) { + super(type, descriptor); + } + + @NonNull + @Override + TimeoutableRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + this.handler = manager.mHandler; + this.timeoutHandler = manager; + return this; + } + + /** + * Sets the operation timeout. + * When the timeout occurs, the request will fail with {@link FailCallback#REASON_TIMEOUT}. + * + * @param timeout the request timeout in milliseconds, 0 to disable timeout. + * @return the callback. + * @throws IllegalStateException thrown when the request has already been started. + * @throws UnsupportedOperationException thrown when the timeout is not allowed for this request, + * as the callback from the system is required. + */ + @NonNull + public TimeoutableRequest timeout(@IntRange(from = 0) final long timeout) { + if (timeoutCallback != null) + throw new IllegalStateException("Request already started"); + this.timeout = timeout; + return this; + } + + /** + * Enqueues the request for asynchronous execution. + *

+ * Use {@link #timeout(long)} to set the maximum time the manager should wait until the device + * is ready. When the timeout occurs, the request will fail with + * {@link FailCallback#REASON_TIMEOUT} and the device will get disconnected. + */ + @Override + public final void enqueue() { + super.enqueue(); + } + + /** + * Enqueues the request for asynchronous execution. + *

+ * When the timeout occurs, the request will fail with {@link FailCallback#REASON_TIMEOUT} + * and the device will get disconnected. + * + * @param timeout the request timeout in milliseconds, 0 to disable timeout. This value will + * override one set in {@link #timeout(long)}. + * @deprecated Use {@link #timeout(long)} and {@link #enqueue()} instead. + */ + @Deprecated + public final void enqueue(@IntRange(from = 0) final long timeout) { + timeout(timeout).enqueue(); + } + + /** + * Synchronously waits until the request is done. + *

+ * Use {@link #timeout(long)} to set the maximum time the manager should wait until the request + * is ready. When the timeout occurs, the {@link InterruptedException} will be thrown. + *

+ * Callbacks set using {@link #done(SuccessCallback)} and {@link #fail(FailCallback)} + * will be ignored. + *

+ * This method may not be called from the main (UI) thread. + * + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link BluetoothGatt#GATT_SUCCESS}. + * @throws InterruptedException thrown if the timeout occurred before the request has + * finished. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter has been disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @see #enqueue() + */ + public final void await() throws RequestFailedException, DeviceDisconnectedException, + BluetoothDisabledException, InvalidRequestException, InterruptedException { + assertNotMainThread(); + + final SuccessCallback sc = successCallback; + final FailCallback fc = failCallback; + try { + syncLock.close(); + final RequestCallback callback = new RequestCallback(); + done(callback).fail(callback).invalid(callback).enqueue(); + + if (!syncLock.block(timeout)) { + throw new InterruptedException(); + } + if (!callback.isSuccess()) { + if (callback.status == FailCallback.REASON_DEVICE_DISCONNECTED) { + throw new DeviceDisconnectedException(); + } + if (callback.status == FailCallback.REASON_BLUETOOTH_DISABLED) { + throw new BluetoothDisabledException(); + } + if (callback.status == RequestCallback.REASON_REQUEST_INVALID) { + throw new InvalidRequestException(this); + } + throw new RequestFailedException(this, callback.status); + } + } finally { + successCallback = sc; + failCallback = fc; + } + } + + /** + * Synchronously waits, for as most as the given number of milliseconds, until the request + * is ready. + *

+ * When the timeout occurs, the {@link InterruptedException} will be thrown. + *

+ * Callbacks set using {@link #done(SuccessCallback)} and {@link #fail(FailCallback)} + * will be ignored. + *

+ * This method may not be called from the main (UI) thread. + * + * @param timeout optional timeout in milliseconds, 0 to disable timeout. This will + * override the timeout set using {@link #timeout(long)}. + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link BluetoothGatt#GATT_SUCCESS}. + * @throws InterruptedException thrown if the timeout occurred before the request has + * finished. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter has been disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @deprecated Use {@link #timeout(long)} and {@link #await()} instead. + */ + @Deprecated + public final void await(@IntRange(from = 0) final long timeout) throws RequestFailedException, + InterruptedException, DeviceDisconnectedException, BluetoothDisabledException, + InvalidRequestException { + timeout(timeout).await(); + } + + @Override + void notifyStarted(@NonNull final BluetoothDevice device) { + if (timeout > 0L) { + timeoutCallback = () -> { + timeoutCallback = null; + if (!finished) { + notifyFail(device, FailCallback.REASON_TIMEOUT); + timeoutHandler.onRequestTimeout(this); + } + }; + handler.postDelayed(timeoutCallback, timeout); + } + super.notifyStarted(device); + } + + @Override + void notifySuccess(@NonNull final BluetoothDevice device) { + if (!finished) { + handler.removeCallbacks(timeoutCallback); + timeoutCallback = null; + } + super.notifySuccess(device); + } + + @Override + void notifyFail(@NonNull final BluetoothDevice device, final int status) { + if (!finished) { + handler.removeCallbacks(timeoutCallback); + timeoutCallback = null; + } + super.notifyFail(device, status); + } + + @Override + void notifyInvalidRequest() { + if (!finished) { + handler.removeCallbacks(timeoutCallback); + timeoutCallback = null; + } + super.notifyInvalidRequest(); + } +} diff --git a/_android/src/no/nordicsemi/android/ble/TimeoutableValueRequest.java b/_android/src/no/nordicsemi/android/ble/TimeoutableValueRequest.java new file mode 100644 index 0000000..1ffcf47 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/TimeoutableValueRequest.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; +import no.nordicsemi.android.ble.exception.BluetoothDisabledException; +import no.nordicsemi.android.ble.exception.DeviceDisconnectedException; +import no.nordicsemi.android.ble.exception.InvalidRequestException; +import no.nordicsemi.android.ble.exception.RequestFailedException; + +/** + * A value request that requires a {@link android.bluetooth.BluetoothGattCallback callback} or + * can't have timeout for any other reason. This class defines the {@link #await()} methods. + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class TimeoutableValueRequest extends TimeoutableRequest { + T valueCallback; + + TimeoutableValueRequest(@NonNull final Type type) { + super(type); + } + + TimeoutableValueRequest(@NonNull final Type type, + @Nullable final BluetoothGattCharacteristic characteristic) { + super(type, characteristic); + } + + TimeoutableValueRequest(@NonNull final Type type, + @Nullable final BluetoothGattDescriptor descriptor) { + super(type, descriptor); + } + + @NonNull + @Override + public TimeoutableValueRequest timeout(@IntRange(from = 0) final long timeout) { + super.timeout(timeout); + return this; + } + + /** + * Sets the value callback. When the request is invoked synchronously, this callback will + * be ignored and the received value will be returned by the await(...) method; + * + * @param callback the callback. + * @return The request. + */ + @NonNull + public TimeoutableValueRequest with(@NonNull final T callback) { + this.valueCallback = callback; + return this; + } + + /** + * Synchronously waits until the request is done. + *

+ * When the timeout, set with {@link #timeout(long)} occurs, the {@link InterruptedException} + * will be thrown. + *

+ * Callbacks set using {@link #done(SuccessCallback)} and {@link #fail(FailCallback)} and + * {@link #with(E)} will be ignored. + *

+ * This method may not be called from the main (UI) thread. + * + * @param response the response object. + * @param a response class. + * @return The response with a response. + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. + * @throws InterruptedException thrown if the timeout occurred before the request has + * finished. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread, or when the trigger was already enqueued. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter is disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + */ + @NonNull + public E await(@NonNull final E response) + throws RequestFailedException, DeviceDisconnectedException, BluetoothDisabledException, + InvalidRequestException, InterruptedException { + assertNotMainThread(); + + final T vc = valueCallback; + try { + with(response).await(); + return response; + } finally { + valueCallback = vc; + } + } + + /** + * Synchronously waits until the request is done. + *

+ * Callbacks set using {@link #done(SuccessCallback)} and {@link #fail(FailCallback)} and + * {@link #with(T)} will be ignored. + *

+ * This method may not be called from the main (UI) thread. + * + * @param responseClass the response class. This class will be instantiate, therefore it has + * to have a default constructor. + * @return The response with a response. + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. + * @throws InterruptedException thrown if the timeout occurred before the request has + * finished. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread. + * @throws IllegalArgumentException thrown when the response class could not be instantiated. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter is disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @see #await(Object) + */ + @NonNull + public E await(@NonNull final Class responseClass) + throws RequestFailedException, DeviceDisconnectedException, BluetoothDisabledException, + InvalidRequestException, InterruptedException { + assertNotMainThread(); + + try { + final E response = responseClass.newInstance(); + return await(response); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Couldn't instantiate " + + responseClass.getCanonicalName() + + " class. Is the default constructor accessible?"); + } catch (InstantiationException e) { + throw new IllegalArgumentException("Couldn't instantiate " + + responseClass.getCanonicalName() + + " class. Does it have a default constructor with no arguments?"); + } + } + + /** + * Synchronously waits until the request is done, for at most given number of milliseconds + * after which the {@link InterruptedException} will be thrown. + *

+ * Callbacks set using {@link #done(SuccessCallback)}, {@link #fail(FailCallback)} and + * {@link #with(E)} will be ignored. + *

+ * This method may not be called from the main (UI) thread. + * + * @param responseClass the response class. This class will be instantiate, therefore it has + * to have a default constructor. + * @param timeout optional timeout in milliseconds. This value will override one set + * in {@link #timeout(long)}. + * @param a response class that extends {@link T}. + * @return The object with a response. + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. + * @throws InterruptedException thrown if the timeout occurred before the request has + * finished. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread. + * @throws IllegalArgumentException thrown when the response class could not be instantiated. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter is disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @deprecated Use {@link #timeout(long)} and {@link #await(Class)} instead. + */ + @NonNull + @Deprecated + public E await(@NonNull final Class responseClass, + @IntRange(from = 0) final long timeout) + throws RequestFailedException, InterruptedException, DeviceDisconnectedException, + BluetoothDisabledException, InvalidRequestException { + return timeout(timeout).await(responseClass); + } + + /** + * Synchronously waits until the request is done, for at most given number of milliseconds + * after which the {@link InterruptedException} will be thrown. + *

+ * Callbacks set using {@link #done(SuccessCallback)}, {@link #fail(FailCallback)} and + * {@link #with(E)} will be ignored. + *

+ * This method may not be called from the main (UI) thread. + * + * @param response the response object. + * @param timeout optional timeout in milliseconds. + * @param a response class that extends {@link T}. + * @return The object with a response. + * @throws RequestFailedException thrown when the BLE request finished with status other + * than {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. + * @throws InterruptedException thrown if the timeout occurred before the request has + * finished. + * @throws IllegalStateException thrown when you try to call this method from the main + * (UI) thread. + * @throws DeviceDisconnectedException thrown when the device disconnected before the request + * was completed. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter is disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @deprecated Use {@link #timeout(long)} and {@link #await(E)} instead. + */ + @NonNull + @Deprecated + public E await(@NonNull final E response, + @IntRange(from = 0) final long timeout) + throws RequestFailedException, InterruptedException, DeviceDisconnectedException, + BluetoothDisabledException, InvalidRequestException { + return timeout(timeout).await(response); + } +} diff --git a/_android/src/no/nordicsemi/android/ble/ValueChangedCallback.java b/_android/src/no/nordicsemi/android/ble/ValueChangedCallback.java new file mode 100644 index 0000000..7ef2610 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/ValueChangedCallback.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.DataReceivedCallback; +import no.nordicsemi.android.ble.callback.ReadProgressCallback; +import no.nordicsemi.android.ble.data.Data; +import no.nordicsemi.android.ble.data.DataFilter; +import no.nordicsemi.android.ble.data.DataMerger; +import no.nordicsemi.android.ble.data.DataStream; + +@SuppressWarnings({"unused", "WeakerAccess", "UnusedReturnValue"}) +public class ValueChangedCallback { + private ReadProgressCallback progressCallback; + private DataReceivedCallback valueCallback; + private DataMerger dataMerger; + private DataStream buffer; + private DataFilter filter; + private int count = 0; + + ValueChangedCallback() { + // empty + } + + /** + * Sets the asynchronous data callback that will be called whenever a notification or + * an indication is received on given characteristic. + * + * @param callback the data callback. + * @return The request. + */ + @NonNull + public ValueChangedCallback with(@NonNull final DataReceivedCallback callback) { + this.valueCallback = callback; + return this; + } + + /** + * Sets a filter which allows to skip some incoming data. + * + * @param filter the data filter. + * @return The request. + */ + @NonNull + public ValueChangedCallback filter(@NonNull final DataFilter filter) { + this.filter = filter; + return this; + } + + /** + * Adds a merger that will be used to merge multiple packets into a single Data. + * The merger may modify each packet if necessary. + * + * @return The request. + */ + @NonNull + public ValueChangedCallback merge(@NonNull final DataMerger merger) { + this.dataMerger = merger; + this.progressCallback = null; + return this; + } + + /** + * Adds a merger that will be used to merge multiple packets into a single Data. + * The merger may modify each packet if necessary. + * + * @return The request. + */ + @NonNull + public ValueChangedCallback merge(@NonNull final DataMerger merger, + @NonNull final ReadProgressCallback callback) { + this.dataMerger = merger; + this.progressCallback = callback; + return this; + } + + ValueChangedCallback free() { + valueCallback = null; + dataMerger = null; + progressCallback = null; + buffer = null; + return this; + } + + boolean matches(final byte[] packet) { + return filter == null || filter.filter(packet); + } + + void notifyValueChanged(@NonNull final BluetoothDevice device, @Nullable final byte[] value) { + // Keep a reference to the value callback, as it may change during execution + final DataReceivedCallback valueCallback = this.valueCallback; + + // With no value callback there is no need for any merging + if (valueCallback == null) { + return; + } + + if (dataMerger == null) { + valueCallback.onDataReceived(device, new Data(value)); + } else { + if (progressCallback != null) + progressCallback.onPacketReceived(device, value, count); + if (buffer == null) + buffer = new DataStream(); + if (dataMerger.merge(buffer, value, count++)) { + valueCallback.onDataReceived(device, buffer.toData()); + buffer = null; + count = 0; + } // else + // wait for more packets to be merged + } + } +} diff --git a/_android/src/no/nordicsemi/android/ble/WaitForValueChangedRequest.java b/_android/src/no/nordicsemi/android/ble/WaitForValueChangedRequest.java new file mode 100644 index 0000000..7b695f8 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/WaitForValueChangedRequest.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.DataReceivedCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.ReadProgressCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; +import no.nordicsemi.android.ble.callback.profile.ProfileReadResponse; +import no.nordicsemi.android.ble.data.Data; +import no.nordicsemi.android.ble.data.DataFilter; +import no.nordicsemi.android.ble.data.DataMerger; +import no.nordicsemi.android.ble.data.DataStream; +import no.nordicsemi.android.ble.exception.BluetoothDisabledException; +import no.nordicsemi.android.ble.exception.DeviceDisconnectedException; +import no.nordicsemi.android.ble.exception.InvalidDataException; +import no.nordicsemi.android.ble.exception.InvalidRequestException; +import no.nordicsemi.android.ble.exception.RequestFailedException; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class WaitForValueChangedRequest extends TimeoutableValueRequest + implements Operation { + static final int NOT_STARTED = -123456; + static final int STARTED = NOT_STARTED + 1; + + private ReadProgressCallback progressCallback; + private DataMerger dataMerger; + private DataStream buffer; + private DataFilter filter; + private Request trigger; + private boolean deviceDisconnected; + private boolean bluetoothDisabled; + private int triggerStatus = BluetoothGatt.GATT_SUCCESS; + private int count = 0; + + WaitForValueChangedRequest(@NonNull final Type type, + @Nullable final BluetoothGattCharacteristic characteristic) { + super(type, characteristic); + } + + @NonNull + @Override + WaitForValueChangedRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @NonNull + @Override + public WaitForValueChangedRequest timeout(@IntRange(from = 0) final long timeout) { + super.timeout(timeout); + return this; + } + + @NonNull + @Override + public WaitForValueChangedRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @NonNull + @Override + public WaitForValueChangedRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public WaitForValueChangedRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public WaitForValueChangedRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + @NonNull + @Override + public WaitForValueChangedRequest with(@NonNull final DataReceivedCallback callback) { + super.with(callback); + return this; + } + + /** + * Sets a filter which allows to skip some incoming data. + * + * @param filter the data filter. + * @return The request. + */ + @NonNull + public WaitForValueChangedRequest filter(@NonNull final DataFilter filter) { + this.filter = filter; + return this; + } + + /** + * Adds a merger that will be used to merge multiple packets into a single Data. + * The merger may modify each packet if necessary. + * + * @return The request. + */ + @NonNull + public WaitForValueChangedRequest merge(@NonNull final DataMerger merger) { + this.dataMerger = merger; + this.progressCallback = null; + return this; + } + + /** + * Adds a merger that will be used to merge multiple packets into a single Data. + * The merger may modify each packet if necessary. + * + * @return The request. + */ + @NonNull + public WaitForValueChangedRequest merge(@NonNull final DataMerger merger, + @NonNull final ReadProgressCallback callback) { + this.dataMerger = merger; + this.progressCallback = callback; + return this; + } + + /** + * Sets an optional request that is suppose to trigger the notification or indication. + * This is to ensure that the characteristic value won't change before the callback was set. + * + * @param trigger the operation that triggers the notification, usually a write characteristic + * request that write some OP CODE. + * @return The request. + */ + @NonNull + public WaitForValueChangedRequest trigger(@NonNull final Operation trigger) { + if (trigger instanceof Request) { + this.trigger = (Request) trigger; + this.triggerStatus = NOT_STARTED; + // The trigger will never receive invalid request event. + // If the BluetoothDevice wasn't set, the whole WaitForValueChangedRequest would be invalid. + /*this.trigger.invalid(() -> { + // never called + });*/ + this.trigger.internalBefore(device -> triggerStatus = STARTED); + this.trigger.internalSuccess(device -> triggerStatus = BluetoothGatt.GATT_SUCCESS); + this.trigger.internalFail((device, status) -> { + triggerStatus = status; + syncLock.open(); + notifyFail(device, status); + }); + } + return this; + } + + @NonNull + @Override + public E await(@NonNull final E response) + throws RequestFailedException, DeviceDisconnectedException, BluetoothDisabledException, + InvalidRequestException, InterruptedException { + assertNotMainThread(); + + try { + // Ensure the trigger request it enqueued after the callback has been set. + if (trigger != null && trigger.enqueued) { + throw new IllegalStateException("Trigger request already enqueued"); + } + super.await(response); + return response; + } catch (final RequestFailedException e) { + if (triggerStatus != BluetoothGatt.GATT_SUCCESS) { + // Trigger will never have invalid request status. The outer request will. + /*if (triggerStatus == RequestCallback.REASON_REQUEST_INVALID) { + throw new InvalidRequestException(trigger); + }*/ + throw new RequestFailedException(trigger, triggerStatus); + } + throw e; + } + } + + /** + * Similar to {@link #await(Class)}, but if the response class extends + * {@link ProfileReadResponse} and the received response is invalid, an exception is thrown. + * This allows to keep all error handling in one place. + * + * @param response the result object. + * @param a response class that extends {@link ProfileReadResponse}. + * @return Object with a valid response. + * @throws IllegalStateException thrown when you try to call this method from + * the main (UI) thread. + * @throws InterruptedException thrown when the timeout occurred before the + * characteristic value has changed. + * @throws RequestFailedException thrown when the trigger request has failed. + * @throws DeviceDisconnectedException thrown when the device disconnected before the + * notification or indication was received. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter has been disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @throws InvalidDataException thrown when the received data were not valid (that is when + * {@link ProfileReadResponse#onDataReceived(BluetoothDevice, Data)} + * failed to parse the data correctly and called + * {@link ProfileReadResponse#onInvalidDataReceived(BluetoothDevice, Data)}). + */ + @SuppressWarnings("ConstantConditions") + @NonNull + public E awaitValid(@NonNull final E response) + throws RequestFailedException, InvalidDataException, DeviceDisconnectedException, + BluetoothDisabledException, InvalidRequestException, InterruptedException { + final E result = await(response); + if (result != null && !result.isValid()) { + throw new InvalidDataException(result); + } + return result; + } + + /** + * Similar to {@link #await(DataReceivedCallback)}, but if the response class extends + * {@link ProfileReadResponse} and the received response is invalid, an exception is thrown. + * This allows to keep all error handling in one place. + * + * @param responseClass the result class. This class will be instantiate, therefore it + * has to have a default constructor. + * @param a response class that extends {@link ProfileReadResponse}. + * @return Object with a valid response. + * @throws IllegalStateException thrown when you try to call this method from + * the main (UI) thread. + * @throws InterruptedException thrown when the timeout occurred before the + * characteristic value has changed. + * @throws IllegalArgumentException thrown when the response class could not be instantiated. + * @throws RequestFailedException thrown when the trigger request has failed. + * @throws DeviceDisconnectedException thrown when the device disconnected before the + * notification or indication was received. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter has been disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @throws InvalidDataException thrown when the received data were not valid (that is when + * {@link ProfileReadResponse#onDataReceived(BluetoothDevice, Data)} + * failed to parse the data correctly and called + * {@link ProfileReadResponse#onInvalidDataReceived(BluetoothDevice, Data)}). + */ + @SuppressWarnings("ConstantConditions") + @NonNull + public E awaitValid(@NonNull final Class responseClass) + throws RequestFailedException, InvalidDataException, DeviceDisconnectedException, + BluetoothDisabledException, InvalidRequestException, InterruptedException { + final E response = await(responseClass); + if (response != null && !response.isValid()) { + throw new InvalidDataException(response); + } + return response; + } + + /** + * Same as {@link #await(Class, long)}, but if received response is not valid, this method will + * thrown an exception. + * + * @param responseClass the result class. This class will be instantiate, therefore it + * has to have a default constructor. + * @param timeout optional timeout in milliseconds. + * @param a response class that extends {@link ProfileReadResponse}. + * @return Object with a valid response. + * @throws IllegalStateException thrown when you try to call this method from + * the main (UI) thread. + * @throws InterruptedException thrown when the timeout occurred before the + * characteristic value has changed. + * @throws IllegalArgumentException thrown when the response class could not be instantiated. + * @throws RequestFailedException thrown when the trigger request has failed. + * @throws DeviceDisconnectedException thrown when the device disconnected before the + * notification or indication was received. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter has been disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @throws InvalidDataException thrown when the received data were not valid (that is when + * {@link ProfileReadResponse#onDataReceived(BluetoothDevice, Data)} + * failed to parse the data correctly and called + * {@link ProfileReadResponse#onInvalidDataReceived(BluetoothDevice, Data)}). + * @deprecated Use {@link #timeout(long)} and {@link #awaitValid(Class)} instead. + */ + @SuppressWarnings("ConstantConditions") + @NonNull + @Deprecated + public E awaitValid(@NonNull final Class responseClass, + @IntRange(from = 0) final long timeout) + throws InterruptedException, InvalidDataException, RequestFailedException, + DeviceDisconnectedException, BluetoothDisabledException, InvalidRequestException { + return timeout(timeout).awaitValid(responseClass); + } + + /** + * Same as {@link #await(Object, long)}, but if received response is not valid, + * this method will thrown an exception. + * + * @param response the result object. + * @param timeout optional timeout in milliseconds. + * @param a response class that extends {@link ProfileReadResponse}. + * @return Object with a valid response. + * @throws IllegalStateException thrown when you try to call this method from + * the main (UI) thread. + * @throws InterruptedException thrown when the timeout occurred before the + * characteristic value has changed. + * @throws RequestFailedException thrown when the trigger request has failed. + * @throws DeviceDisconnectedException thrown when the device disconnected before the + * notification or indication was received. + * @throws BluetoothDisabledException thrown when the Bluetooth adapter has been disabled. + * @throws InvalidRequestException thrown when the request was called before the device was + * connected at least once (unknown device). + * @throws InvalidDataException thrown when the received data were not valid (that is when + * {@link ProfileReadResponse#onDataReceived(BluetoothDevice, Data)} + * failed to parse the data correctly and called + * {@link ProfileReadResponse#onInvalidDataReceived(BluetoothDevice, Data)}). + * @deprecated Use {@link #timeout(long)} and {@link #awaitValid(E)} instead. + */ + @SuppressWarnings("ConstantConditions") + @NonNull + @Deprecated + public E awaitValid(@NonNull final E response, + @IntRange(from = 0) final long timeout) + throws InterruptedException, InvalidDataException, DeviceDisconnectedException, + RequestFailedException, BluetoothDisabledException, InvalidRequestException { + return timeout(timeout).awaitValid(response); + } + + boolean matches(final byte[] packet) { + return filter == null || filter.filter(packet); + } + + void notifyValueChanged(final BluetoothDevice device, final byte[] value) { + // Keep a reference to the value callback, as it may change during execution + final DataReceivedCallback valueCallback = this.valueCallback; + + // With no value callback there is no need for any merging + if (valueCallback == null) { + return; + } + + if (dataMerger == null) { + valueCallback.onDataReceived(device, new Data(value)); + } else { + if (progressCallback != null) + progressCallback.onPacketReceived(device, value, count); + if (buffer == null) + buffer = new DataStream(); + if (dataMerger.merge(buffer, value, count++)) { + valueCallback.onDataReceived(device, buffer.toData()); + buffer = null; + count = 0; + } // else + // wait for more packets to be merged + } + } + + boolean hasMore() { + return count > 0; + } + + @Nullable + Request getTrigger() { + return trigger; + } + + boolean isTriggerPending() { + return triggerStatus == NOT_STARTED; + } + + boolean isTriggerCompleteOrNull() { + return triggerStatus != STARTED; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/WriteRequest.java b/_android/src/no/nordicsemi/android/ble/WriteRequest.java new file mode 100644 index 0000000..6944283 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/WriteRequest.java @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +import java.util.Arrays; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.annotation.WriteType; +import no.nordicsemi.android.ble.callback.BeforeCallback; +import no.nordicsemi.android.ble.callback.DataSentCallback; +import no.nordicsemi.android.ble.callback.FailCallback; +import no.nordicsemi.android.ble.callback.InvalidRequestCallback; +import no.nordicsemi.android.ble.callback.SuccessCallback; +import no.nordicsemi.android.ble.callback.WriteProgressCallback; +import no.nordicsemi.android.ble.data.Data; +import no.nordicsemi.android.ble.data.DataSplitter; +import no.nordicsemi.android.ble.data.DefaultMtuSplitter; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public final class WriteRequest extends SimpleValueRequest implements Operation { + private final static DataSplitter MTU_SPLITTER = new DefaultMtuSplitter(); + + private WriteProgressCallback progressCallback; + private DataSplitter dataSplitter; + private final byte[] data; + private final int writeType; + private byte[] currentChunk; + private byte[] nextChunk; + private int count = 0; + private boolean complete = false; + + WriteRequest(@NonNull final Type type) { + this(type, null); + } + + WriteRequest(@NonNull final Type type, @Nullable final BluetoothGattCharacteristic characteristic) { + super(type, characteristic); + // not used: + this.data = null; + this.writeType = 0; + // getData(int) isn't called on enabling and disabling notifications/indications. + this.complete = true; + } + + WriteRequest(@NonNull final Type type, @Nullable final BluetoothGattCharacteristic characteristic, + @Nullable final byte[] data, + @IntRange(from = 0) final int offset, @IntRange(from = 0) final int length, + @WriteType final int writeType) { + super(type, characteristic); + this.data = copy(data, offset, length); + this.writeType = writeType; + } + + WriteRequest(@NonNull final Type type, @Nullable final BluetoothGattDescriptor descriptor, + @Nullable final byte[] data, + @IntRange(from = 0) final int offset, @IntRange(from = 0) final int length) { + super(type, descriptor); + this.data = copy(data, offset, length); + this.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT; + } + + @NonNull + @Override + WriteRequest setManager(@NonNull final BleManager manager) { + super.setManager(manager); + return this; + } + + @Override + @NonNull + public WriteRequest done(@NonNull final SuccessCallback callback) { + super.done(callback); + return this; + } + + @Override + @NonNull + public WriteRequest fail(@NonNull final FailCallback callback) { + super.fail(callback); + return this; + } + + @NonNull + @Override + public WriteRequest invalid(@NonNull final InvalidRequestCallback callback) { + super.invalid(callback); + return this; + } + + @Override + @NonNull + public WriteRequest before(@NonNull final BeforeCallback callback) { + super.before(callback); + return this; + } + + @Override + @NonNull + public WriteRequest with(@NonNull final DataSentCallback callback) { + super.with(callback); + return this; + } + + /** + * Adds a splitter that will be used to cut given data into multiple packets. + * The splitter may modify each packet if necessary, i.e. add a flag indicating first packet, + * continuation or the last packet. + * + * @param splitter an implementation of a splitter. + * @return The request. + * @see #split() + */ + @NonNull + public WriteRequest split(@NonNull final DataSplitter splitter) { + this.dataSplitter = splitter; + this.progressCallback = null; + return this; + } + + /** + * Adds a splitter that will be used to cut given data into multiple packets. + * The splitter may modify each packet if necessary, i.e. add a flag indicating first packet, + * continuation or the last packet. + * + * @param splitter an implementation of a splitter. + * @param callback the progress callback that will be notified each time a packet was sent. + * @return The request. + * @see #split() + */ + @NonNull + public WriteRequest split(@NonNull final DataSplitter splitter, + @NonNull final WriteProgressCallback callback) { + this.dataSplitter = splitter; + this.progressCallback = callback; + return this; + } + + /** + * Adds a default MTU splitter that will be used to cut given data into at-most MTU-3 + * bytes long packets. + * + * @return The request. + */ + @NonNull + public WriteRequest split() { + this.dataSplitter = MTU_SPLITTER; + this.progressCallback = null; + return this; + } + + /** + * Adds a default MTU splitter that will be used to cut given data into at-most MTU-3 + * bytes long packets. + * + * @param callback the progress callback that will be notified each time a packet was sent. + * @return The request. + */ + @NonNull + public WriteRequest split(@NonNull final WriteProgressCallback callback) { + this.dataSplitter = MTU_SPLITTER; + this.progressCallback = callback; + return this; + } + + /** + * This method makes sure the data sent will be split to at-most MTU-3 bytes long packets. + * This is because Long Write does not work with Reliable Write. + */ + void forceSplit() { + if (dataSplitter == null) + split(); + } + + /** + * Returns the next chunk to be sent. If data splitter was not set the date returned may + * be longer than MTU. Android will try to send them using Long Write sub-procedure if + * write type is {@link BluetoothGattCharacteristic#WRITE_TYPE_DEFAULT}. Other write types + * will cause the data to be truncated. + * + * @param mtu the current MTU. + * @return The next bytes to be sent. + */ + byte[] getData(@IntRange(from = 23, to = 517) final int mtu) { + if (dataSplitter == null || data == null) { + complete = true; + return currentChunk = data; + } + + // Write Request and Write Command require 3 bytes for handler and op code. + // Write Signed requires 12 bytes, as the signature is sent. + final int maxLength = writeType != BluetoothGattCharacteristic.WRITE_TYPE_SIGNED ? + mtu - 3 : mtu - 12; + + byte[] chunk = nextChunk; + // Get the first chunk. + if (chunk == null) { + chunk = dataSplitter.chunk(data, count, maxLength); + } + // If there's something to send, check if there are any more packets to be sent later. + if (chunk != null) { + nextChunk = dataSplitter.chunk(data, count + 1, maxLength); + } + // If there's no next packet left, we are done. + if (nextChunk == null) { + complete = true; + } + return currentChunk = chunk; + } + + /** + * Method called when packet has been sent and confirmed (when Write With Response was used), + * or added to local outgoing buffer (when Write Without Response was used). + * + * @param device the target device. + * @param data the data received in the + * {@link android.bluetooth.BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int)}. + * @return True, if the data received are equal to data sent. + */ + boolean notifyPacketSent(@NonNull final BluetoothDevice device, @Nullable final byte[] data) { + if (progressCallback != null) + progressCallback.onPacketSent(device, data, count); + count++; + if (complete && valueCallback != null) + valueCallback.onDataSent(device, new Data(WriteRequest.this.data)); + return Arrays.equals(data, currentChunk); + } + + /** + * Returns whether there are more bytes to be sent from this Write Request. + * @return True if not all data were sent, false if the request is complete. + */ + boolean hasMore() { + return !complete; + } + + /** + * Returns the write type that should be used to send the data. + * @return The write type. + */ + @WriteType + int getWriteType() { + return writeType; + } + + private static byte[] copy(@Nullable final byte[] value, + @IntRange(from = 0) final int offset, + @IntRange(from = 0) final int length) { + if (value == null || offset > value.length) + return null; + final int maxLength = Math.min(value.length - offset, length); + final byte[] copy = new byte[maxLength]; + System.arraycopy(value, offset, copy, 0, maxLength); + return copy; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/annotation/ConnectionPriority.java b/_android/src/no/nordicsemi/android/ble/annotation/ConnectionPriority.java new file mode 100644 index 0000000..313bf6d --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/annotation/ConnectionPriority.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import androidx.annotation.IntDef; +import no.nordicsemi.android.ble.ConnectionPriorityRequest; + +@Retention(RetentionPolicy.SOURCE) +@IntDef(value = { + ConnectionPriorityRequest.CONNECTION_PRIORITY_BALANCED, + ConnectionPriorityRequest.CONNECTION_PRIORITY_HIGH, + ConnectionPriorityRequest.CONNECTION_PRIORITY_LOW_POWER +}) +public @interface ConnectionPriority {} \ No newline at end of file diff --git a/_android/src/no/nordicsemi/android/ble/annotation/ConnectionState.java b/_android/src/no/nordicsemi/android/ble/annotation/ConnectionState.java new file mode 100644 index 0000000..ec19d2a --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/annotation/ConnectionState.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.annotation; + +import android.bluetooth.BluetoothProfile; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import androidx.annotation.IntDef; + +@Retention(RetentionPolicy.SOURCE) +@IntDef(value = { + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.STATE_CONNECTING, + BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.STATE_DISCONNECTING, +}) +public @interface ConnectionState {} \ No newline at end of file diff --git a/_android/src/no/nordicsemi/android/ble/annotation/PhyMask.java b/_android/src/no/nordicsemi/android/ble/annotation/PhyMask.java new file mode 100644 index 0000000..cd7efd7 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/annotation/PhyMask.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import androidx.annotation.IntDef; +import no.nordicsemi.android.ble.PhyRequest; + +@Retention(RetentionPolicy.SOURCE) +@IntDef(flag = true, value = { + PhyRequest.PHY_LE_1M_MASK, + PhyRequest.PHY_LE_2M_MASK, + PhyRequest.PHY_LE_CODED_MASK +}) +public @interface PhyMask {} diff --git a/_android/src/no/nordicsemi/android/ble/annotation/PhyOption.java b/_android/src/no/nordicsemi/android/ble/annotation/PhyOption.java new file mode 100644 index 0000000..84fb5cf --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/annotation/PhyOption.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import androidx.annotation.IntDef; +import no.nordicsemi.android.ble.PhyRequest; + +@Retention(RetentionPolicy.SOURCE) +@IntDef(value = { + PhyRequest.PHY_OPTION_NO_PREFERRED, + PhyRequest.PHY_OPTION_S2, + PhyRequest.PHY_OPTION_S8 +}) +public @interface PhyOption {} \ No newline at end of file diff --git a/_android/src/no/nordicsemi/android/ble/annotation/PhyValue.java b/_android/src/no/nordicsemi/android/ble/annotation/PhyValue.java new file mode 100644 index 0000000..6b1623f --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/annotation/PhyValue.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import androidx.annotation.IntDef; +import no.nordicsemi.android.ble.callback.PhyCallback; + +@Retention(RetentionPolicy.SOURCE) +@IntDef(value = { + PhyCallback.PHY_LE_1M, + PhyCallback.PHY_LE_2M, + PhyCallback.PHY_LE_CODED +}) +public @interface PhyValue {} diff --git a/_android/src/no/nordicsemi/android/ble/annotation/WriteType.java b/_android/src/no/nordicsemi/android/ble/annotation/WriteType.java new file mode 100644 index 0000000..03bbce1 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/annotation/WriteType.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.annotation; + +import android.bluetooth.BluetoothGattCharacteristic; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import androidx.annotation.IntDef; + +@Retention(RetentionPolicy.SOURCE) +@IntDef(value = { + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT, + BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE, + BluetoothGattCharacteristic.WRITE_TYPE_SIGNED, +}) +public @interface WriteType {} \ No newline at end of file diff --git a/_android/src/no/nordicsemi/android/ble/callback/BeforeCallback.java b/_android/src/no/nordicsemi/android/ble/callback/BeforeCallback.java new file mode 100644 index 0000000..3eb287f --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/BeforeCallback.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; + +public interface BeforeCallback { + + /** + * A callback invoked when the request is about to start. + * + * @param device the target device. + */ + void onRequestStarted(@NonNull final BluetoothDevice device); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/ConnectionPriorityCallback.java b/_android/src/no/nordicsemi/android/ble/callback/ConnectionPriorityCallback.java new file mode 100644 index 0000000..3e6ebed --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/ConnectionPriorityCallback.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +/** + * The connection parameters for a Bluetooth LE connection is a set of parameters that determine + * when and how the Central and a Peripheral in a link transmits data. + * It is always the Central that actually sets the connection parameters used, but the Peripheral + * can send a so-called Connection Parameter Update Request, that the Central can then accept or reject. + *

+ * On Android, requesting connection parameters is available since Android Lollipop using + * {@link android.bluetooth.BluetoothGatt#requestConnectionPriority(int)}. There are 3 options + * available: {@link android.bluetooth.BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}, + * {@link android.bluetooth.BluetoothGatt#CONNECTION_PRIORITY_BALANCED} and + * {@link android.bluetooth.BluetoothGatt#CONNECTION_PRIORITY_HIGH}. See + * {@link no.nordicsemi.android.ble.Request#newConnectionPriorityRequest(int)} for details. + *

+ * Until Android 8.0 Oreo, there was no callback indicating whether the change has succeeded, + * or not. Also, when a Central or Peripheral requested connection parameters change without + * explicit calling of this method, the application was not aware of it. + * Android Oreo added a hidden callback to {@link android.bluetooth.BluetoothGattCallback} + * notifying about connection parameters change. Those values will be reported with this callback. + */ +public interface ConnectionPriorityCallback { + + /** + * Callback indicating the connection parameters were updated. Works on Android 8.0 Oreo or newer. + * + * @param device the target device. + * @param interval Connection interval used on this connection, 1.25ms unit. Valid range is from + * 6 (7.5ms) to 3200 (4000ms). + * @param latency Slave latency for the connection in number of connection events. Valid range + * is from 0 to 499. + * @param timeout Supervision timeout for this connection, in 10ms unit. Valid range is from 10 + * (100 ms = 0.1s) to 3200 (32s). + */ + void onConnectionUpdated(@NonNull final BluetoothDevice device, + @IntRange(from = 6, to = 3200) final int interval, + @IntRange(from = 0, to = 499) final int latency, + @IntRange(from = 10, to = 3200) final int timeout); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/DataReceivedCallback.java b/_android/src/no/nordicsemi/android/ble/callback/DataReceivedCallback.java new file mode 100644 index 0000000..668a61c --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/DataReceivedCallback.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.data.Data; +import no.nordicsemi.android.ble.data.DataMerger; + +public interface DataReceivedCallback { + + /** + * Callback received each time the value was read or has changed using + * notifications or indications. + * + * @param device the target device. + * @param data the data received. If the {@link DataMerger} was used, + * this contains the merged result. + */ + void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/DataSentCallback.java b/_android/src/no/nordicsemi/android/ble/callback/DataSentCallback.java new file mode 100644 index 0000000..572b62a --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/DataSentCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.data.Data; +import no.nordicsemi.android.ble.data.DataSplitter; + +public interface DataSentCallback { + + /** + * Callback received each time the value was written to a characteristic or descriptor. + * + * @param device the target device. + * @param data the data sent. If the {@link DataSplitter} was used, this contains the full data. + */ + void onDataSent(@NonNull final BluetoothDevice device, @NonNull final Data data); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/FailCallback.java b/_android/src/no/nordicsemi/android/ble/callback/FailCallback.java new file mode 100644 index 0000000..4af98bb --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/FailCallback.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; + +public interface FailCallback { + int REASON_DEVICE_DISCONNECTED = -1; + int REASON_DEVICE_NOT_SUPPORTED = -2; + int REASON_NULL_ATTRIBUTE = -3; + int REASON_REQUEST_FAILED = -4; + int REASON_TIMEOUT = -5; + int REASON_VALIDATION = -6; + int REASON_BLUETOOTH_DISABLED = -100; + + /** + * A callback invoked when the request has failed with status other than + * {@link android.bluetooth.BluetoothGatt#GATT_SUCCESS}. + * + * @param device target device. + * @param status error status code, one of BluetoothGatt#GATT_* constants or + * {@link #REASON_DEVICE_DISCONNECTED}, {@link #REASON_TIMEOUT}, + * {@link #REASON_DEVICE_NOT_SUPPORTED} (only for Connect request), + * {@link #REASON_BLUETOOTH_DISABLED}, {@link #REASON_NULL_ATTRIBUTE}, + * {@link #REASON_VALIDATION} or {@link #REASON_REQUEST_FAILED} (for other reason). + */ + void onRequestFailed(@NonNull final BluetoothDevice device, final int status); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/InvalidRequestCallback.java b/_android/src/no/nordicsemi/android/ble/callback/InvalidRequestCallback.java new file mode 100644 index 0000000..cb48051 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/InvalidRequestCallback.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +public interface InvalidRequestCallback { + + /** + * A callback invoked when the request was invalid, for example when was called before the + * device was connected. + */ + void onInvalidRequest(); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/MtuCallback.java b/_android/src/no/nordicsemi/android/ble/callback/MtuCallback.java new file mode 100644 index 0000000..7879ce8 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/MtuCallback.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +public interface MtuCallback { + + /** + * Method called when the MTU request has finished with success. The MTU value may + * be different than requested one. The maximum packet size is 3 bytes less then MTU. + * + * @param device the target device. + * @param mtu the new MTU (Maximum Transfer Unit). + */ + void onMtuChanged(@NonNull final BluetoothDevice device, + @IntRange(from = 23, to = 517) final int mtu); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/PhyCallback.java b/_android/src/no/nordicsemi/android/ble/callback/PhyCallback.java new file mode 100644 index 0000000..8dc13d6 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/PhyCallback.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.annotation.PhyValue; + +@SuppressWarnings("unused") +public interface PhyCallback { + /** + * Bluetooth LE 1M PHY. Used to refer to LE 1M Physical Channel for advertising, scanning or + * connection. + */ + int PHY_LE_1M = 1; + + /** + * Bluetooth LE 2M PHY. Used to refer to LE 2M Physical Channel for advertising, scanning or + * connection. + */ + int PHY_LE_2M = 2; + + /** + * Bluetooth LE Coded PHY. Used to refer to LE Coded Physical Channel for advertising, scanning + * or connection. + */ + int PHY_LE_CODED = 3; + + /** + * Method called when the PHY value has changed or was read. + * + * @param device the target device. + * @param txPhy the transmitter PHY in use. One of {@link #PHY_LE_1M}, + * {@link #PHY_LE_2M}, and {@link #PHY_LE_CODED}. + * @param rxPhy the receiver PHY in use. One of {@link #PHY_LE_1M}, + * {@link #PHY_LE_2M}, and {@link #PHY_LE_CODED}. + */ + void onPhyChanged(@NonNull final BluetoothDevice device, + @PhyValue final int txPhy, @PhyValue final int rxPhy); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/ReadProgressCallback.java b/_android/src/no/nordicsemi/android/ble/callback/ReadProgressCallback.java new file mode 100644 index 0000000..61d0e88 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/ReadProgressCallback.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.data.DataMerger; + +public interface ReadProgressCallback { + + /** + * Callback received each time the value was read or has changed using notifications or + * indications when {@link DataMerger} was used. + * + * @param device the target device. + * @param data the last packet received. + * @param index the index of a packet that will be merged into a single Data. + */ + void onPacketReceived(@NonNull final BluetoothDevice device, + @Nullable final byte[] data, @IntRange(from = 0) final int index); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/RssiCallback.java b/_android/src/no/nordicsemi/android/ble/callback/RssiCallback.java new file mode 100644 index 0000000..9ca6873 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/RssiCallback.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +public interface RssiCallback { + + /** + * Method called when the RSSI value has been read. + * + * @param device the target device. + * @param rssi the current RSSI value, in dBm. + */ + void onRssiRead(@NonNull final BluetoothDevice device, + @IntRange(from = -128, to = 20) final int rssi); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/SuccessCallback.java b/_android/src/no/nordicsemi/android/ble/callback/SuccessCallback.java new file mode 100644 index 0000000..99367ad --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/SuccessCallback.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; + +public interface SuccessCallback { + + /** + * A callback invoked when the request completed successfully. + * + * @param device the target device. + */ + void onRequestCompleted(@NonNull final BluetoothDevice device); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/WriteProgressCallback.java b/_android/src/no/nordicsemi/android/ble/callback/WriteProgressCallback.java new file mode 100644 index 0000000..d62752f --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/WriteProgressCallback.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.data.DataSplitter; + +public interface WriteProgressCallback { + + /** + * Callback called each time a packet has been sent when {@link DataSplitter} was used. + * + * @param device the target device. + * @param data the last packet sent. + * @param index the index of a packet that the initial Data was cut into. + */ + void onPacketSent(@NonNull final BluetoothDevice device, + @Nullable final byte[] data, @IntRange(from = 0) final int index); +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/profile/ProfileDataCallback.java b/_android/src/no/nordicsemi/android/ble/callback/profile/ProfileDataCallback.java new file mode 100644 index 0000000..a47c770 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/profile/ProfileDataCallback.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback.profile; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.callback.DataReceivedCallback; +import no.nordicsemi.android.ble.data.Data; +import no.nordicsemi.android.ble.data.DataMerger; + +@SuppressWarnings("unused") +public interface ProfileDataCallback extends DataReceivedCallback { + + /** + * Callback called when the data received do not conform to required scheme. + * @param device the target device. + * @param data the data received. If the {@link DataMerger} was used, this contains the + * merged result. + */ + default void onInvalidDataReceived(@NonNull final BluetoothDevice device, + @NonNull final Data data) { + // ignore + } +} diff --git a/_android/src/no/nordicsemi/android/ble/callback/profile/ProfileReadResponse.java b/_android/src/no/nordicsemi/android/ble/callback/profile/ProfileReadResponse.java new file mode 100644 index 0000000..b26e025 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/callback/profile/ProfileReadResponse.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.callback.profile; + +import android.bluetooth.BluetoothDevice; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.data.Data; +import no.nordicsemi.android.ble.response.ReadResponse; + +/** + * A response type for read requests with basic validation check. + * When read was requested as a synchronous call the {@link #isValid()} can be used to + * check if data were parsed successfully. Parsing method must call super methods on + * both {@link #onDataReceived(BluetoothDevice, Data)} and + * {@link #onInvalidDataReceived(BluetoothDevice, Data)} in order to make getters working properly. + *

+ * Check out profile data callbacks in the Android BLE Common Library for example of usage. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class ProfileReadResponse extends ReadResponse implements ProfileDataCallback, Parcelable { + private boolean valid = true; + + public ProfileReadResponse() { + // empty + } + + @Override + public void onInvalidDataReceived(@NonNull final BluetoothDevice device, + @NonNull final Data data) { + this.valid = false; + } + + /** + * Returns true if {@link #onInvalidDataReceived(BluetoothDevice, Data)} wasn't called. + * + * @return True, if profile data were valid, false if parsing error occurred. + */ + public boolean isValid() { + return valid; + } + + // Parcelable + protected ProfileReadResponse(final Parcel in) { + super(in); + valid = in.readByte() != 0; + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + super.writeToParcel(dest, flags); + dest.writeByte((byte) (valid ? 1 : 0)); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ProfileReadResponse createFromParcel(final Parcel in) { + return new ProfileReadResponse(in); + } + + @Override + public ProfileReadResponse[] newArray(final int size) { + return new ProfileReadResponse[size]; + } + }; +} diff --git a/_android/src/no/nordicsemi/android/ble/data/Data.java b/_android/src/no/nordicsemi/android/ble/data/Data.java new file mode 100644 index 0000000..e22dc4e --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/data/Data.java @@ -0,0 +1,470 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.data; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.IntDef; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@SuppressWarnings({"WeakerAccess", "unused", "UnusedReturnValue"}) +public class Data implements Parcelable { + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + FORMAT_UINT8, + FORMAT_UINT16, + FORMAT_UINT24, + FORMAT_UINT32, + FORMAT_SINT8, + FORMAT_SINT16, + FORMAT_SINT24, + FORMAT_SINT32, + FORMAT_FLOAT, + FORMAT_SFLOAT + }) + public @interface ValueFormat {} + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + FORMAT_UINT8, + FORMAT_UINT16, + FORMAT_UINT24, + FORMAT_UINT32, + FORMAT_SINT8, + FORMAT_SINT16, + FORMAT_SINT24, + FORMAT_SINT32 + }) + public @interface IntFormat {} + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + FORMAT_UINT32, + FORMAT_SINT32 + }) + public @interface LongFormat {} + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + FORMAT_FLOAT, + FORMAT_SFLOAT + }) + public @interface FloatFormat {} + + private static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + + /** + * Data value format type uint8 + */ + public final static int FORMAT_UINT8 = 0x11; + + /** + * Data value format type uint16 + */ + public final static int FORMAT_UINT16 = 0x12; + + /** + * Data value format type uint24 + */ + public final static int FORMAT_UINT24 = 0x13; + + /** + * Data value format type uint32 + */ + public final static int FORMAT_UINT32 = 0x14; + + /** + * Data value format type sint8 + */ + public final static int FORMAT_SINT8 = 0x21; + + /** + * Data value format type sint16 + */ + public final static int FORMAT_SINT16 = 0x22; + + /** + * Data value format type sint24 + */ + public final static int FORMAT_SINT24 = 0x23; + + /** + * Data value format type sint32 + */ + public final static int FORMAT_SINT32 = 0x24; + + /** + * Data value format type sfloat (16-bit float, IEEE-11073) + */ + public final static int FORMAT_SFLOAT = 0x32; + + /** + * Data value format type float (32-bit float, IEEE-11073) + */ + public final static int FORMAT_FLOAT = 0x34; + + protected byte[] mValue; + + public Data() { + this.mValue = null; + } + + public Data(@Nullable final byte[] value) { + this.mValue = value; + } + + public static Data from(@NonNull final String value) { + return new Data(value.getBytes()); // UTF-8 + } + + public static Data from(@NonNull final BluetoothGattCharacteristic characteristic) { + return new Data(characteristic.getValue()); + } + + public static Data from(@NonNull final BluetoothGattDescriptor descriptor) { + return new Data(descriptor.getValue()); + } + + public static Data opCode(final byte opCode) { + return new Data(new byte[] { opCode }); + } + + public static Data opCode(final byte opCode, final byte parameter) { + return new Data(new byte[] { opCode, parameter }); + } + + /** + * Returns the underlying byte array. + * + * @return Data received. + */ + @Nullable + public byte[] getValue() { + return mValue; + } + + /** + * Return the stored value of this characteristic. + *

See {@link #getValue} for details. + * + * @param offset Offset at which the string value can be found. + * @return Cached value of the characteristic + */ + @Nullable + public String getStringValue(@IntRange(from = 0) final int offset) { + if (mValue == null || offset > mValue.length) + return null; + final byte[] strBytes = new byte[mValue.length - offset]; + for (int i = 0; i != (mValue.length - offset); ++i) + strBytes[i] = mValue[offset+i]; + return new String(strBytes); + } + + /** + * Returns the size of underlying byte array. + * + * @return Length of the data. + */ + public int size() { + return mValue != null ? mValue.length : 0; + } + + @Override + public String toString() { + if (size() == 0) + return ""; + + final char[] out = new char[mValue.length * 3 - 1]; + for (int j = 0; j < mValue.length; j++) { + int v = mValue[j] & 0xFF; + out[j * 3] = HEX_ARRAY[v >>> 4]; + out[j * 3 + 1] = HEX_ARRAY[v & 0x0F]; + if (j != mValue.length - 1) + out[j * 3 + 2] = '-'; + } + return "(0x) " + new String(out); + } + + /** + * Returns a byte at the given offset from the byte array. + * + * @param offset Offset at which the byte value can be found. + * @return Cached value or null of offset exceeds value size. + */ + @Nullable + public Byte getByte(@IntRange(from = 0) final int offset) { + if (offset + 1 > size()) return null; + + return mValue[offset]; + } + + /** + * Returns an integer value from the byte array. + *

+ *

The formatType parameter determines how the value + * is to be interpreted. For example, setting formatType to + * {@link #FORMAT_UINT16} specifies that the first two bytes of the + * value at the given offset are interpreted to generate the + * return value. + * + * @param formatType The format type used to interpret the value. + * @param offset Offset at which the integer value can be found. + * @return Cached value or null of offset exceeds value size. + */ + @Nullable + public Integer getIntValue(@IntFormat final int formatType, + @IntRange(from = 0) final int offset) { + if ((offset + getTypeLen(formatType)) > size()) return null; + + switch (formatType) { + case FORMAT_UINT8: + return unsignedByteToInt(mValue[offset]); + + case FORMAT_UINT16: + return unsignedBytesToInt(mValue[offset], mValue[offset + 1]); + + case FORMAT_UINT24: + return unsignedBytesToInt(mValue[offset], mValue[offset + 1], + mValue[offset + 2], (byte) 0); + + case FORMAT_UINT32: + return unsignedBytesToInt(mValue[offset], mValue[offset + 1], + mValue[offset + 2], mValue[offset + 3]); + + case FORMAT_SINT8: + return unsignedToSigned(unsignedByteToInt(mValue[offset]), 8); + + case FORMAT_SINT16: + return unsignedToSigned(unsignedBytesToInt(mValue[offset], + mValue[offset + 1]), 16); + + case FORMAT_SINT24: + return unsignedToSigned(unsignedBytesToInt(mValue[offset], + mValue[offset + 1], mValue[offset + 2], (byte) 0), 24); + + case FORMAT_SINT32: + return unsignedToSigned(unsignedBytesToInt(mValue[offset], + mValue[offset + 1], mValue[offset + 2], mValue[offset + 3]), 32); + } + + return null; + } + + /** + * Returns a long value from the byte array. + *

Only {@link #FORMAT_UINT32} and {@link #FORMAT_SINT32} are supported. + *

The formatType parameter determines how the value + * is to be interpreted. For example, setting formatType to + * {@link #FORMAT_UINT32} specifies that the first four bytes of the + * value at the given offset are interpreted to generate the + * return value. + * + * @param formatType The format type used to interpret the value. + * @param offset Offset at which the integer value can be found. + * @return Cached value or null of offset exceeds value size. + */ + @Nullable + public Long getLongValue(@LongFormat final int formatType, + @IntRange(from = 0) final int offset) { + if ((offset + getTypeLen(formatType)) > size()) return null; + + switch (formatType) { + case FORMAT_SINT32: + return unsignedToSigned(unsignedBytesToLong(mValue[offset], + mValue[offset + 1], mValue[offset + 2], mValue[offset + 3]), 32); + + case FORMAT_UINT32: + return unsignedBytesToLong(mValue[offset], mValue[offset + 1], + mValue[offset + 2], mValue[offset + 3]); + } + + return null; + } + + /** + * Returns an float value from the given byte array. + * + * @param formatType The format type used to interpret the value. + * @param offset Offset at which the float value can be found. + * @return Cached value at a given offset or null if the requested offset exceeds the value size. + */ + @Nullable + public Float getFloatValue(@FloatFormat final int formatType, + @IntRange(from = 0) final int offset) { + if ((offset + getTypeLen(formatType)) > size()) return null; + + switch (formatType) { + case FORMAT_SFLOAT: + if (mValue[offset + 1] == 0x07 && mValue[offset] == (byte) 0xFE) + return Float.POSITIVE_INFINITY; + if ((mValue[offset + 1] == 0x07 && mValue[offset] == (byte) 0xFF) || + (mValue[offset + 1] == 0x08 && mValue[offset] == 0x00) || + (mValue[offset + 1] == 0x08 && mValue[offset] == 0x01)) + return Float.NaN; + if (mValue[offset + 1] == 0x08 && mValue[offset] == 0x02) + return Float.NEGATIVE_INFINITY; + + return bytesToFloat(mValue[offset], mValue[offset + 1]); + + case FORMAT_FLOAT: + if (mValue[offset + 3] == 0x00) { + if (mValue[offset + 2] == 0x7F && mValue[offset + 1] == (byte) 0xFF) { + if (mValue[offset] == (byte) 0xFE) + return Float.POSITIVE_INFINITY; + if (mValue[offset] == (byte) 0xFF) + return Float.NaN; + } else if (mValue[offset + 2] == (byte) 0x80 && mValue[offset + 1] == 0x00) { + if (mValue[offset] == 0x00 || mValue[offset] == 0x01) + return Float.NaN; + if (mValue[offset] == 0x02) + return Float.NEGATIVE_INFINITY; + } + } + + return bytesToFloat(mValue[offset], mValue[offset + 1], + mValue[offset + 2], mValue[offset + 3]); + } + + return null; + } + + /** + * Returns the size of a give value type. + */ + public static int getTypeLen(@ValueFormat final int formatType) { + return formatType & 0xF; + } + + /** + * Convert a signed byte to an unsigned int. + */ + private static int unsignedByteToInt(final byte b) { + return b & 0xFF; + } + + /** + * Convert a signed byte to an unsigned int. + */ + private static long unsignedByteToLong(final byte b) { + return b & 0xFFL; + } + + /** + * Convert signed bytes to a 16-bit unsigned int. + */ + private static int unsignedBytesToInt(final byte b0, final byte b1) { + return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8)); + } + + /** + * Convert signed bytes to a 32-bit unsigned int. + */ + private static int unsignedBytesToInt(final byte b0, final byte b1, final byte b2, final byte b3) { + return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8)) + + (unsignedByteToInt(b2) << 16) + (unsignedByteToInt(b3) << 24); + } + + /** + * Convert signed bytes to a 32-bit unsigned long. + */ + private static long unsignedBytesToLong(final byte b0, final byte b1, final byte b2, final byte b3) { + return (unsignedByteToLong(b0) + (unsignedByteToLong(b1) << 8)) + + (unsignedByteToLong(b2) << 16) + (unsignedByteToLong(b3) << 24); + } + + /** + * Convert signed bytes to a 16-bit short float value. + */ + private static float bytesToFloat(final byte b0, final byte b1) { + int mantissa = unsignedToSigned(unsignedByteToInt(b0) + + ((unsignedByteToInt(b1) & 0x0F) << 8), 12); + int exponent = unsignedToSigned(unsignedByteToInt(b1) >> 4, 4); + return (float) (mantissa * Math.pow(10, exponent)); + } + + /** + * Convert signed bytes to a 32-bit short float value. + */ + private static float bytesToFloat(final byte b0, final byte b1, final byte b2, final byte b3) { + int mantissa = unsignedToSigned(unsignedByteToInt(b0) + + (unsignedByteToInt(b1) << 8) + + (unsignedByteToInt(b2) << 16), 24); + return (float) (mantissa * Math.pow(10, b3)); + } + + /** + * Convert an unsigned integer value to a two's-complement encoded + * signed value. + */ + private static int unsignedToSigned(int unsigned, final int size) { + if ((unsigned & (1 << size - 1)) != 0) { + unsigned = -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1))); + } + return unsigned; + } + + /** + * Convert an unsigned long value to a two's-complement encoded + * signed value. + */ + private static long unsignedToSigned(long unsigned, final int size) { + if ((unsigned & (1 << size - 1)) != 0) { + unsigned = -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1))); + } + return unsigned; + } + + // Parcelable + protected Data(final Parcel in) { + mValue = in.createByteArray(); + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeByteArray(mValue); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public Data createFromParcel(final Parcel in) { + return new Data(in); + } + + @Override + public Data[] newArray(final int size) { + return new Data[size]; + } + }; +} diff --git a/_android/src/no/nordicsemi/android/ble/data/DataFilter.java b/_android/src/no/nordicsemi/android/ble/data/DataFilter.java new file mode 100644 index 0000000..321da2f --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/data/DataFilter.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.data; + +import androidx.annotation.Nullable; + +public interface DataFilter { + + /** + * This method should return true if the packet matches the filter and should be processed + * by the request. + * + * @param lastPacket the packet received. + * @return True, if packet should be processed, false if it should be ignored. + */ + boolean filter(@Nullable final byte[] lastPacket); +} diff --git a/_android/src/no/nordicsemi/android/ble/data/DataMerger.java b/_android/src/no/nordicsemi/android/ble/data/DataMerger.java new file mode 100644 index 0000000..2aa81f0 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/data/DataMerger.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.data; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface DataMerger { + + /** + * This method should merge the last packet into the output message. + * + * @param output the stream for the output message, initially empty. + * @param lastPacket the data received in the last read/notify/indicate operation. + * @param index an index of the packet, 0-based (if you expect 3 packets, they will be + * called with indexes 0, 1, 2). + * @return True, if the message is complete, false if more data are expected. + */ + boolean merge(@NonNull final DataStream output, + @Nullable final byte[] lastPacket, @IntRange(from = 0) final int index); +} diff --git a/_android/src/no/nordicsemi/android/ble/data/DataSplitter.java b/_android/src/no/nordicsemi/android/ble/data/DataSplitter.java new file mode 100644 index 0000000..1cb1e9d --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/data/DataSplitter.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.data; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface DataSplitter { + + /** + * The implementation should return a index'th byte array from given message, + * with at most maxLength size, or null if no bytes are left to be sent. + * + * @param message the full message to be chunk. + * @param index index of a packet, 0-based. + * @param maxLength maximum length of the returned packet. Equals to MTU-3. + * Use {@link no.nordicsemi.android.ble.BleManager#requestMtu(int)} to request + * higher MTU, or {@link no.nordicsemi.android.ble.BleManager#overrideMtu(int)} + * If the MTU change was initiated by the target device. + * @return The packet to be sent, or null, if the whole message was already split. + */ + @Nullable + byte[] chunk(@NonNull final byte[] message, + @IntRange(from = 0) final int index, @IntRange(from = 20) final int maxLength); +} diff --git a/_android/src/no/nordicsemi/android/ble/data/DataStream.java b/_android/src/no/nordicsemi/android/ble/data/DataStream.java new file mode 100644 index 0000000..4aba77e --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/data/DataStream.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.data; + +import java.io.ByteArrayOutputStream; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +@SuppressWarnings("WeakerAccess") +public class DataStream { + private final ByteArrayOutputStream buffer; + + public DataStream() { + buffer = new ByteArrayOutputStream(); + } + + @SuppressWarnings("SimplifiableIfStatement") + public boolean write(@Nullable final byte[] data) { + if (data == null) + return false; + + return write(data, 0, data.length); + } + + public boolean write(@Nullable final byte[] data, + @IntRange(from = 0) final int offset, @IntRange(from = 0) final int length) { + if (data == null || data.length < offset) + return false; + + final int len = Math.min(data.length - offset, length); + buffer.write(data, offset, len); + return true; + } + + public boolean write(@Nullable final Data data) { + return data != null && write(data.getValue()); + } + + @IntRange(from = 0) + public int size() { + return buffer.size(); + } + + @NonNull + public byte[] toByteArray() { + return buffer.toByteArray(); + } + + @NonNull + public Data toData() { + return new Data(buffer.toByteArray()); + } +} diff --git a/_android/src/no/nordicsemi/android/ble/data/DefaultMtuSplitter.java b/_android/src/no/nordicsemi/android/ble/data/DefaultMtuSplitter.java new file mode 100644 index 0000000..5c15484 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/data/DefaultMtuSplitter.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.data; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Splits the message into at-most MTU-3 size packets. + */ +public final class DefaultMtuSplitter implements DataSplitter { + + @Nullable + @Override + public byte[] chunk(@NonNull final byte[] message, + @IntRange(from = 0) final int index, + @IntRange(from = 20) final int maxLength) { + final int offset = index * maxLength; + final int length = Math.min(maxLength, message.length - offset); + + if (length <= 0) + return null; + + final byte[] data = new byte[length]; + System.arraycopy(message, offset, data, 0, length); + return data; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/data/MutableData.java b/_android/src/no/nordicsemi/android/ble/data/MutableData.java new file mode 100644 index 0000000..acfe914 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/data/MutableData.java @@ -0,0 +1,409 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.data; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +@SuppressWarnings({"unused", "SameParameterValue", "WeakerAccess", "UnusedReturnValue"}) +public class MutableData extends Data { + // Values required to convert float to IEEE-11073 SFLOAT + private final static int SFLOAT_POSITIVE_INFINITY = 0x07FE; + private final static int SFLOAT_NAN = 0x07FF; + // private final static int SFLOAT_NOT_AT_THIS_RESOLUTION = 0x0800; + // private final static int SFLOAT_RESERVED_VALUE = 0x0801; + private final static int SFLOAT_NEGATIVE_INFINITY = 0x0802; + private final static int SFLOAT_MANTISSA_MAX = 0x07FD; + private final static int SFLOAT_EXPONENT_MAX = 7; + private final static int SFLOAT_EXPONENT_MIN = -8; + private final static float SFLOAT_MAX = 20450000000.0f; + private final static float SFLOAT_MIN = -SFLOAT_MAX; + private final static int SFLOAT_PRECISION = 10000; + // private final static float SFLOAT_EPSILON = 1e-8f; + + // Values required to convert float to IEEE-11073 FLOAT + private final static int FLOAT_POSITIVE_INFINITY = 0x007FFFFE; + private final static int FLOAT_NAN = 0x007FFFFF; + // private final static int FLOAT_NOT_AT_THIS_RESOLUTION = 0x00800000; + // private final static int FLOAT_RESERVED_VALUE = 0x00800001; + private final static int FLOAT_NEGATIVE_INFINITY = 0x00800002; + private final static int FLOAT_MANTISSA_MAX = 0x007FFFFD; + private final static int FLOAT_EXPONENT_MAX = 127; + private final static int FLOAT_EXPONENT_MIN = -128; + private final static int FLOAT_PRECISION = 10000000; + // private final static float FLOAT_EPSILON = 1e-128f; + + public MutableData() { + super(); + } + + public MutableData(@Nullable final byte[] data) { + super(data); + } + + public static MutableData from(@NonNull final BluetoothGattCharacteristic characteristic) { + return new MutableData(characteristic.getValue()); + } + + public static MutableData from(@NonNull final BluetoothGattDescriptor descriptor) { + return new MutableData(descriptor.getValue()); + } + + /** + * Updates the locally stored value of this data. + * + * @param value New value + * @return true if the locally stored value has been set, false if the + * requested value could not be stored locally. + */ + public boolean setValue(@Nullable final byte[] value) { + mValue = value; + return true; + } + + /** + * Updates the byte at offset position. + * + * @param value Byte to set + * @param offset The offset + * @return true if the locally stored value has been set, false if the + * requested value could not be stored locally. + */ + public boolean setByte(final int value, @IntRange(from = 0) final int offset) { + final int len = offset + 1; + if (mValue == null) mValue = new byte[len]; + if (len > mValue.length) return false; + mValue[offset] = (byte) value; + return true; + } + + /** + * Set the locally stored value of this data. + *

See {@link #setValue(byte[])} for details. + * + * @param value New value for this data + * @param formatType Integer format type used to transform the value parameter + * @param offset Offset at which the value should be placed + * @return true if the locally stored value has been set + */ + public boolean setValue(int value, @IntFormat int formatType, @IntRange(from = 0) int offset) { + final int len = offset + getTypeLen(formatType); + if (mValue == null) mValue = new byte[len]; + if (len > mValue.length) return false; + + switch (formatType) { + case FORMAT_SINT8: + value = intToSignedBits(value, 8); + // Fall-through intended + case FORMAT_UINT8: + mValue[offset] = (byte) (value & 0xFF); + break; + + case FORMAT_SINT16: + value = intToSignedBits(value, 16); + // Fall-through intended + case FORMAT_UINT16: + mValue[offset++] = (byte) (value & 0xFF); + mValue[offset] = (byte) ((value >> 8) & 0xFF); + break; + + case FORMAT_SINT24: + value = intToSignedBits(value, 24); + // Fall-through intended + case FORMAT_UINT24: + mValue[offset++] = (byte) (value & 0xFF); + mValue[offset++] = (byte) ((value >> 8) & 0xFF); + mValue[offset] = (byte) ((value >> 16) & 0xFF); + break; + + case FORMAT_SINT32: + value = intToSignedBits(value, 32); + // Fall-through intended + case FORMAT_UINT32: + mValue[offset++] = (byte) (value & 0xFF); + mValue[offset++] = (byte) ((value >> 8) & 0xFF); + mValue[offset++] = (byte) ((value >> 16) & 0xFF); + mValue[offset] = (byte) ((value >> 24) & 0xFF); + break; + + default: + return false; + } + return true; + } + + /** + * Set the locally stored value of this data. + *

See {@link #setValue(byte[])} for details. + * + * @param mantissa Mantissa for this data + * @param exponent Exponent value for this data + * @param formatType Float format type used to transform the value parameter + * @param offset Offset at which the value should be placed + * @return true if the locally stored value has been set + */ + public boolean setValue(int mantissa, int exponent, + @FloatFormat int formatType, @IntRange(from = 0) int offset) { + final int len = offset + getTypeLen(formatType); + if (mValue == null) mValue = new byte[len]; + if (len > mValue.length) return false; + + switch (formatType) { + case FORMAT_SFLOAT: + mantissa = intToSignedBits(mantissa, 12); + exponent = intToSignedBits(exponent, 4); + mValue[offset++] = (byte) (mantissa & 0xFF); + mValue[offset] = (byte) ((mantissa >> 8) & 0x0F); + mValue[offset] += (byte) ((exponent & 0x0F) << 4); + break; + + case FORMAT_FLOAT: + mantissa = intToSignedBits(mantissa, 24); + exponent = intToSignedBits(exponent, 8); + mValue[offset++] = (byte) (mantissa & 0xFF); + mValue[offset++] = (byte) ((mantissa >> 8) & 0xFF); + mValue[offset++] = (byte) ((mantissa >> 16) & 0xFF); + mValue[offset] += (byte) (exponent & 0xFF); + break; + + default: + return false; + } + + return true; + } + + /** + * Set the locally stored value of this data. + *

See {@link #setValue(byte[])} for details. + * + * @param value New value for this data. This allows to send {@link #FORMAT_UINT32}. + * @param formatType Integer format type used to transform the value parameter + * @param offset Offset at which the value should be placed + * @return true if the locally stored value has been set + */ + public boolean setValue(long value, @LongFormat int formatType, @IntRange(from = 0) int offset) { + final int len = offset + getTypeLen(formatType); + if (mValue == null) mValue = new byte[len]; + if (len > mValue.length) return false; + + switch (formatType) { + case FORMAT_SINT32: + value = longToSignedBits(value, 32); + // Fall-through intended + case FORMAT_UINT32: + mValue[offset++] = (byte) (value & 0xFF); + mValue[offset++] = (byte) ((value >> 8) & 0xFF); + mValue[offset++] = (byte) ((value >> 16) & 0xFF); + mValue[offset] = (byte) ((value >> 24) & 0xFF); + break; + + default: + return false; + } + return true; + } + + /** + * Set the locally stored value of this data. + *

See {@link #setValue(byte[])} for details. + * + * @param value Float value to be written + * @param formatType Float format type used to transform the value parameter + * @param offset Offset at which the value should be placed + * @return true if the locally stored value has been set + */ + public boolean setValue(float value, + @FloatFormat int formatType, @IntRange(from = 0) int offset) { + final int len = offset + getTypeLen(formatType); + if (mValue == null) mValue = new byte[len]; + if (len > mValue.length) return false; + + switch (formatType) { + case FORMAT_SFLOAT: + final int sfloatAsInt = sfloatToInt(value); + mValue[offset++] = (byte) (sfloatAsInt & 0xFF); + mValue[offset] = (byte) ((sfloatAsInt >> 8) & 0xFF); + break; + + case FORMAT_FLOAT: + final int floatAsInt = floatToInt(value); + mValue[offset++] = (byte) (floatAsInt & 0xFF); + mValue[offset++] = (byte) ((floatAsInt >> 8) & 0xFF); + mValue[offset++] = (byte) ((floatAsInt >> 16) & 0xFF); + mValue[offset] += (byte) ((floatAsInt >> 24) & 0xFF); + break; + + default: + return false; + } + + return true; + } + + /** + * Converts float to SFLOAT IEEE 11073 format as UINT16, rounding up or down. + * See: https://github.com/signove/antidote/blob/master/src/util/bytelib.c + * + * @param value the value to be converted. + * @return given float as UINT16 in IEEE 11073 format. + */ + private static int sfloatToInt(final float value) { + if (Float.isNaN(value)) { + return SFLOAT_NAN; + } else if (value > SFLOAT_MAX) { + return SFLOAT_POSITIVE_INFINITY; + } else if (value < SFLOAT_MIN) { + return SFLOAT_NEGATIVE_INFINITY; + } + + int sign = value >= 0 ? +1 : -1; + float mantissa = Math.abs(value); + int exponent = 0; // Note: 10**x exponent, not 2**x + + // scale up if number is too big + while (mantissa > SFLOAT_MANTISSA_MAX) { + mantissa /= 10.0f; + ++exponent; + if (exponent > SFLOAT_EXPONENT_MAX) { + // argh, should not happen + if (sign > 0) { + return SFLOAT_POSITIVE_INFINITY; + } else { + return SFLOAT_NEGATIVE_INFINITY; + } + } + } + + // scale down if number is too small + while (mantissa < 1) { + mantissa *= 10; + --exponent; + if (exponent < SFLOAT_EXPONENT_MIN) { + // argh, should not happen + return 0; + } + } + + // scale down if number needs more precision + double smantissa = Math.round(mantissa * SFLOAT_PRECISION); + double rmantissa = Math.round(mantissa) * SFLOAT_PRECISION; + double mdiff = Math.abs(smantissa - rmantissa); + while (mdiff > 0.5 && exponent > SFLOAT_EXPONENT_MIN && + (mantissa * 10) <= SFLOAT_MANTISSA_MAX) { + mantissa *= 10; + --exponent; + smantissa = Math.round(mantissa * SFLOAT_PRECISION); + rmantissa = Math.round(mantissa) * SFLOAT_PRECISION; + mdiff = Math.abs(smantissa - rmantissa); + } + + int int_mantissa = Math.round(sign * mantissa); + return ((exponent & 0xF) << 12) | (int_mantissa & 0xFFF); + } + + /** + * Converts float to FLOAT IEEE 11073 format as UINT32, rounding up or down. + * See: https://github.com/signove/antidote/blob/master/src/util/bytelib.c + * + * @param value the value to be converted. + * @return given float as UINT32 in IEEE 11073 format. + */ + private static int floatToInt(final float value) { + if (Float.isNaN(value)) { + return FLOAT_NAN; + } else if (value == Float.POSITIVE_INFINITY) { + return FLOAT_POSITIVE_INFINITY; + } else if (value == Float.NEGATIVE_INFINITY) { + return FLOAT_NEGATIVE_INFINITY; + } + + int sign = value >= 0 ? +1 : -1; + float mantissa = Math.abs(value); + int exponent = 0; // Note: 10**x exponent, not 2**x + + // scale up if number is too big + while (mantissa > FLOAT_MANTISSA_MAX) { + mantissa /= 10.0f; + ++exponent; + if (exponent > FLOAT_EXPONENT_MAX) { + // argh, should not happen + if (sign > 0) { + return FLOAT_POSITIVE_INFINITY; + } else { + return FLOAT_NEGATIVE_INFINITY; + } + } + } + + // scale down if number is too small + while (mantissa < 1) { + mantissa *= 10; + --exponent; + if (exponent < FLOAT_EXPONENT_MIN) { + // argh, should not happen + return 0; + } + } + + // scale down if number needs more precision + double smantissa = Math.round(mantissa * FLOAT_PRECISION); + double rmantissa = Math.round(mantissa) * FLOAT_PRECISION; + double mdiff = Math.abs(smantissa - rmantissa); + while (mdiff > 0.5 && exponent > FLOAT_EXPONENT_MIN && + (mantissa * 10) <= FLOAT_MANTISSA_MAX) { + mantissa *= 10; + --exponent; + smantissa = Math.round(mantissa * FLOAT_PRECISION); + rmantissa = Math.round(mantissa) * FLOAT_PRECISION; + mdiff = Math.abs(smantissa - rmantissa); + } + + int int_mantissa = Math.round(sign * mantissa); + return (exponent << 24) | (int_mantissa & 0xFFFFFF); + } + + /** + * Convert an integer into the signed bits of a given length. + */ + private static int intToSignedBits(int i, int size) { + if (i < 0) { + i = (1 << size - 1) + (i & ((1 << size - 1) - 1)); + } + return i; + } + + /** + * Convert a long into the signed bits of a given length. + */ + private static long longToSignedBits(long i, int size) { + if (i < 0) { + i = (1L << size - 1) + (i & ((1L << size - 1) - 1)); + } + return i; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/error/GattError.java b/_android/src/no/nordicsemi/android/ble/error/GattError.java new file mode 100644 index 0000000..39dc53c --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/error/GattError.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.ble.error; + +import android.bluetooth.BluetoothGatt; + +/** + * Parses the GATT and HCI errors to human readable strings. + *

+ * See: gatt_api.h for details.
+ * See also: hcidefs.h for other possible HCI errors. + */ +@SuppressWarnings("WeakerAccess") +public class GattError { + public static final int GATT_SUCCESS = BluetoothGatt.GATT_SUCCESS; + public static final int GATT_CONN_L2C_FAILURE = 0x01; + public static final int GATT_CONN_TIMEOUT = 0x08; + public static final int GATT_CONN_TERMINATE_PEER_USER = 0x13; + public static final int GATT_CONN_TERMINATE_LOCAL_HOST = 0x16; + public static final int GATT_CONN_FAIL_ESTABLISH = 0x3E; + public static final int GATT_CONN_LMP_TIMEOUT = 0x22; + public static final int GATT_CONN_CANCEL = 0x0100; + public static final int GATT_ERROR = 0x0085; // Device not reachable + + public static final int GATT_INVALID_HANDLE = 0x0001; + public static final int GATT_READ_NOT_PERMIT = 0x0002; + public static final int GATT_WRITE_NOT_PERMIT = 0x0003; + public static final int GATT_INVALID_PDU = 0x0004; + public static final int GATT_INSUF_AUTHENTICATION = 0x0005; + public static final int GATT_REQ_NOT_SUPPORTED = 0x0006; + public static final int GATT_INVALID_OFFSET = 0x0007; + public static final int GATT_INSUF_AUTHORIZATION = 0x0008; + public static final int GATT_PREPARE_Q_FULL = 0x0009; + public static final int GATT_NOT_FOUND = 0x000a; + public static final int GATT_NOT_LONG = 0x000b; + public static final int GATT_INSUF_KEY_SIZE = 0x000c; + public static final int GATT_INVALID_ATTR_LEN = 0x000d; + public static final int GATT_ERR_UNLIKELY = 0x000e; + public static final int GATT_INSUF_ENCRYPTION = 0x000f; + public static final int GATT_UNSUPPORT_GRP_TYPE = 0x0010; + public static final int GATT_INSUF_RESOURCE = 0x0011; + public static final int GATT_CONTROLLER_BUSY = 0x003A; + public static final int GATT_UNACCEPT_CONN_INTERVAL = 0x003B; + public static final int GATT_ILLEGAL_PARAMETER = 0x0087; + public static final int GATT_NO_RESOURCES = 0x0080; + public static final int GATT_INTERNAL_ERROR = 0x0081; + public static final int GATT_WRONG_STATE = 0x0082; + public static final int GATT_DB_FULL = 0x0083; + public static final int GATT_BUSY = 0x0084; + public static final int GATT_CMD_STARTED = 0x0086; + public static final int GATT_PENDING = 0x0088; + public static final int GATT_AUTH_FAIL = 0x0089; + public static final int GATT_MORE = 0x008a; + public static final int GATT_INVALID_CFG = 0x008b; + public static final int GATT_SERVICE_STARTED = 0x008c; + public static final int GATT_ENCRYPTED_NO_MITM = 0x008d; + public static final int GATT_NOT_ENCRYPTED = 0x008e; + public static final int GATT_CONGESTED = 0x008f; + public static final int GATT_CCCD_CFG_ERROR = 0x00FD; + public static final int GATT_PROCEDURE_IN_PROGRESS = 0x00FE; + public static final int GATT_VALUE_OUT_OF_RANGE = 0x00FF; + public static final int TOO_MANY_OPEN_CONNECTIONS = 0x0101; + + /** + * Converts the connection status given by the + * {@link android.bluetooth.BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int)} + * to error name. + * + * @param error the status number. + * @return The error name as stated in the links in {@link GattError} documentation. + */ + public static String parseConnectionError(final int error) { + switch (error) { + case GATT_SUCCESS: + return "SUCCESS"; + case GATT_CONN_L2C_FAILURE: + return "GATT CONN L2C FAILURE"; + case GATT_CONN_TIMEOUT: + return "GATT CONN TIMEOUT"; + case GATT_CONN_TERMINATE_PEER_USER: + return "GATT CONN TERMINATE PEER USER"; + case GATT_CONN_TERMINATE_LOCAL_HOST: + return "GATT CONN TERMINATE LOCAL HOST"; + case GATT_CONN_FAIL_ESTABLISH: + return "GATT CONN FAIL ESTABLISH"; + case GATT_CONN_LMP_TIMEOUT: + return "GATT CONN LMP TIMEOUT"; + case GATT_CONN_CANCEL: + return "GATT CONN CANCEL "; + case GATT_ERROR: + return "GATT ERROR"; // Device not reachable + default: + return "UNKNOWN (" + error + ")"; + } + } + + /** + * Converts the Bluetooth communication status given by other BluetoothGattCallbacks to error + * name. It also parses the DFU errors. + * + * @param error the status number. + * @return The error name as stated in the links in {@link GattError} documentation. + */ + public static String parse(final int error) { + switch (error) { + case GATT_INVALID_HANDLE: + return "GATT INVALID HANDLE"; + case GATT_READ_NOT_PERMIT: + return "GATT READ NOT PERMIT"; + case GATT_WRITE_NOT_PERMIT: + return "GATT WRITE NOT PERMIT"; + case GATT_INVALID_PDU: + return "GATT INVALID PDU"; + case GATT_INSUF_AUTHENTICATION: + return "GATT INSUF AUTHENTICATION"; + case GATT_REQ_NOT_SUPPORTED: + return "GATT REQ NOT SUPPORTED"; + case GATT_INVALID_OFFSET: + return "GATT INVALID OFFSET"; + case GATT_INSUF_AUTHORIZATION: + return "GATT INSUF AUTHORIZATION"; + case GATT_PREPARE_Q_FULL: + return "GATT PREPARE Q FULL"; + case GATT_NOT_FOUND: + return "GATT NOT FOUND"; + case GATT_NOT_LONG: + return "GATT NOT LONG"; + case GATT_INSUF_KEY_SIZE: + return "GATT INSUF KEY SIZE"; + case GATT_INVALID_ATTR_LEN: + return "GATT INVALID ATTR LEN"; + case GATT_ERR_UNLIKELY: + return "GATT ERR UNLIKELY"; + case GATT_INSUF_ENCRYPTION: + return "GATT INSUF ENCRYPTION"; + case GATT_UNSUPPORT_GRP_TYPE: + return "GATT UNSUPPORT GRP TYPE"; + case GATT_INSUF_RESOURCE: + return "GATT INSUF RESOURCE"; + case GATT_CONN_LMP_TIMEOUT: + return "GATT CONN LMP TIMEOUT"; + case GATT_CONTROLLER_BUSY: + return "GATT CONTROLLER BUSY"; + case GATT_UNACCEPT_CONN_INTERVAL: + return "GATT UNACCEPT CONN INTERVAL"; + case GATT_ILLEGAL_PARAMETER: + return "GATT ILLEGAL PARAMETER"; + case GATT_NO_RESOURCES: + return "GATT NO RESOURCES"; + case GATT_INTERNAL_ERROR: + return "GATT INTERNAL ERROR"; + case GATT_WRONG_STATE: + return "GATT WRONG STATE"; + case GATT_DB_FULL: + return "GATT DB FULL"; + case GATT_BUSY: + return "GATT BUSY"; + case GATT_ERROR: + return "GATT ERROR"; + case GATT_CMD_STARTED: + return "GATT CMD STARTED"; + case GATT_PENDING: + return "GATT PENDING"; + case GATT_AUTH_FAIL: + return "GATT AUTH FAIL"; + case GATT_MORE: + return "GATT MORE"; + case GATT_INVALID_CFG: + return "GATT INVALID CFG"; + case GATT_SERVICE_STARTED: + return "GATT SERVICE STARTED"; + case GATT_ENCRYPTED_NO_MITM: + return "GATT ENCRYPTED NO MITM"; + case GATT_NOT_ENCRYPTED: + return "GATT NOT ENCRYPTED"; + case GATT_CONGESTED: + return "GATT CONGESTED"; + case GATT_CCCD_CFG_ERROR: + return "GATT CCCD CFG ERROR"; + case GATT_PROCEDURE_IN_PROGRESS: + return "GATT PROCEDURE IN PROGRESS"; + case GATT_VALUE_OUT_OF_RANGE: + return "GATT VALUE OUT OF RANGE"; + case TOO_MANY_OPEN_CONNECTIONS: + return "TOO MANY OPEN CONNECTIONS"; + default: + return "UNKNOWN (" + error + ")"; + } + } +} diff --git a/_android/src/no/nordicsemi/android/ble/exception/BluetoothDisabledException.java b/_android/src/no/nordicsemi/android/ble/exception/BluetoothDisabledException.java new file mode 100644 index 0000000..7c07421 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/exception/BluetoothDisabledException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.exception; + +public class BluetoothDisabledException extends ConnectionException { +} diff --git a/_android/src/no/nordicsemi/android/ble/exception/ConnectionException.java b/_android/src/no/nordicsemi/android/ble/exception/ConnectionException.java new file mode 100644 index 0000000..40794d7 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/exception/ConnectionException.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.exception; + +@SuppressWarnings("WeakerAccess") +public class ConnectionException extends Exception { +} diff --git a/_android/src/no/nordicsemi/android/ble/exception/DeviceDisconnectedException.java b/_android/src/no/nordicsemi/android/ble/exception/DeviceDisconnectedException.java new file mode 100644 index 0000000..ccca470 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/exception/DeviceDisconnectedException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.exception; + +public class DeviceDisconnectedException extends ConnectionException { +} diff --git a/_android/src/no/nordicsemi/android/ble/exception/InvalidDataException.java b/_android/src/no/nordicsemi/android/ble/exception/InvalidDataException.java new file mode 100644 index 0000000..3ae6a05 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/exception/InvalidDataException.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.exception; + +import androidx.annotation.NonNull; +import no.nordicsemi.android.ble.callback.profile.ProfileReadResponse; + +@SuppressWarnings("unused") +public final class InvalidDataException extends Exception { + private final ProfileReadResponse response; + + public InvalidDataException(@NonNull final ProfileReadResponse response) { + this.response = response; + } + + public ProfileReadResponse getResponse() { + return response; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/exception/InvalidRequestException.java b/_android/src/no/nordicsemi/android/ble/exception/InvalidRequestException.java new file mode 100644 index 0000000..3ab4d09 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/exception/InvalidRequestException.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.exception; + +import no.nordicsemi.android.ble.Request; + +@SuppressWarnings({"WeakerAccess", "unused"}) +public final class InvalidRequestException extends Exception { + private final Request request; + + public InvalidRequestException(final Request request) { + super("Invalid request"); + this.request = request; + } + + /** + * Returns the invalid request. + * @return The invalid request. + */ + public Request getRequest() { + return request; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/exception/RequestFailedException.java b/_android/src/no/nordicsemi/android/ble/exception/RequestFailedException.java new file mode 100644 index 0000000..677798a --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/exception/RequestFailedException.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.exception; + +import no.nordicsemi.android.ble.Request; + +@SuppressWarnings({"WeakerAccess", "unused"}) +public final class RequestFailedException extends Exception { + private final Request request; + private final int status; + + public RequestFailedException(final Request request, final int status) { + super("Request failed with status " + status); + this.request = request; + this.status = status; + } + + /** + * Returns the request status. One of {{@link android.bluetooth.BluetoothGatt}} GATT_* + * or {@link no.nordicsemi.android.ble.callback.FailCallback} REASON_* codes. + * + * @return Error code. + */ + public int getStatus() { + return status; + } + + /** + * Returns the request that failed. + * @return The request that failed. + */ + public Request getRequest() { + return request; + } +} diff --git a/_android/src/no/nordicsemi/android/ble/response/ConnectionPriorityResponse.java b/_android/src/no/nordicsemi/android/ble/response/ConnectionPriorityResponse.java new file mode 100644 index 0000000..bdabfda --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/response/ConnectionPriorityResponse.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.response; + +import android.bluetooth.BluetoothDevice; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.ConnectionPriorityCallback; + +/** + * The synchronous response type for connection priority requests. + * + * @see ConnectionPriorityCallback + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class ConnectionPriorityResponse implements ConnectionPriorityCallback, Parcelable { + private BluetoothDevice device; + + @IntRange(from = 6, to = 3200) + private int interval; + + @IntRange(from = 0, to = 499) + private int latency; + + @IntRange(from = 10, to = 3200) + private int supervisionTimeout; + + @Override + public void onConnectionUpdated(@NonNull final BluetoothDevice device, + @IntRange(from = 6, to = 3200) final int interval, + @IntRange(from = 0, to = 499) final int latency, + @IntRange(from = 10, to = 3200) final int timeout) { + this.device = device; + this.interval = interval; + this.latency = latency; + this.supervisionTimeout = timeout; + } + + @Nullable + public BluetoothDevice getBluetoothDevice() { + return device; + } + + /** + * The connection interval determines how often the Central will ask for data from the Peripheral. + * When the Peripheral requests an update, it supplies a maximum and a minimum wanted interval. + * The connection interval must be between 7.5 ms and 4 s. + * + * @return Connection interval used on this connection, 1.25ms unit. + * Valid range is from 6 (7.5ms) to 3200 (4000ms). + */ + @IntRange(from = 6, to = 3200) + public int getConnectionInterval() { + return interval; + } + + /** + * By setting a non-zero slave latency, the Peripheral can choose to not answer when + * the Central asks for data up to the slave latency number of times. + * However, if the Peripheral has data to send, it can choose to send data at any time. + * This enables a peripheral to stay sleeping for a longer time, if it doesn't have data to send, + * but still send data fast if needed. The text book example of such device is for example + * keyboard and mice, which want to be sleeping for as long as possible when there is + * no data to send, but still have low latency (and for the mouse: low connection interval) + * when needed. + * + * @return Slave latency for the connection in number of connection events. + * Valid range is from 0 to 499. + */ + @IntRange(from = 0, to = 499) + public int getSlaveLatency() { + return latency; + } + + /** + * This timeout determines the timeout from the last data exchange till a link is considered lost. + * A Central will not start trying to reconnect before the timeout has passed, + * so if you have a device which goes in and out of range often, and you need to notice when + * that happens, it might make sense to have a short timeout. + * + * @return Supervision timeout for this connection, in 10ms unit. + * Valid range is from 10 (100 ms = 0.1s) to 3200 (32s). + */ + @IntRange(from = 10, to = 3200) + public int getSupervisionTimeout() { + return supervisionTimeout; + } + + // Parcelable + protected ConnectionPriorityResponse(final Parcel in) { + device = in.readParcelable(BluetoothDevice.class.getClassLoader()); + interval = in.readInt(); + latency = in.readInt(); + supervisionTimeout = in.readInt(); + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeParcelable(device, flags); + dest.writeInt(interval); + dest.writeInt(latency); + dest.writeInt(supervisionTimeout); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ConnectionPriorityResponse createFromParcel(final Parcel in) { + return new ConnectionPriorityResponse(in); + } + + @Override + public ConnectionPriorityResponse[] newArray(final int size) { + return new ConnectionPriorityResponse[size]; + } + }; +} diff --git a/_android/src/no/nordicsemi/android/ble/response/MtuResult.java b/_android/src/no/nordicsemi/android/ble/response/MtuResult.java new file mode 100644 index 0000000..50db621 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/response/MtuResult.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.response; + +import android.bluetooth.BluetoothDevice; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.MtuCallback; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class MtuResult implements MtuCallback, Parcelable { + private BluetoothDevice device; + + @IntRange(from = 23, to = 517) + private int mtu; + + @Override + public void onMtuChanged(@NonNull final BluetoothDevice device, + @IntRange(from = 23, to = 517) final int mtu) { + this.device = device; + this.mtu = mtu; + } + + @Nullable + public BluetoothDevice getBluetoothDevice() { + return device; + } + + /** + * Returns the agreed MTU. The maximum packet size is 3 bytes less then MTU. + * + * @return The MTU. + */ + @IntRange(from = 23, to = 517) + public int getMtu() { + return mtu; + } + + // Parcelable + protected MtuResult(final Parcel in) { + device = in.readParcelable(BluetoothDevice.class.getClassLoader()); + mtu = in.readInt(); + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeParcelable(device, flags); + dest.writeInt(mtu); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public MtuResult createFromParcel(final Parcel in) { + return new MtuResult(in); + } + + @Override + public MtuResult[] newArray(final int size) { + return new MtuResult[size]; + } + }; +} diff --git a/_android/src/no/nordicsemi/android/ble/response/PhyResult.java b/_android/src/no/nordicsemi/android/ble/response/PhyResult.java new file mode 100644 index 0000000..9eccd16 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/response/PhyResult.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.response; + +import android.bluetooth.BluetoothDevice; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.annotation.PhyValue; +import no.nordicsemi.android.ble.callback.PhyCallback; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class PhyResult implements PhyCallback, Parcelable { + private BluetoothDevice device; + + @PhyValue + private int txPhy; + + @PhyValue + private int rxPhy; + + @Override + public void onPhyChanged(@NonNull final BluetoothDevice device, + @PhyValue final int txPhy, @PhyValue final int rxPhy) { + this.device = device; + this.txPhy = txPhy; + this.rxPhy = rxPhy; + } + + @Nullable + public BluetoothDevice getBluetoothDevice() { + return device; + } + + @PhyValue + public int getTxPhy() { + return txPhy; + } + + @PhyValue + public int getRxPhy() { + return rxPhy; + } + + // Parcelable + protected PhyResult(final Parcel in) { + device = in.readParcelable(BluetoothDevice.class.getClassLoader()); + txPhy = in.readInt(); + rxPhy = in.readInt(); + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeParcelable(device, flags); + dest.writeInt(txPhy); + dest.writeInt(rxPhy); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public PhyResult createFromParcel(final Parcel in) { + return new PhyResult(in); + } + + @Override + public PhyResult[] newArray(final int size) { + return new PhyResult[size]; + } + }; +} diff --git a/_android/src/no/nordicsemi/android/ble/response/ReadResponse.java b/_android/src/no/nordicsemi/android/ble/response/ReadResponse.java new file mode 100644 index 0000000..8daa110 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/response/ReadResponse.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.response; + +import android.bluetooth.BluetoothDevice; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.DataReceivedCallback; +import no.nordicsemi.android.ble.data.Data; + +/** + * Generic read response class that returns the data received and the device from which data + * were read. + * Overriding class must call super on {@link #onDataReceived(BluetoothDevice, Data)} in + * order to make getters work properly. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class ReadResponse implements DataReceivedCallback, Parcelable { + private BluetoothDevice device; + private Data data; + + public ReadResponse() { + // empty + } + + @Override + public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { + this.device = device; + this.data = data; + } + + @Nullable + public BluetoothDevice getBluetoothDevice() { + return device; + } + + @Nullable + public Data getRawData() { + return data; + } + + // Parcelable + protected ReadResponse(final Parcel in) { + device = in.readParcelable(BluetoothDevice.class.getClassLoader()); + data = in.readParcelable(Data.class.getClassLoader()); + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeParcelable(device, flags); + dest.writeParcelable(data, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ReadResponse createFromParcel(final Parcel in) { + return new ReadResponse(in); + } + + @Override + public ReadResponse[] newArray(final int size) { + return new ReadResponse[size]; + } + }; +} diff --git a/_android/src/no/nordicsemi/android/ble/response/RssiResult.java b/_android/src/no/nordicsemi/android/ble/response/RssiResult.java new file mode 100644 index 0000000..2fd34dc --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/response/RssiResult.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.response; + +import android.bluetooth.BluetoothDevice; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.RssiCallback; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class RssiResult implements RssiCallback, Parcelable { + private BluetoothDevice device; + + @IntRange(from = -128, to = 20) + private int rssi; + + @Override + public void onRssiRead(@NonNull final BluetoothDevice device, + @IntRange(from = -128, to = 20) final int rssi) { + this.device = device; + this.rssi = rssi; + } + + @Nullable + public BluetoothDevice getBluetoothDevice() { + return device; + } + + @IntRange(from = -128, to = 20) + public int getRssi() { + return rssi; + } + + // Parcelable + protected RssiResult(final Parcel in) { + device = in.readParcelable(BluetoothDevice.class.getClassLoader()); + rssi = in.readInt(); + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeParcelable(device, flags); + dest.writeInt(rssi); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public RssiResult createFromParcel(final Parcel in) { + return new RssiResult(in); + } + + @Override + public RssiResult[] newArray(final int size) { + return new RssiResult[size]; + } + }; +} diff --git a/_android/src/no/nordicsemi/android/ble/response/WriteResponse.java b/_android/src/no/nordicsemi/android/ble/response/WriteResponse.java new file mode 100644 index 0000000..7e12973 --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/response/WriteResponse.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.ble.response; + +import android.bluetooth.BluetoothDevice; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import no.nordicsemi.android.ble.callback.DataSentCallback; +import no.nordicsemi.android.ble.data.Data; + +/** + * Generic write response class that returns the target device and the data that were sent. + * Overriding class must call super on {@link #onDataSent(BluetoothDevice, Data)} in + * order to make getters work properly. + */ +@SuppressWarnings({"unused", "WeakerAccess"}) +public class WriteResponse implements DataSentCallback, Parcelable { + private BluetoothDevice device; + private Data data; + + @Override + public void onDataSent(@NonNull final BluetoothDevice device, @NonNull final Data data) { + this.device = device; + this.data = data; + } + + @Nullable + public BluetoothDevice getBluetoothDevice() { + return device; + } + + @Nullable + public Data getRawData() { + return data; + } + + // Parcelable + protected WriteResponse(final Parcel in) { + device = in.readParcelable(BluetoothDevice.class.getClassLoader()); + data = in.readParcelable(Data.class.getClassLoader()); + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + dest.writeParcelable(device, flags); + dest.writeParcelable(data, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public WriteResponse createFromParcel(final Parcel in) { + return new WriteResponse(in); + } + + @Override + public WriteResponse[] newArray(final int size) { + return new WriteResponse[size]; + } + }; +} diff --git a/_android/src/no/nordicsemi/android/ble/utils/ILogger.java b/_android/src/no/nordicsemi/android/ble/utils/ILogger.java new file mode 100644 index 0000000..c94f01c --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/utils/ILogger.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.ble.utils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +@SuppressWarnings("unused") +public interface ILogger { + + /** + * Logs the given message with given log priority into the all managed devices' log session. + * + * @param priority the log priority. + * @param message the message to be logged. + */ + void log(final int priority, @NonNull final String message); + + /** + * Logs the given message with given log priority into the all managed devices' log session. + * + * @param priority the log priority. + * @param messageRes string resource id. + * @param params additional (optional) parameters used to fill the message. + */ + void log(final int priority, @StringRes final int messageRes, @Nullable final Object... params); +} diff --git a/_android/src/no/nordicsemi/android/ble/utils/ParserUtils.java b/_android/src/no/nordicsemi/android/ble/utils/ParserUtils.java new file mode 100644 index 0000000..385b26a --- /dev/null +++ b/_android/src/no/nordicsemi/android/ble/utils/ParserUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2018, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package no.nordicsemi.android.ble.utils; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +@SuppressWarnings({"WeakerAccess", "unused"}) +public class ParserUtils { + protected final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + + public static String parse(final BluetoothGattCharacteristic characteristic) { + return parse(characteristic.getValue()); + } + + public static String parse(final BluetoothGattDescriptor descriptor) { + return parse(descriptor.getValue()); + } + + public static String parse(final byte[] data) { + if (data == null || data.length == 0) + return ""; + + final char[] out = new char[data.length * 3 - 1]; + for (int j = 0; j < data.length; j++) { + int v = data[j] & 0xFF; + out[j * 3] = HEX_ARRAY[v >>> 4]; + out[j * 3 + 1] = HEX_ARRAY[v & 0x0F]; + if (j != data.length - 1) + out[j * 3 + 2] = '-'; + } + return "(0x) " + new String(out); + } +} diff --git a/main.qml b/main.qml index 6618ebe..b656d3c 100644 --- a/main.qml +++ b/main.qml @@ -1,6 +1,7 @@ import QtQuick 2.9 import QtQuick.Window 2.2 import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.3 Window { id: window @@ -13,50 +14,87 @@ Window { Column { id: column - Label { - text: "dist1: " + ((mgmt.dist1 ? mgmt.dist1+sld1.value : 0)/1000).toFixed(2); - } + Label { + text: "FTM dist1: " + ((mgmt.dist1 ? mgmt.dist1+sld1.value : 0)/1000).toFixed(2); + } - Label { - text: "dist2: " + ((mgmt.dist2 ? mgmt.dist2+sld1.value : 0)/1000).toFixed(2); - } + Label { + text: "FTM dist2: " + ((mgmt.dist2 ? mgmt.dist2+sld1.value : 0)/1000).toFixed(2); + } - Label { - text: "dist3: " + ((mgmt.dist3 ? mgmt.dist3+sld1.value : 0)/1000).toFixed(2); - } + Label { + text: "FTM dist3: " + ((mgmt.dist3 ? mgmt.dist3+sld1.value : 0)/1000).toFixed(2); + } + + Label { + text: "FTM dist4: " + ((mgmt.dist4 ? mgmt.dist4+sld1.value : 0)/1000).toFixed(2); + } - Label { - text: "dist4: " + ((mgmt.dist4 ? mgmt.dist4+sld1.value : 0)/1000).toFixed(2); - } Slider { width: 400; id: sld1; - from: -3000; - to: 5000; - onValueChanged: leCanvas.requestPaint(); + from: -1000; + to: 1000; + value: mgmt.offset; + onValueChanged: { + mgmt.offset = value; + leCanvas.requestPaint(); + } } Label { text: "offset: " + sld1.value; } + Label { + text: "UWB dist1: " + mgmt.uwbDist1.toFixed(2); + } + + Label { + text: "UWB dist2: " + mgmt.uwbDist2.toFixed(2); + } + + Label { + text: "UWB dist3: " + mgmt.uwbDist3.toFixed(2); + } + + Label { + text: "UWB dist4: " + mgmt.uwbDist4.toFixed(2); + } + + Connections { target: mgmt onDistChanged: leCanvas.requestPaint(); } - Button { - text: "party hard"; - onClicked: mgmt.trigger(); - } + RowLayout { + Button { + text: "party hard"; + onClicked: mgmt.trigger(); + } - Button { - text: "stop" - onClicked: mgmt.stop(); + Button { + text: "stop" + onClicked: mgmt.stop(); + } + Button { + text: "reset sld" + onClicked: { + sld1.value = 0; + } + } + Button { + text: "High Five" + onClicked: { + mgmt.manualCheckpoint(); + } + } } + } Canvas { @@ -74,14 +112,17 @@ Window { readonly property double s: 0.02; - function leArc(ctx, cx, cy, dist, stdDev) { + function leArc(ctx, cx, cy, dist, stdDev, caption) { // center circle - ctx.fillStyle = "#000000"; + ctx.fillStyle = (dist < 0 ? "#FF0000" : "#000000"); ctx.beginPath(); ctx.arc(cx*s, cy*s, 2, 0, 360); ctx.fill(); + ctx.font = "12px Arial"; + ctx.fillText(caption, cx*s, cy*s); + // error circle // ctx.beginPath(); // ctx.arc(cx*s, cy*s, (dist+sld1.value)*s, 0, 360); @@ -91,12 +132,13 @@ Window { // ctx.stroke(); // circle - ctx.lineWidth = 1; - ctx.strokeStyle = "#aa000000"; - ctx.beginPath(); - ctx.arc(cx*s, cy*s, (dist+sld1.value)*s, 0, 360); - ctx.stroke(); - + if (dist > 0) { + ctx.lineWidth = 1; + ctx.strokeStyle = "#aa000000"; + ctx.beginPath(); + ctx.arc(cx*s, cy*s, dist*s, 0, 360); + ctx.stroke(); + } } onPaint: { @@ -110,53 +152,68 @@ Window { //ctx.strokeStyle = Qt.rgba(0, 0, 0, 1) //ctx.lineWidth = 1 - var ox = 4000; - var oy = 4000; +// var ox = 4000; +// var oy = 4000; - var cx1 = ox+0; - var cy1 = oy+0; +// var cx1 = ox+0; +// var cy1 = oy+0; - var cx2 = ox+0; - var cy2 = oy+7250; +// var cx2 = ox+0; +// var cy2 = oy+7250; - var cx3 = ox+9000; - var cy3 = oy+7250; +// var cx3 = ox+9000; +// var cy3 = oy+7250; - var cx4 = ox+9000; - var cy4 = oy+0; +// var cx4 = ox+9000; +// var cy4 = oy+0; - leArc(ctx, cx1, cy1, mgmt.dist1, mgmt.stdDev1); - leArc(ctx, cx2, cy2, mgmt.dist2, mgmt.stdDev2); - leArc(ctx, cx3, cy3, mgmt.dist3, mgmt.stdDev3); - leArc(ctx, cx4, cy4, mgmt.dist4, mgmt.stdDev4); + var ox = 4000; + var oy = 4000; + + var cx1 = ox+0; + var cy1 = oy+0; + + var cx2 = ox+9000; + var cy2 = oy+0; + + var cx3 = ox+9000; + var cy3 = oy+6200; + + var cx4 = ox+0; + var cy4 = oy+6200; + + leArc(ctx, cx1, cy1, mgmt.dist1, mgmt.stdDev1, 1); + leArc(ctx, cx2, cy2, mgmt.dist2, mgmt.stdDev2, 2); + leArc(ctx, cx3, cy3, mgmt.dist3, mgmt.stdDev3, 3); + leArc(ctx, cx4, cy4, mgmt.dist4, mgmt.stdDev4, 4); var sigma = 2000; var stepSize = 333; var maxP = Math.pow( 1.0 / Math.sqrt(2*Math.PI*sigma), 4.1); - for (var y = 0; y < leCanvas.height/s; y += stepSize) { - for (var x = 0; x < leCanvas.width/s; x += stepSize) { +// for (var y = 0; y < leCanvas.height/s; y += stepSize) { +// for (var x = 0; x < leCanvas.width/s; x += stepSize) { - var d1 = Math.sqrt( Math.pow(x-cx1, 2) + Math.pow(y-cy1, 2) ) - (mgmt.dist1-sld1.value); - var d2 = Math.sqrt( Math.pow(x-cx2, 2) + Math.pow(y-cy2, 2) ) - (mgmt.dist2-sld1.value); - var d3 = Math.sqrt( Math.pow(x-cx3, 2) + Math.pow(y-cy3, 2) ) - (mgmt.dist3-sld1.value); - var d4 = Math.sqrt( Math.pow(x-cx4, 2) + Math.pow(y-cy4, 2) ) - (mgmt.dist4-sld1.value); +// var d1 = Math.sqrt( Math.pow(x-cx1, 2) + Math.pow(y-cy1, 2) ) - (mgmt.dist1-sld1.value); +// var d2 = Math.sqrt( Math.pow(x-cx2, 2) + Math.pow(y-cy2, 2) ) - (mgmt.dist2-sld1.value); +// var d3 = Math.sqrt( Math.pow(x-cx3, 2) + Math.pow(y-cy3, 2) ) - (mgmt.dist3-sld1.value); +// var d4 = Math.sqrt( Math.pow(x-cx4, 2) + Math.pow(y-cy4, 2) ) - (mgmt.dist4-sld1.value); - var p = 1; - p *= 1.0 / Math.sqrt(2*Math.PI*sigma) * Math.exp( - d1*d1/(2*sigma*sigma) ); - p *= 1.0 / Math.sqrt(2*Math.PI*sigma) * Math.exp( - d2*d2/(2*sigma*sigma) ); - p *= 1.0 / Math.sqrt(2*Math.PI*sigma) * Math.exp( - d3*d3/(2*sigma*sigma) ); - p *= 1.0 / Math.sqrt(2*Math.PI*sigma) * Math.exp( - d4*d4/(2*sigma*sigma) ); +// var p = 1; +// p *= 1.0 / Math.sqrt(2*Math.PI*sigma) * Math.exp( - d1*d1/(2*sigma*sigma) ); +// p *= 1.0 / Math.sqrt(2*Math.PI*sigma) * Math.exp( - d2*d2/(2*sigma*sigma) ); +// p *= 1.0 / Math.sqrt(2*Math.PI*sigma) * Math.exp( - d3*d3/(2*sigma*sigma) ); +// p *= 1.0 / Math.sqrt(2*Math.PI*sigma) * Math.exp( - d4*d4/(2*sigma*sigma) ); - //p = Math.pow(p, 1); +// //p = Math.pow(p, 1); - if (p > maxP / 50) { - ctx.fillStyle = Qt.rgba(1,0,0, p/maxP); - ctx.fillRect(x*s, y*s, stepSize*s, stepSize*s); - } - } - } +// if (p > maxP / 50) { +// ctx.fillStyle = Qt.rgba(1,0,0, p/maxP); +// ctx.fillRect(x*s, y*s, stepSize*s, stepSize*s); +// } +// } +// } } } } diff --git a/uwb.h b/uwb.h new file mode 100644 index 0000000..eab1c04 --- /dev/null +++ b/uwb.h @@ -0,0 +1,99 @@ +#ifndef UWB_H +#define UWB_H + +#include +#include + +struct UwbPosition { + int x = 0; + int y = 0; + int z = 0; + uint8_t quali = 0; +}; + +struct UwbDistance { + uint16_t nodeID = 0; + int distance = 0; + uint8_t quali = 0; +}; + +struct UwbResult { + UwbPosition pos; + std::vector distances; +}; + + +static void uwb_parse_pos(const unsigned char input[], UwbPosition& pos) +{ + // X,Y,Z coordinates (each 4 bytes) and quality factor (1 byte), total size: 13 bytes + + pos.x = input[0]; + pos.x |= input[1] << 8; + pos.x |= input[2] << 16; + pos.x |= input[3] << 24; + + pos.y = input[4]; + pos.y |= input[5] << 8; + pos.y |= input[6] << 16; + pos.y |= input[7] << 24; + + pos.z = input[8]; + pos.z |= input[9] << 8; + pos.z |= input[10] << 16; + pos.z |= input[11] << 24; + + pos.quali = input[12]; +} + +static void uwb_parse_dist(const unsigned char input[], std::vector& result) +{ + // First byte is distance count(1 byte) + // Sequence of node ID(2 bytes), distance in mm(4 bytes) and quality factor(1 byte) + // Max value contains 15 elements, size: 8 - 106 + + size_t numOfAnchors = input[0]; + + for (size_t i = 0; i < numOfAnchors; i++) + { + size_t offset = 1 + i * 7; + + UwbDistance dist; + + dist.nodeID = input[offset + 1] << 8 | input[offset]; + dist.distance = input[offset + 5] << 24 + | input[offset + 4] << 16 + | input[offset + 3] << 8 + | input[offset + 2]; + dist.quali = input[offset + 6]; + + result.push_back(dist); + } +} + +static UwbResult uwb_parse(const unsigned char input[]) +{ + UwbResult result; + + if (input[0] == 0) + { + // pos only + uwb_parse_pos(&input[1], result.pos); + } + else if (input[0] == 1) + { + // dist only + uwb_parse_dist(&input[1], result.distances); + } + else if (input[0] == 2) + { + // Encoded Position (as above, 13 bytes) + // Encoded Distances(as above, 8 - 29 bytes).Position and distances + // are sent by tag, with a maximum number of 4 ranging anchors + uwb_parse_pos(&input[1], result.pos); + uwb_parse_dist(&input[1 + 13], result.distances); + } + + return result; +} + +#endif // UWB_H