From 2138c42ee0f496b813b4fe60caf80d01468c9244 Mon Sep 17 00:00:00 2001 From: Markus Bullmann Date: Tue, 2 Apr 2019 10:41:57 +0200 Subject: [PATCH] Directly include BLE lib as java files --- .idea/vcs.xml | 6 + app/build.gradle | 1 - .../com/example/nrftest/MainActivity.java | 2 +- .../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 + app/src/main/res/layout/activity_main.xml | 4 +- gradle.properties | 3 + 70 files changed, 11154 insertions(+), 4 deletions(-) create mode 100644 .idea/vcs.xml create mode 100644 app/src/main/java/no/nordicsemi/android/ble/BleManager.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/BleManagerCallbacks.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/ConnectRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/ConnectionPriorityRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/DisconnectRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/MainThreadBluetoothGattCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/MtuRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/Operation.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/PhyRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/ReadRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/ReadRssiRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/ReliableWriteRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/Request.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/RequestQueue.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/SimpleRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/SimpleValueRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/SleepRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/TimeoutHandler.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/TimeoutableRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/TimeoutableValueRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/ValueChangedCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/WaitForValueChangedRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/WriteRequest.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/annotation/ConnectionPriority.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/annotation/ConnectionState.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/annotation/PhyMask.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/annotation/PhyOption.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/annotation/PhyValue.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/annotation/WriteType.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/BeforeCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/ConnectionPriorityCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/DataReceivedCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/DataSentCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/FailCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/InvalidRequestCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/MtuCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/PhyCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/ReadProgressCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/RssiCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/SuccessCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/WriteProgressCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/profile/ProfileDataCallback.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/callback/profile/ProfileReadResponse.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/data/Data.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/data/DataFilter.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/data/DataMerger.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/data/DataSplitter.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/data/DataStream.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/data/DefaultMtuSplitter.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/data/MutableData.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/error/GattError.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/exception/BluetoothDisabledException.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/exception/ConnectionException.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/exception/DeviceDisconnectedException.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/exception/InvalidDataException.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/exception/InvalidRequestException.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/exception/RequestFailedException.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/response/ConnectionPriorityResponse.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/response/MtuResult.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/response/PhyResult.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/response/ReadResponse.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/response/RssiResult.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/response/WriteResponse.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/utils/ILogger.java create mode 100644 app/src/main/java/no/nordicsemi/android/ble/utils/ParserUtils.java diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 39b92ed..2c49ef0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,5 +29,4 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - implementation 'no.nordicsemi.android:ble:2.1.1' } diff --git a/app/src/main/java/com/example/nrftest/MainActivity.java b/app/src/main/java/com/example/nrftest/MainActivity.java index b1682f7..98a39de 100644 --- a/app/src/main/java/com/example/nrftest/MainActivity.java +++ b/app/src/main/java/com/example/nrftest/MainActivity.java @@ -9,11 +9,11 @@ import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.content.Context; import android.content.Intent; -import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; import java.util.List; diff --git a/app/src/main/java/no/nordicsemi/android/ble/BleManager.java b/app/src/main/java/no/nordicsemi/android/ble/BleManager.java new file mode 100644 index 0000000..01c2ccd --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/BleManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/ble/BleManagerCallbacks.java new file mode 100644 index 0000000..e98035a --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/ConnectRequest.java b/app/src/main/java/no/nordicsemi/android/ble/ConnectRequest.java new file mode 100644 index 0000000..567acae --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/ConnectionPriorityRequest.java b/app/src/main/java/no/nordicsemi/android/ble/ConnectionPriorityRequest.java new file mode 100644 index 0000000..9dabbd0 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/DisconnectRequest.java b/app/src/main/java/no/nordicsemi/android/ble/DisconnectRequest.java new file mode 100644 index 0000000..5e57914 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/MainThreadBluetoothGattCallback.java b/app/src/main/java/no/nordicsemi/android/ble/MainThreadBluetoothGattCallback.java new file mode 100644 index 0000000..be589e7 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/MtuRequest.java b/app/src/main/java/no/nordicsemi/android/ble/MtuRequest.java new file mode 100644 index 0000000..f67ccdd --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/Operation.java b/app/src/main/java/no/nordicsemi/android/ble/Operation.java new file mode 100644 index 0000000..a701401 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/PhyRequest.java b/app/src/main/java/no/nordicsemi/android/ble/PhyRequest.java new file mode 100644 index 0000000..0a9e8c1 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/ReadRequest.java b/app/src/main/java/no/nordicsemi/android/ble/ReadRequest.java new file mode 100644 index 0000000..476ab12 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/ReadRssiRequest.java b/app/src/main/java/no/nordicsemi/android/ble/ReadRssiRequest.java new file mode 100644 index 0000000..a189e93 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/ReliableWriteRequest.java b/app/src/main/java/no/nordicsemi/android/ble/ReliableWriteRequest.java new file mode 100644 index 0000000..e2bc384 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/Request.java b/app/src/main/java/no/nordicsemi/android/ble/Request.java new file mode 100644 index 0000000..38a3118 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/RequestQueue.java b/app/src/main/java/no/nordicsemi/android/ble/RequestQueue.java new file mode 100644 index 0000000..6d97ab2 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/SimpleRequest.java b/app/src/main/java/no/nordicsemi/android/ble/SimpleRequest.java new file mode 100644 index 0000000..23359d7 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/SimpleValueRequest.java b/app/src/main/java/no/nordicsemi/android/ble/SimpleValueRequest.java new file mode 100644 index 0000000..7115a67 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/SleepRequest.java b/app/src/main/java/no/nordicsemi/android/ble/SleepRequest.java new file mode 100644 index 0000000..ab10d4e --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/TimeoutHandler.java b/app/src/main/java/no/nordicsemi/android/ble/TimeoutHandler.java new file mode 100644 index 0000000..54b4aa4 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/TimeoutableRequest.java b/app/src/main/java/no/nordicsemi/android/ble/TimeoutableRequest.java new file mode 100644 index 0000000..9e69a14 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/TimeoutableValueRequest.java b/app/src/main/java/no/nordicsemi/android/ble/TimeoutableValueRequest.java new file mode 100644 index 0000000..1ffcf47 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/ValueChangedCallback.java b/app/src/main/java/no/nordicsemi/android/ble/ValueChangedCallback.java new file mode 100644 index 0000000..7ef2610 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/WaitForValueChangedRequest.java b/app/src/main/java/no/nordicsemi/android/ble/WaitForValueChangedRequest.java new file mode 100644 index 0000000..7b695f8 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/WriteRequest.java b/app/src/main/java/no/nordicsemi/android/ble/WriteRequest.java new file mode 100644 index 0000000..6944283 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/annotation/ConnectionPriority.java b/app/src/main/java/no/nordicsemi/android/ble/annotation/ConnectionPriority.java new file mode 100644 index 0000000..313bf6d --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/annotation/ConnectionState.java b/app/src/main/java/no/nordicsemi/android/ble/annotation/ConnectionState.java new file mode 100644 index 0000000..ec19d2a --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/annotation/PhyMask.java b/app/src/main/java/no/nordicsemi/android/ble/annotation/PhyMask.java new file mode 100644 index 0000000..cd7efd7 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/annotation/PhyOption.java b/app/src/main/java/no/nordicsemi/android/ble/annotation/PhyOption.java new file mode 100644 index 0000000..84fb5cf --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/annotation/PhyValue.java b/app/src/main/java/no/nordicsemi/android/ble/annotation/PhyValue.java new file mode 100644 index 0000000..6b1623f --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/annotation/WriteType.java b/app/src/main/java/no/nordicsemi/android/ble/annotation/WriteType.java new file mode 100644 index 0000000..03bbce1 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/BeforeCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/BeforeCallback.java new file mode 100644 index 0000000..3eb287f --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/ConnectionPriorityCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/ConnectionPriorityCallback.java new file mode 100644 index 0000000..3e6ebed --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/DataReceivedCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/DataReceivedCallback.java new file mode 100644 index 0000000..668a61c --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/DataSentCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/DataSentCallback.java new file mode 100644 index 0000000..572b62a --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/FailCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/FailCallback.java new file mode 100644 index 0000000..4af98bb --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/InvalidRequestCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/InvalidRequestCallback.java new file mode 100644 index 0000000..cb48051 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/MtuCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/MtuCallback.java new file mode 100644 index 0000000..7879ce8 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/PhyCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/PhyCallback.java new file mode 100644 index 0000000..8dc13d6 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/ReadProgressCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/ReadProgressCallback.java new file mode 100644 index 0000000..61d0e88 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/RssiCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/RssiCallback.java new file mode 100644 index 0000000..9ca6873 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/SuccessCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/SuccessCallback.java new file mode 100644 index 0000000..99367ad --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/WriteProgressCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/WriteProgressCallback.java new file mode 100644 index 0000000..d62752f --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/profile/ProfileDataCallback.java b/app/src/main/java/no/nordicsemi/android/ble/callback/profile/ProfileDataCallback.java new file mode 100644 index 0000000..a47c770 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/callback/profile/ProfileReadResponse.java b/app/src/main/java/no/nordicsemi/android/ble/callback/profile/ProfileReadResponse.java new file mode 100644 index 0000000..b26e025 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/data/Data.java b/app/src/main/java/no/nordicsemi/android/ble/data/Data.java new file mode 100644 index 0000000..e22dc4e --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/data/DataFilter.java b/app/src/main/java/no/nordicsemi/android/ble/data/DataFilter.java new file mode 100644 index 0000000..321da2f --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/data/DataMerger.java b/app/src/main/java/no/nordicsemi/android/ble/data/DataMerger.java new file mode 100644 index 0000000..2aa81f0 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/data/DataSplitter.java b/app/src/main/java/no/nordicsemi/android/ble/data/DataSplitter.java new file mode 100644 index 0000000..1cb1e9d --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/data/DataStream.java b/app/src/main/java/no/nordicsemi/android/ble/data/DataStream.java new file mode 100644 index 0000000..4aba77e --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/data/DefaultMtuSplitter.java b/app/src/main/java/no/nordicsemi/android/ble/data/DefaultMtuSplitter.java new file mode 100644 index 0000000..5c15484 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/data/MutableData.java b/app/src/main/java/no/nordicsemi/android/ble/data/MutableData.java new file mode 100644 index 0000000..acfe914 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/error/GattError.java b/app/src/main/java/no/nordicsemi/android/ble/error/GattError.java new file mode 100644 index 0000000..39dc53c --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/exception/BluetoothDisabledException.java b/app/src/main/java/no/nordicsemi/android/ble/exception/BluetoothDisabledException.java new file mode 100644 index 0000000..7c07421 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/exception/ConnectionException.java b/app/src/main/java/no/nordicsemi/android/ble/exception/ConnectionException.java new file mode 100644 index 0000000..40794d7 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/exception/DeviceDisconnectedException.java b/app/src/main/java/no/nordicsemi/android/ble/exception/DeviceDisconnectedException.java new file mode 100644 index 0000000..ccca470 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/exception/InvalidDataException.java b/app/src/main/java/no/nordicsemi/android/ble/exception/InvalidDataException.java new file mode 100644 index 0000000..3ae6a05 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/exception/InvalidRequestException.java b/app/src/main/java/no/nordicsemi/android/ble/exception/InvalidRequestException.java new file mode 100644 index 0000000..3ab4d09 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/exception/RequestFailedException.java b/app/src/main/java/no/nordicsemi/android/ble/exception/RequestFailedException.java new file mode 100644 index 0000000..677798a --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/response/ConnectionPriorityResponse.java b/app/src/main/java/no/nordicsemi/android/ble/response/ConnectionPriorityResponse.java new file mode 100644 index 0000000..bdabfda --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/response/MtuResult.java b/app/src/main/java/no/nordicsemi/android/ble/response/MtuResult.java new file mode 100644 index 0000000..50db621 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/response/PhyResult.java b/app/src/main/java/no/nordicsemi/android/ble/response/PhyResult.java new file mode 100644 index 0000000..9eccd16 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/response/ReadResponse.java b/app/src/main/java/no/nordicsemi/android/ble/response/ReadResponse.java new file mode 100644 index 0000000..8daa110 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/response/RssiResult.java b/app/src/main/java/no/nordicsemi/android/ble/response/RssiResult.java new file mode 100644 index 0000000..2fd34dc --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/response/WriteResponse.java b/app/src/main/java/no/nordicsemi/android/ble/response/WriteResponse.java new file mode 100644 index 0000000..7e12973 --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/utils/ILogger.java b/app/src/main/java/no/nordicsemi/android/ble/utils/ILogger.java new file mode 100644 index 0000000..c94f01c --- /dev/null +++ b/app/src/main/java/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/app/src/main/java/no/nordicsemi/android/ble/utils/ParserUtils.java b/app/src/main/java/no/nordicsemi/android/ble/utils/ParserUtils.java new file mode 100644 index 0000000..385b26a --- /dev/null +++ b/app/src/main/java/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/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f5d42d7..61f528f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 82618ce..b3908aa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,3 +13,6 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true + +android.enableJetifier=true +android.useAndroidX=true \ No newline at end of file