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.BluetoothGattServer;
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.SystemClock;
import android.util.Log;
import android.util.Pair;

import java.lang.reflect.Method;
import java.security.InvalidParameterException;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingDeque;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
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.*;
import no.nordicsemi.android.ble.data.Data;
import no.nordicsemi.android.ble.error.GattError;
import no.nordicsemi.android.ble.observer.BondingObserver;
import no.nordicsemi.android.ble.observer.ConnectionObserver;
import no.nordicsemi.android.ble.utils.ParserUtils;

@SuppressWarnings({"WeakerAccess", "DeprecatedIsStillUsed", "unused", "deprecation"})
abstract class BleManagerHandler extends RequestHandler {
	private final static String TAG = "BleManager";

	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 static String ERROR_NOTIFY = "Error on sending notification/indication";

	private final Object LOCK = new Object();
	private BluetoothDevice bluetoothDevice;
	private BluetoothGatt bluetoothGatt;
	private BleManager manager;
	private BleServerManager serverManager;
	private Handler handler;

	private final Deque<Request> taskQueue = new LinkedBlockingDeque<>();
	private Deque<Request> initQueue;
	private boolean initInProgress;

	/**
	 * 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 when services were discovered.
	 */
	private boolean servicesDiscovered;
	/**
	 * Flag set to true when the {@link #isRequiredServiceSupported(BluetoothGatt)} returned false.
	 */
	private boolean deviceNotSupported;
	/**
	 * Flag set when service discovery was requested.
	 */
	private boolean serviceDiscoveryRequested;
	/**
	 * 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 connectionTime;
	/**
	 * A temporary counter to prevent requesting service discovery for old connection.
	 */
	private int connectionCount = 0;
	/**
	 * Flag set to true when the device is connected.
	 */
	private boolean connected;
	/**
	 * Flag set to true when the initialization queue is complete.
	 */
	private boolean ready;
	/**
	 * A flag indicating that an operation is currently in progress.
	 */
	private boolean operationInProgress;
	/**
	 * This flag is set to false only when the {@link ConnectRequest#shouldAutoConnect()} method
	 * returns true and the device got disconnected without calling {@link BleManager#disconnect()}
	 * method. If {@link ConnectRequest#shouldAutoConnect()} returns false (default) this is always
	 * set to true.
	 */
	private boolean userDisconnected;
	/**
	 * Flag set to true when {@link ConnectRequest#shouldAutoConnect()} method returned true.
	 * The first connection attempt is done with <code>autoConnect</code> flag set to false
	 * (to make the first connection quick) but on connection lost the manager will call
	 * {@link BleManager#connect(BluetoothDevice)} again. This time this method will call
	 * {@link BluetoothGatt#connect()} which always uses <code>autoConnect</code> equal true.
	 */
	private boolean initialConnection;
	/**
	 * The connection state. One of:
	 * {@link BluetoothGatt#STATE_CONNECTING STATE_CONNECTING},
	 * {@link BluetoothGatt#STATE_CONNECTED STATE_CONNECTED},
	 * {@link BluetoothGatt#STATE_DISCONNECTING STATE_DISCONNECTING},
	 * {@link BluetoothGatt#STATE_DISCONNECTED STATE_DISCONNECTED}
	 */
	@ConnectionState
	private int connectionState = BluetoothGatt.STATE_DISCONNECTED;
	/**
	 * 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 connectionPriorityOperationInProgress = false;
	/**
	 * A flag indicating that Reliable Write is in progress.
	 */
	private boolean reliableWriteInProgress;
	/**
	 * The current MTU (Maximum Transfer Unit). The maximum number of bytes that can be sent in
	 * a single packet is MTU-3.
	 */
	private int mtu = 23;
	/**
	 * 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 batteryValue = -1;
	/** Values of non-shared characteristics. Each connected device has its own copy of such. */
	private Map<BluetoothGattCharacteristic, byte[]> characteristicValues;
	/** Values of non-shared descriptors. Each connected device has its own copy of such. */
	private Map<BluetoothGattDescriptor, byte[]> descriptorValues;
	/**
	 * Temporary values of characteristic to support Reliable Write. The temp value will be
	 * set as valid when the write request is executed, or discarded when aborted.
	 */
	private Deque<Pair<Object /* BluetoothGattCharacteristic of BluetoothGattDescriptor> */, byte[]>> preparedValues;
	private int prepareError;
	/**
	 * The connect request. This is instantiated in {@link BleManager#connect(BluetoothDevice, int)}
	 * and nullified after the device is ready.
	 * <p>
	 * This request has a separate reference, as it is notified when the device becomes ready,
	 * after the initialization requests are done.
	 */
	private ConnectRequest connectRequest;
	/**
	 * Currently performed request or null in idle state.
	 */
	private Request request;
	/**
	 * Currently performer request set, or null if none.
	 */
	private RequestQueue requestQueue;
	/**
	 * A map of {@link ValueChangedCallback}s for handling notifications, indications and
	 * write callbacks to server characteristic and descriptors.
	 */
	@NonNull
	private final HashMap<Object, ValueChangedCallback> valueChangedCallbacks = new HashMap<>();
	/**
	 * A special handler for Battery Level notifications.
	 */
	@Nullable
	@Deprecated
	private ValueChangedCallback batteryLevelNotificationCallback;
	/**
	 * 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.
	 */
	@Nullable
	private AwaitingRequest<?> awaitingRequest;

	private final BroadcastReceiver bluetoothStateBroadcastReceiver = 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) {
						// No more calls are possible
						operationInProgress = true;
						taskQueue.clear();
						initQueue = null;

						final BluetoothDevice device = bluetoothDevice;
						if (device != null) {
							// Signal the current request, if any
							if (request != null && request.type != Request.Type.DISCONNECT) {
								request.notifyFail(device, FailCallback.REASON_BLUETOOTH_DISABLED);
								request = null;
							}
							if (awaitingRequest != null) {
								awaitingRequest.notifyFail(device, FailCallback.REASON_BLUETOOTH_DISABLED);
								awaitingRequest = null;
							}
							if (connectRequest != null) {
								connectRequest.notifyFail(device, FailCallback.REASON_BLUETOOTH_DISABLED);
								connectRequest = null;
							}
						}

						// The connection is killed by the system, no need to disconnect gently.
						userDisconnected = true;
						// 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
						operationInProgress = false;
						// This will call close()
						if (device != null) {
							notifyDeviceDisconnected(device, ConnectionObserver.REASON_TERMINATE_LOCAL_HOST);
						}
					} 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 final 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 (bluetoothDevice == null || device == null
					|| !device.getAddress().equals(bluetoothDevice.getAddress()))
				return;

			log(Log.DEBUG, "[Broadcast] Action received: " +
					BluetoothDevice.ACTION_BOND_STATE_CHANGED +
					", bond state changed to: " + ParserUtils.bondStateToString(bondState) +
					" (" + bondState + ")");

			switch (bondState) {
				case BluetoothDevice.BOND_NONE:
					if (previousBondState == BluetoothDevice.BOND_BONDING) {
						postCallback(c -> c.onBondingFailed(device));
						postBondingStateChange(o -> o.onBondingFailed(device));
						log(Log.WARN, "Bonding failed");
						if (request != null) { // CREATE_BOND request
							request.notifyFail(device, FailCallback.REASON_REQUEST_FAILED);
							request = null;
						}
					} else if (previousBondState == BluetoothDevice.BOND_BONDED) {
						if (request != null && request.type == Request.Type.REMOVE_BOND) {
							// The device has already disconnected by now.
							log(Log.INFO, "Bond information removed");
							request.notifySuccess(device);
							request = null;
						}
						// When the bond information has been removed (either with Remove Bond request
						// or in Android Settings), the BluetoothGatt object should be closed, so
						// the library won't reconnect to the device automatically.
						// See: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/157
						close();
					}
					break;
				case BluetoothDevice.BOND_BONDING:
					postCallback(c -> c.onBondingRequired(device));
					postBondingStateChange(o -> o.onBondingRequired(device));
					return;
				case BluetoothDevice.BOND_BONDED:
					log(Log.INFO, "Device bonded");
					postCallback(c -> c.onBonded(device));
					postBondingStateChange(o -> o.onBonded(device));
					if (request != null && request.type == Request.Type.CREATE_BOND) {
						request.notifySuccess(device);
						request = null;
						break;
					}
					// If the device started to pair just after the connection was
					// established the services were not discovered.
					if (!servicesDiscovered && !serviceDiscoveryRequested) {
						serviceDiscoveryRequested = true;
						post(() -> {
							log(Log.VERBOSE, "Discovering services...");
							log(Log.DEBUG, "gatt.discoverServices()");
							bluetoothGatt.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 (request != null) {
							// Repeat the last command in that case.
							enqueueFirst(request);
							break;
						}
					}
					// No need to repeat the request.
					return;
			}
			nextRequest(true);
		}
	};

	/**
	 * Initializes the object.
	 *
	 * @param manager The BLE manager.
	 */
	void init(@NonNull final BleManager manager, @NonNull final Handler handler) {
		this.manager = manager;
		this.handler = handler;
	}

	/**
	 * Binds the server with the BLE manager handler. Call with null to unbind the server.
	 *
	 * @param server the server to bind; null to unbind the server.
	 */
	void useServer(@Nullable final BleServerManager server) {
		this.serverManager = server;
	}

	/**
	 * Closes and releases resources.
	 */
	void close() {
		try {
			final Context context = manager.getContext();
			context.unregisterReceiver(bluetoothStateBroadcastReceiver);
			context.unregisterReceiver(mBondingBroadcastReceiver);
		} catch (final Exception e) {
			// the receiver must have been not registered or unregistered before.
		}
		synchronized (LOCK) {
			if (bluetoothGatt != null) {
				if (manager.shouldClearCacheWhenDisconnected()) {
					if (internalRefreshDeviceCache()) {
						log(Log.INFO, "Cache refreshed");
					} else {
						log(Log.WARN, "Refreshing failed");
					}
				}
				log(Log.DEBUG, "gatt.close()");
				try {
					bluetoothGatt.close();
				} catch (final Throwable t) {
					// ignore
				}
				bluetoothGatt = null;
			}
			reliableWriteInProgress = false;
			initialConnection = false;
			valueChangedCallbacks.clear();
			// 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.operationInProgress = false;
			taskQueue.clear();
			initQueue = null;
			bluetoothDevice = null;
		}
	}

	public BluetoothDevice getBluetoothDevice() {
		return bluetoothDevice;
	}

	/**
	 * Returns the value of the server characteristic. For characteristics that are not shared,
	 * the value may be different for each connected device.
	 *
	 * @param serverCharacteristic The characteristic to get value of.
	 * @return The value.
	 */
	@Nullable
	public final byte[] getCharacteristicValue(@NonNull final BluetoothGattCharacteristic serverCharacteristic) {
		if (characteristicValues != null && characteristicValues.containsKey(serverCharacteristic))
			return characteristicValues.get(serverCharacteristic);
		return serverCharacteristic.getValue();
	}

	/**
	 * Returns the value of the server descriptor. For descriptor that are not shared,
	 * the value may be different for each connected device.
	 *
	 * @param serverDescriptor The descriptor to get value of.
	 * @return The value.
	 */
	@Nullable
	public final byte[] getDescriptorValue(@NonNull final BluetoothGattDescriptor serverDescriptor) {
		if (descriptorValues != null && descriptorValues.containsKey(serverDescriptor))
			return descriptorValues.get(serverDescriptor);
		return serverDescriptor.getValue();
	}

	// Requests implementation

	private boolean internalConnect(@NonNull final BluetoothDevice device,
									@Nullable final ConnectRequest connectRequest) {
		final boolean bluetoothEnabled = BluetoothAdapter.getDefaultAdapter().isEnabled();
		if (connected || !bluetoothEnabled) {
			final BluetoothDevice currentDevice = bluetoothDevice;
			if (bluetoothEnabled && currentDevice != null && currentDevice.equals(device)) {
				this.connectRequest.notifySuccess(device);
			} else {
				// We can't return false here, as the request would be notified with
				// bluetoothDevice instance instead, and that may be null or a wrong device.
				if (this.connectRequest != null) {
					this.connectRequest.notifyFail(device,
							bluetoothEnabled ?
									FailCallback.REASON_REQUEST_FAILED :
									FailCallback.REASON_BLUETOOTH_DISABLED);
				} // else, the request was already failed by the Bluetooth state receiver
			}
			this.connectRequest = null;
			nextRequest(true);
			return true;
		}

		final Context context = manager.getContext();
		synchronized (LOCK) {
			if (bluetoothGatt != 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 (!initialConnection) {
					log(Log.DEBUG, "gatt.close()");
					try {
						bluetoothGatt.close();
					} catch (final Throwable t) {
						// ignore
					}
					bluetoothGatt = 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.
					initialConnection = false;
					connectionTime = 0L; // no timeout possible when autoConnect used
					connectionState = BluetoothGatt.STATE_CONNECTING;
					log(Log.VERBOSE, "Connecting...");
					postCallback(c -> c.onDeviceConnecting(device));
					postConnectionStateChange(o -> o.onDeviceConnecting(device));
					log(Log.DEBUG, "gatt.connect()");
					bluetoothGatt.connect();
					return true;
				}
			} else {
				// Register bonding broadcast receiver
				context.registerReceiver(bluetoothStateBroadcastReceiver,
						new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
				context.registerReceiver(mBondingBroadcastReceiver,
						new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
			}
		}

		// 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.
		userDisconnected = !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) {
			initialConnection = true;
		}
		bluetoothDevice = device;
		log(Log.VERBOSE, connectRequest.isFirstAttempt() ? "Connecting..." : "Retrying...");
		connectionState = BluetoothGatt.STATE_CONNECTING;
		postCallback(c -> c.onDeviceConnecting(device));
		postConnectionStateChange(o -> o.onDeviceConnecting(device));
		connectionTime = 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, "
					+ ParserUtils.phyMaskToString(preferredPhy) + ")");
			// A variant of connectGatt with Handled can't be used here.
			// Check https://github.com/NordicSemiconductor/Android-BLE-Library/issues/54
			bluetoothGatt = device.connectGatt(context, false, gattCallback,
					BluetoothDevice.TRANSPORT_LE, preferredPhy/*, handler*/);
		} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			log(Log.DEBUG, "gatt = device.connectGatt(autoConnect = false, TRANSPORT_LE)");
			bluetoothGatt = device.connectGatt(context, false, gattCallback,
					BluetoothDevice.TRANSPORT_LE);
		} else {
			log(Log.DEBUG, "gatt = device.connectGatt(autoConnect = false)");
			bluetoothGatt = device.connectGatt(context, false, gattCallback);
		}
		return true;
	}

	private boolean internalDisconnect() {
		userDisconnected = true;
		initialConnection = false;
		ready = false;

		if (bluetoothGatt != null) {
			connectionState = BluetoothGatt.STATE_DISCONNECTING;
			log(Log.VERBOSE, connected ? "Disconnecting..." : "Cancelling connection...");
			final BluetoothDevice device = bluetoothGatt.getDevice();
			if (connected) {
				postCallback(c -> c.onDeviceDisconnecting(device));
				postConnectionStateChange(o -> o.onDeviceDisconnecting(device));
			}
			log(Log.DEBUG, "gatt.disconnect()");
			bluetoothGatt.disconnect();
			final boolean wasConnected = connected;
			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.
			connectionState = BluetoothGatt.STATE_DISCONNECTED;
			log(Log.INFO, "Disconnected");
			postCallback(c -> c.onDeviceDisconnected(device));
			postConnectionStateChange(o -> o.onDeviceDisconnected(device, ConnectionObserver.REASON_SUCCESS));
		}
		// request may be of type DISCONNECT or CONNECT (timeout).
		// For the latter, it has already been notified with REASON_TIMEOUT.
		if (request != null && request.type == Request.Type.DISCONNECT) {
			if (bluetoothDevice != null)
				request.notifySuccess(bluetoothDevice);
			else
				request.notifyInvalidRequest();
		}
		nextRequest(true);
		return true;
	}

	private boolean internalCreateBond() {
		final BluetoothDevice device = bluetoothDevice;
		if (device == null)
			return false;

		log(Log.VERBOSE, "Starting bonding...");

		// Warning: The check below only ensures that the bond information is present on the
		//          Android side, not on both. If the bond information has been remove from the
		//          peripheral side, the code below will notify bonding as success, but in fact the
		//          link will not be encrypted! Currently there is no way to ensure that the link
		//          is secure.
		//          Android, despite reporting bond state as BONDED, creates an unencrypted link
		//          and does not report this as a problem. Calling createBond() on a valid,
		//          encrypted link, to ensure that the link is encrypted, returns false (error).
		//          The same result is returned if only the Android side has bond information,
		//          making both cases indistinguishable.
		//
		// Solution: To make sure that sensitive data are sent only on encrypted link make sure
		//           the characteristic/descriptor is protected and reading/writing to it will
		//           initiate bonding request.
		if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
			log(Log.WARN, "Bond information present on client, skipping bonding");
			request.notifySuccess(device);
			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)");
				//noinspection ConstantConditions
				return (Boolean) createBond.invoke(device);
			} catch (final Exception e) {
				Log.w(TAG, "An exception occurred while creating bond", e);
			}
		}
		return false;
	}

	private boolean internalRemoveBond() {
		final BluetoothDevice device = bluetoothDevice;
		if (device == null)
			return false;

		log(Log.VERBOSE, "Removing bond information...");

		if (device.getBondState() == BluetoothDevice.BOND_NONE) {
			log(Log.WARN, "Device is not bonded");
			request.notifySuccess(device);
			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)");
			//noinspection ConstantConditions
			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 <code>false</code>.
	 *
	 * @return <code>true</code> when the request has been sent, <code>false</code> 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 = bluetoothGatt;
		if (gatt == null || !connected)
			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(BleManager.GENERIC_ATTRIBUTE_SERVICE);
		if (gaService == null)
			return false;

		final BluetoothGattCharacteristic scCharacteristic =
				gaService.getCharacteristic(BleManager.SERVICE_CHANGED_CHARACTERISTIC);
		if (scCharacteristic == null)
			return false;

		log(Log.INFO, "Service Changed characteristic found on a bonded device");
		return internalEnableIndications(scCharacteristic);
	}

	private boolean internalEnableNotifications(@Nullable final BluetoothGattCharacteristic characteristic) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || characteristic == null || !connected)
			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(" +
					BleManager.CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID + ", value=0x01-00)");
			return internalWriteDescriptorWorkaround(descriptor);
		}
		return false;
	}

	private boolean internalDisableNotifications(@Nullable final BluetoothGattCharacteristic characteristic) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || characteristic == null || !connected)
			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(" +
					BleManager.CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID + ", value=0x00-00)");
			return internalWriteDescriptorWorkaround(descriptor);
		}
		return false;
	}

	private boolean internalEnableIndications(@Nullable final BluetoothGattCharacteristic characteristic) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || characteristic == null || !connected)
			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(" +
					BleManager.CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID + ", value=0x02-00)");
			return internalWriteDescriptorWorkaround(descriptor);
		}
		return false;
	}

	private boolean internalDisableIndications(@Nullable final BluetoothGattCharacteristic characteristic) {
		// This writes exactly the same settings so do not duplicate code.
		return internalDisableNotifications(characteristic);
	}

	private boolean internalSendNotification(@Nullable final BluetoothGattCharacteristic serverCharacteristic,
											 final boolean confirm) {
		if (serverManager == null || serverManager.getServer() == null || serverCharacteristic == null)
			return false;
		final int requiredProperty = confirm ? BluetoothGattCharacteristic.PROPERTY_INDICATE : BluetoothGattCharacteristic.PROPERTY_NOTIFY;
		if ((serverCharacteristic.getProperties() & requiredProperty) == 0)
			return false;
		final BluetoothGattDescriptor cccd = serverCharacteristic.getDescriptor(BleManager.CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
		if (cccd == null)
			return false;
		// If notifications/indications were enabled, send the notification/indication.
		final byte[] value = descriptorValues.containsKey(cccd) ? descriptorValues.get(cccd) : cccd.getValue();
		if (value != null && value.length == 2 && value[0] != 0) {
			log(Log.VERBOSE, "[Server] Sending " + (confirm ? "indication" : "notification") + " to " + serverCharacteristic.getUuid());
			log(Log.DEBUG, "server.notifyCharacteristicChanged(device, " + serverCharacteristic.getUuid() + ", " + confirm + ")");
			final boolean result = serverManager.getServer().notifyCharacteristicChanged(bluetoothDevice, serverCharacteristic, confirm);
			if (result && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
				post(() -> {
					notifyNotificationSent(bluetoothDevice);
					nextRequest(true);
				});
			}
			return result;
		}
		// Otherwise, assume the data was sent. The remote side has not registered for them.
		nextRequest(true);
		return true;
	}

	/**
	 * 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(BleManager.CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID);
	}

	private boolean internalReadCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || characteristic == null || !connected)
			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);
	}

	private boolean internalWriteCharacteristic(@Nullable final BluetoothGattCharacteristic characteristic) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || characteristic == null || !connected)
			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() +
				" (" + ParserUtils.writeTypeToString(characteristic.getWriteType()) + ")");
		log(Log.DEBUG, "gatt.writeCharacteristic(" + characteristic.getUuid() + ")");
		return gatt.writeCharacteristic(characteristic);
	}

	private boolean internalReadDescriptor(@Nullable final BluetoothGattDescriptor descriptor) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || descriptor == null || !connected)
			return false;

		log(Log.VERBOSE, "Reading descriptor " + descriptor.getUuid());
		log(Log.DEBUG, "gatt.readDescriptor(" + descriptor.getUuid() + ")");
		return gatt.readDescriptor(descriptor);
	}

	private boolean internalWriteDescriptor(@Nullable final BluetoothGattDescriptor descriptor) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || descriptor == null || !connected)
			return false;

		log(Log.VERBOSE, "Writing descriptor " + descriptor.getUuid());
		log(Log.DEBUG, "gatt.writeDescriptor(" + descriptor.getUuid() + ")");
		return internalWriteDescriptorWorkaround(descriptor);
	}

	/**
	 * 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.
	 * <p>
	 * See: <a href="https://android.googlesource.com/platform/frameworks/base/+/942aebc95924ab1e7ea1e92aaf4e7fc45f695a6c%5E%21/#F0">
	 * https://android.googlesource.com/platform/frameworks/base/+/942aebc95924ab1e7ea1e92aaf4e7fc45f695a6c%5E%21/#F0</a>
	 *
	 * @param descriptor the descriptor to be written
	 * @return the result of {@link BluetoothGatt#writeDescriptor(BluetoothGattDescriptor)}
	 */
	private boolean internalWriteDescriptorWorkaround(@Nullable final BluetoothGattDescriptor descriptor) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || descriptor == null || !connected)
			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;
	}

	private boolean internalBeginReliableWrite() {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || !connected)
			return false;

		// Reliable Write can't be before the old one isn't executed or aborted.
		if (reliableWriteInProgress)
			return true;

		log(Log.VERBOSE, "Beginning reliable write...");
		log(Log.DEBUG, "gatt.beginReliableWrite()");
		return reliableWriteInProgress = gatt.beginReliableWrite();
	}

	private boolean internalExecuteReliableWrite() {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || !connected)
			return false;

		if (!reliableWriteInProgress)
			return false;

		log(Log.VERBOSE, "Executing reliable write...");
		log(Log.DEBUG, "gatt.executeReliableWrite()");
		return gatt.executeReliableWrite();
	}

	private boolean internalAbortReliableWrite() {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || !connected)
			return false;

		if (!reliableWriteInProgress)
			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(gatt.getDevice());
		}
		return true;
	}

	@Deprecated
	private boolean internalReadBatteryLevel() {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || !connected)
			return false;

		final BluetoothGattService batteryService = gatt.getService(BleManager.BATTERY_SERVICE);
		if (batteryService == null)
			return false;

		final BluetoothGattCharacteristic batteryLevelCharacteristic =
				batteryService.getCharacteristic(BleManager.BATTERY_LEVEL_CHARACTERISTIC);
		return internalReadCharacteristic(batteryLevelCharacteristic);
	}

	@Deprecated
	private boolean internalSetBatteryNotifications(final boolean enable) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || !connected)
			return false;

		final BluetoothGattService batteryService = gatt.getService(BleManager.BATTERY_SERVICE);
		if (batteryService == null)
			return false;

		final BluetoothGattCharacteristic batteryLevelCharacteristic =
				batteryService.getCharacteristic(BleManager.BATTERY_LEVEL_CHARACTERISTIC);
		if (enable)
			return internalEnableNotifications(batteryLevelCharacteristic);
		else
			return internalDisableNotifications(batteryLevelCharacteristic);
	}

	@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
	private boolean internalRequestMtu(@IntRange(from = 23, to = 517) final int mtu) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || !connected)
			return false;

		log(Log.VERBOSE, "Requesting new MTU...");
		log(Log.DEBUG, "gatt.requestMtu(" + mtu + ")");
		return gatt.requestMtu(mtu);
	}

	@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
	private boolean internalRequestConnectionPriority(@ConnectionPriority final int priority) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || !connected)
			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);
	}

	@RequiresApi(api = Build.VERSION_CODES.O)
	private boolean internalSetPreferredPhy(@PhyMask final int txPhy, @PhyMask final int rxPhy,
											@PhyOption final int phyOptions) {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || !connected)
			return false;

		log(Log.VERBOSE, "Requesting preferred PHYs...");
		log(Log.DEBUG, "gatt.setPreferredPhy(" + ParserUtils.phyMaskToString(txPhy) + ", "
				+ ParserUtils.phyMaskToString(rxPhy) + ", coding option = "
				+ ParserUtils.phyCodedOptionToString(phyOptions) + ")");
		gatt.setPreferredPhy(txPhy, rxPhy, phyOptions);
		return true;
	}

	@RequiresApi(api = Build.VERSION_CODES.O)
	private boolean internalReadPhy() {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || !connected)
			return false;

		log(Log.VERBOSE, "Reading PHY...");
		log(Log.DEBUG, "gatt.readPhy()");
		gatt.readPhy();
		return true;
	}

	private boolean internalReadRssi() {
		final BluetoothGatt gatt = bluetoothGatt;
		if (gatt == null || !connected)
			return false;

		log(Log.VERBOSE, "Reading remote RSSI...");
		log(Log.DEBUG, "gatt.readRemoteRssi()");
		return gatt.readRemoteRssi();
	}

	/**
	 * Sets and returns a callback that will respond to value changes.
	 *
	 * @param attribute attribute to bind the callback with. If null, the returned
	 *                  callback will not be null, but will not be used.
	 * @return The callback.
	 */
	@NonNull
	ValueChangedCallback getValueChangedCallback(@Nullable final Object attribute) {
		ValueChangedCallback callback = valueChangedCallbacks.get(attribute);
		if (callback == null) {
			callback = new ValueChangedCallback(this);
			if (attribute != null) {
				valueChangedCallbacks.put(attribute, callback);
			}
		}
		return callback.free();
	}

	/**
	 * Removes the callback set using {@link #getValueChangedCallback(Object)}.
	 *
	 * @param attribute attribute to unbind the callback from.
	 */
	void removeValueChangedCallback(@Nullable final Object attribute) {
		valueChangedCallbacks.remove(attribute);
	}

	@Deprecated
	DataReceivedCallback getBatteryLevelCallback() {
		return (device, data) -> {
			if (data.size() == 1) {
				//noinspection ConstantConditions
				final int batteryLevel = data.getIntValue(Data.FORMAT_UINT8, 0);
				log(Log.INFO, "Battery Level received: " + batteryLevel + "%");
				batteryValue = batteryLevel;
				onBatteryValueReceived(bluetoothGatt, batteryLevel);
				postCallback(c -> c.onBatteryValueReceived(device, batteryLevel));
			}
		};
	}

	@Deprecated
	void setBatteryLevelNotificationCallback() {
		if (batteryLevelNotificationCallback == null) {
			batteryLevelNotificationCallback = new ValueChangedCallback(this)
					.with((device, data) -> {
						if (data.size() == 1) {
							//noinspection ConstantConditions
							final int batteryLevel = data.getIntValue(Data.FORMAT_UINT8, 0);
							batteryValue = batteryLevel;
							onBatteryValueReceived(bluetoothGatt, batteryLevel);
							postCallback(c -> c.onBatteryValueReceived(device, batteryLevel));
						}
					});
		}
	}

	/**
	 * Clears the device cache.
	 */
	@SuppressWarnings("JavaReflectionMemberAccess")
	private boolean internalRefreshDeviceCache() {
		final BluetoothGatt gatt = bluetoothGatt;
		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");
			//noinspection ConstantConditions
			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;
	}

	// Request Handler methods

	/**
	 * Enqueues the given request at the front of the the init or task queue, depending
	 * on whether the initialization is in progress, or not.
	 *
	 * This method sets the {@link #operationInProgress} to false, assuming the newly added
	 * request will be executed immediately after this method ends.
	 *
	 * @param request the request to be added.
	 */
	private void enqueueFirst(@NonNull final Request request) {
		final RequestQueue rq = requestQueue;
		if (rq == null) {
			final Deque<Request> queue = initInProgress ? initQueue : taskQueue;
			queue.addFirst(request);
		} else {
			rq.addFirst(request);
		}
		request.enqueued = true;
		// This ensures that the request that was put as first will be executed.
		// The reason this was added is stated in
		// https://github.com/NordicSemiconductor/Android-BLE-Library/issues/200
		// Basically, an operation done in several requests (like WriteRequest with split())
		// must be able to be performed despite awaiting request.
		operationInProgress = false;
		// nextRequest(...) must be called after enqueuing this request.
	}

	@Override
	final void enqueue(@NonNull final Request request) {
		final Deque<Request> queue = initInProgress ? initQueue : taskQueue;
		queue.add(request);
		request.enqueued = true;
		nextRequest(false);
	}

	@Override
	final void cancelQueue() {
		taskQueue.clear();
		initQueue = null;
		if (awaitingRequest != null) {
			awaitingRequest.notifyFail(bluetoothDevice, FailCallback.REASON_CANCELLED);
		}
		if (request != null && awaitingRequest != request) {
			request.notifyFail(bluetoothDevice, FailCallback.REASON_CANCELLED);
			request = null;
		}
		awaitingRequest = null;
		if (requestQueue != null) {
			requestQueue.notifyFail(bluetoothDevice, FailCallback.REASON_CANCELLED);
			requestQueue = null;
		}
		if (connectRequest != null) {
			connectRequest.notifyFail(bluetoothDevice, FailCallback.REASON_CANCELLED);
			connectRequest = null;
			internalDisconnect();
		} else {
			nextRequest(true);
		}
	}

	@Override
	final void onRequestTimeout(@NonNull final TimeoutableRequest request) {
		this.request = null;
		awaitingRequest = null;
		if (request.type == Request.Type.CONNECT) {
			connectRequest = 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;
		}
		nextRequest(true);
	}

	@Override
	public void post(@NonNull final Runnable r) {
		handler.post(r);
	}

	@Override
	public void postDelayed(@NonNull final Runnable r, final long delayMillis) {
		handler.postDelayed(r, delayMillis);
	}

	@Override
	public void removeCallbacks(@NonNull final Runnable r) {
		handler.removeCallbacks(r);
	}

	// Helper methods
	@Deprecated
	private interface CallbackRunnable {
		void run(@NonNull final BleManagerCallbacks callbacks);
	}

	@Deprecated
	private void postCallback(@NonNull final CallbackRunnable r) {
		final BleManagerCallbacks callbacks = manager.callbacks;
		if (callbacks != null) {
			post(() -> r.run(callbacks));
		}
	}

	private interface BondingObserverRunnable {
		void run(@NonNull final BondingObserver observer);
	}

	private void postBondingStateChange(@NonNull final BondingObserverRunnable r) {
		final BondingObserver observer = manager.bondingObserver;
		if (observer != null) {
			post(() -> r.run(observer));
		}
	}

	private interface ConnectionObserverRunnable {
		void run(@NonNull final ConnectionObserver observer);
	}

	private void postConnectionStateChange(@NonNull final ConnectionObserverRunnable r) {
		final ConnectionObserver observer = manager.connectionObserver;
		if (observer != null) {
			post(() -> r.run(observer));
		}
	}

	/**
	 * 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
	final int getConnectionState() {
		return connectionState;
	}

	/**
	 * This method returns true if the device is connected. Services could have not been
	 * discovered yet.
	 */
	final boolean isConnected() {
		return connected;
	}

	/**
	 * 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.
	 */
	@Deprecated
	final int getBatteryValue() {
		return batteryValue;
	}

	/**
	 * Returns true if the device is connected and the initialization has finished,
	 * that is when {@link #onDeviceReady()} was called.
	 */
	final boolean isReady() {
		return ready;
	}

	/**
	 * Returns true if {@link BluetoothGatt#beginReliableWrite()} has been called and
	 * the Reliable Write hasn't been executed nor aborted yet.
	 */
	final boolean isReliableWriteInProgress() {
		return reliableWriteInProgress;
	}

	/**
	 * Returns the current MTU (Maximum Transfer Unit).
	 */
	final int getMtu() {
		return mtu;
	}

	final void overrideMtu(@IntRange(from = 23, to = 517) final int mtu) {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
			this.mtu = mtu;
		}
	}

	/**
	 * This method should return <code>true</code> when the gatt device supports the
	 * required services.
	 *
	 * @param gatt the gatt device with services discovered
	 * @return <code>True</code> when the device has the required service.
	 */
	protected abstract boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt);

	/**
	 * This method should return <code>true</code> when the gatt device supports the
	 * optional services. The default implementation returns <code>false</code>.
	 *
	 * @param gatt the gatt device with services discovered
	 * @return <code>True</code> 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.
	 * <p>
	 * 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<Request> 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)}.
	 * <p>
	 * This method is called from the main thread when the services has been discovered and
	 * the device is supported (has required service).
	 * <p>
	 * Remember to call {@link Request#enqueue()} for each request.
	 * <p>
	 * A sample initialization should look like this:
	 * <pre>
	 * &#64;Override
	 * protected void initialize() {
	 *    requestMtu(MTU)
	 *       .with((device, mtu) -> {
	 *           ...
	 *       })
	 *       .enqueue();
	 *    setNotificationCallback(characteristic)
	 *       .with((device, data) -> {
	 *           ...
	 *       });
	 *    enableNotifications(characteristic)
	 *       .done(device -> {
	 *           ...
	 *       })
	 *       .fail((device, status) -> {
	 *           ...
	 *       })
	 *       .enqueue();
	 * }
	 * </pre>
	 */
	protected void initialize() {
		// empty initialization queue
	}

	/**
	 * In this method the manager should get references to server characteristics and descriptors
	 * that will use. The method is called after the service discovery of a remote device has
	 * finished and {@link #isRequiredServiceSupported(BluetoothGatt)} returned true.
	 * <p>
	 * The references obtained in this method should be released in {@link #onDeviceDisconnected()}.
	 * <p>
	 * This method is called only when the server was set by
	 * {@link BleManager#useServer(BleServerManager)} and opened using {@link BleServerManager#open()}.
	 *
	 * @param server The GATT Server instance. Use {@link BluetoothGattServer#getService(UUID)} to
	 *               obtain service instance.
	 */
	protected void onServerReady(@NonNull final BluetoothGattServer server) {
		// empty initialization
	}

	/**
	 * Called when the initialization queue is complete.
	 */
	protected void onDeviceReady() {
		// empty
	}

	/**
	 * 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 int status) {
		final boolean wasConnected = connected;
		connected = false;
		servicesDiscovered = false;
		serviceDiscoveryRequested = false;
		deviceNotSupported = false;
		initInProgress = false;
		connectionState = BluetoothGatt.STATE_DISCONNECTED;
		checkCondition();
		if (!wasConnected) {
			log(Log.WARN, "Connection attempt timed out");
			close();
			postCallback(c -> c.onDeviceDisconnected(device));
			postConnectionStateChange(o -> o.onDeviceFailedToConnect(device, status));
			// ConnectRequest was already notified
		} else if (userDisconnected) {
			log(Log.INFO, "Disconnected");
			close();
			postCallback(c -> c.onDeviceDisconnected(device));
			postConnectionStateChange(o -> o.onDeviceDisconnected(device, status));
			final Request request = this.request;
			if (request != null && request.type == Request.Type.DISCONNECT) {
				request.notifySuccess(device);
			}
		} else {
			log(Log.WARN, "Connection lost");
			postCallback(c -> c.onLinkLossOccurred(device));
			postConnectionStateChange(o -> o.onDeviceDisconnected(device, ConnectionObserver.REASON_LINK_LOSS));
			// 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.
	 * <p>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.
	 * <p>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.
	 * <p>
	 * This method will not be called if {@link BleManager#readBatteryLevel()} and
	 * {@link BleManager#enableBatteryLevelNotifications()} were overridden.
	 * </p>
	 *
	 * @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));
		postCallback(c -> c.onError(device, message, errorCode));
	}

	private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {

		@Override
		public final void onConnectionStateChange(@NonNull final BluetoothGatt gatt,
												  final int status, final int newState) {
			log(Log.DEBUG, "[Callback] Connection state changed with status: " +
					status + " and new state: " + newState + " (" + ParserUtils.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 (bluetoothDevice == 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());
				connected = true;
				connectionTime = 0L;
				connectionState = BluetoothGatt.STATE_CONNECTED;
				postCallback(c -> c.onDeviceConnected(gatt.getDevice()));
				postConnectionStateChange(o -> o.onDeviceConnected(gatt.getDevice()));

				if (!serviceDiscoveryRequested) {
					final boolean bonded = gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED;
					final int delay = manager.getServiceDiscoveryDelay(bonded);
					if (delay > 0)
						log(Log.DEBUG, "wait(" + delay + ")");

					final int connectionCount = ++BleManagerHandler.this.connectionCount;
					postDelayed(() -> {
						if (connectionCount != BleManagerHandler.this.connectionCount) {
							// 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 (connected &&
								gatt.getDevice().getBondState() != BluetoothDevice.BOND_BONDING) {
							serviceDiscoveryRequested = 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 = connectionTime > 0;
					final boolean timeout = canTimeout && now > connectionTime + 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
							&& connectRequest != null && connectRequest.canRetry()) {
						final int delay = connectRequest.getRetryDelay();
						if (delay > 0)
							log(Log.DEBUG, "wait(" + delay + ")");
						postDelayed(() -> internalConnect(gatt.getDevice(), connectRequest), delay);
						return;
					}

					operationInProgress = true; // no more calls are possible
					taskQueue.clear();
					initQueue = null;
					ready = false;

					// Store the current value of the connected and deviceNotSupported flags...
					final boolean wasConnected = connected;
					final boolean notSupported = deviceNotSupported;
					// ...because the next method sets them to false.
					notifyDeviceDisconnected(gatt.getDevice(), // this may call close()
							timeout ?
								ConnectionObserver.REASON_TIMEOUT :
									notSupported ?
										ConnectionObserver.REASON_NOT_SUPPORTED :
										mapDisconnectStatusToReason(status));

					// Signal the current request, if any.
					if (request != null) {
						if (request.type != Request.Type.DISCONNECT && request.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.
							request.notifyFail(gatt.getDevice(),
									status == BluetoothGatt.GATT_SUCCESS ?
											FailCallback.REASON_DEVICE_DISCONNECTED : status);
							request = null;
						}
					}
					if (awaitingRequest != null) {
						awaitingRequest.notifyFail(gatt.getDevice(), FailCallback.REASON_DEVICE_DISCONNECTED);
						awaitingRequest = null;
					}
					if (connectRequest != null) {
						int reason;
						if (notSupported)
							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;
						connectRequest.notifyFail(gatt.getDevice(), reason);
						connectRequest = null;
					}

					// Reset flag, so the next Connect could be enqueued.
					operationInProgress = 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 && initialConnection) {
						internalConnect(gatt.getDevice(), null);
					} else {
						initialConnection = 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));
				}
				postCallback(c -> c.onError(gatt.getDevice(), ERROR_CONNECTION_STATE_CHANGE, status));
			}
		}

		@Override
		public final void onServicesDiscovered(@NonNull final BluetoothGatt gatt, final int status) {
			serviceDiscoveryRequested = false;
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Services discovered");
				servicesDiscovered = true;
				if (isRequiredServiceSupported(gatt)) {
					log(Log.VERBOSE, "Primary service found");
					deviceNotSupported = false;
					final boolean optionalServicesFound = isOptionalServiceSupported(gatt);
					if (optionalServicesFound)
						log(Log.VERBOSE, "Secondary service found");

					// Notify the parent activity.
					postCallback(c -> c.onServicesDiscovered(gatt.getDevice(), optionalServicesFound));

					// Initialize server attributes.
					if (serverManager != null) {
						final BluetoothGattServer server = serverManager.getServer();
						if (server != null) {
							for (final BluetoothGattService service: server.getServices()) {
								for (final BluetoothGattCharacteristic characteristic: service.getCharacteristics()) {
									if (!serverManager.isShared(characteristic)) {
										if (characteristicValues == null)
											characteristicValues = new HashMap<>();
										characteristicValues.put(characteristic, characteristic.getValue());
									}
									for (final BluetoothGattDescriptor descriptor: characteristic.getDescriptors()) {
										if (!serverManager.isShared(descriptor)) {
											if (descriptorValues == null)
												descriptorValues = new HashMap<>();
											descriptorValues.put(descriptor, descriptor.getValue());
										}
									}
								}
							}
							onServerReady(server);
						}
					}

					// Obtain the queue of initialization requests.
					// First, let's call the deprecated initGatt(...).
					initInProgress = true;
					operationInProgress = true;
					initQueue = initGatt(gatt);

					final boolean deprecatedApiUsed = initQueue != null;
					if (deprecatedApiUsed) {
						for (final Request request : initQueue) {
							request.enqueued = true;
						}
					}

					if (initQueue == null)
						initQueue = new LinkedBlockingDeque<>();

					// 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()
								.setRequestHandler(BleManagerHandler.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)
						manager.readBatteryLevel();
						// 3. Enable Battery Level notifications if required (if this char. does not
						//    exist, this operation will be skipped)
						if (manager.callbacks != null &&
							manager.callbacks.shouldEnableBatteryLevelNotifications(gatt.getDevice()))
							manager.enableBatteryLevelNotifications();
					}
					// End

					initialize();
					initInProgress = false;
					nextRequest(true);
				} else {
					log(Log.WARN, "Device is not supported");
					deviceNotSupported = true;
					postCallback(c -> c.onDeviceNotSupported(gatt.getDevice()));
					internalDisconnect();
				}
			} else {
				Log.e(TAG, "onServicesDiscovered error " + status);
				onError(gatt.getDevice(), ERROR_DISCOVERY_SERVICE, status);
				if (connectRequest != null) {
					connectRequest.notifyFail(gatt.getDevice(), FailCallback.REASON_REQUEST_FAILED);
					connectRequest = null;
				}
				internalDisconnect();
			}
		}

		@Override
		public void onCharacteristicRead(final BluetoothGatt gatt,
										 final BluetoothGattCharacteristic characteristic,
										 final int status) {
			final byte[] data = characteristic.getValue();

			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Read Response received from " + characteristic.getUuid() +
						", value: " + ParserUtils.parse(data));

				BleManagerHandler.this.onCharacteristicRead(gatt, characteristic);
				if (request instanceof ReadRequest) {
					final ReadRequest rr = (ReadRequest) request;
					final boolean matches = rr.matches(data);
					if (matches) {
						rr.notifyValueChanged(gatt.getDevice(), data);
					}
					if (!matches || rr.hasMore()) {
						enqueueFirst(rr);
					} else {
						rr.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);
					postCallback(c -> c.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 (request instanceof ReadRequest) {
					request.notifyFail(gatt.getDevice(), status);
				}
				awaitingRequest = null;
				onError(gatt.getDevice(), ERROR_READ_CHARACTERISTIC, status);
			}
			checkCondition();
			nextRequest(true);
		}

		@Override
		public void onCharacteristicWrite(final BluetoothGatt gatt,
										  final BluetoothGattCharacteristic characteristic,
										  final int status) {
			final byte[] data = characteristic.getValue();

			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Data written to " + characteristic.getUuid() +
						", value: " + ParserUtils.parse(data));

				BleManagerHandler.this.onCharacteristicWrite(gatt, characteristic);
				if (request instanceof WriteRequest) {
					final WriteRequest wr = (WriteRequest) request;
					final boolean valid = wr.notifyPacketSent(gatt.getDevice(), data);
					if (!valid && requestQueue instanceof ReliableWriteRequest) {
						wr.notifyFail(gatt.getDevice(), FailCallback.REASON_VALIDATION);
						requestQueue.cancelQueue();
					} else if (wr.hasMore()) {
						enqueueFirst(wr);
					} else {
						wr.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);
					postCallback(c -> c.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 (request instanceof WriteRequest) {
					request.notifyFail(gatt.getDevice(), status);
					// Automatically abort Reliable Write when write error happen
					if (requestQueue instanceof ReliableWriteRequest)
						requestQueue.cancelQueue();
				}
				awaitingRequest = null;
				onError(gatt.getDevice(), ERROR_WRITE_CHARACTERISTIC, status);
			}
			checkCondition();
			nextRequest(true);
		}

		@Override
		public final void onReliableWriteCompleted(@NonNull final BluetoothGatt gatt,
												   final int status) {
			final boolean execute = request.type == Request.Type.EXECUTE_RELIABLE_WRITE;
			reliableWriteInProgress = false;
			if (status == BluetoothGatt.GATT_SUCCESS) {
				if (execute) {
					log(Log.INFO, "Reliable Write executed");
					request.notifySuccess(gatt.getDevice());
				} else {
					log(Log.WARN, "Reliable Write aborted");
					request.notifySuccess(gatt.getDevice());
					requestQueue.notifyFail(gatt.getDevice(), FailCallback.REASON_REQUEST_FAILED);
				}
			} else {
				Log.e(TAG, "onReliableWriteCompleted execute " + execute + ", error " + status);
				request.notifyFail(gatt.getDevice(), status);
				onError(gatt.getDevice(), ERROR_RELIABLE_WRITE, status);
			}
			checkCondition();
			nextRequest(true);
		}

		@Override
		public void onDescriptorRead(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) {
			final byte[] data = descriptor.getValue();

			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Read Response received from descr. " + descriptor.getUuid() +
						", value: " + ParserUtils.parse(data));

				BleManagerHandler.this.onDescriptorRead(gatt, descriptor);
				if (request instanceof ReadRequest) {
					final ReadRequest request = (ReadRequest) BleManagerHandler.this.request;
					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);
					postCallback(c -> c.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 (request instanceof ReadRequest) {
					request.notifyFail(gatt.getDevice(), status);
				}
				awaitingRequest = null;
				onError(gatt.getDevice(), ERROR_READ_DESCRIPTOR, status);
			}
			checkCondition();
			nextRequest(true);
		}

		@Override
		public void onDescriptorWrite(final BluetoothGatt gatt,
									  final BluetoothGattDescriptor descriptor,
									  final int status) {
			final byte[] data = descriptor.getValue();

			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:
								log(Log.INFO, "Notifications and indications disabled");
								break;
							case 0x01:
								log(Log.INFO, "Notifications enabled");
								break;
							case 0x02:
								log(Log.INFO, "Indications enabled");
								break;
						}
						BleManagerHandler.this.onDescriptorWrite(gatt, descriptor);
					}
				} else {
					BleManagerHandler.this.onDescriptorWrite(gatt, descriptor);
				}
				if (request instanceof WriteRequest) {
					final WriteRequest wr = (WriteRequest) request;
					final boolean valid = wr.notifyPacketSent(gatt.getDevice(), data);
					if (!valid && requestQueue instanceof ReliableWriteRequest) {
						wr.notifyFail(gatt.getDevice(), FailCallback.REASON_VALIDATION);
						requestQueue.cancelQueue();
					} else if (wr.hasMore()) {
						enqueueFirst(wr);
					} else {
						wr.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);
					postCallback(c -> c.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 (request instanceof WriteRequest) {
					request.notifyFail(gatt.getDevice(), status);
					// Automatically abort Reliable Write when write error happen
					if (requestQueue instanceof ReliableWriteRequest)
						requestQueue.cancelQueue();
				}
				awaitingRequest = null;
				onError(gatt.getDevice(), ERROR_WRITE_DESCRIPTOR, status);
			}
			checkCondition();
			nextRequest(true);
		}

		@Override
		public void onCharacteristicChanged(final BluetoothGatt gatt,
											final BluetoothGattCharacteristic characteristic) {
			final byte[] data = characteristic.getValue();

			if (isServiceChangedCharacteristic(characteristic)) {
				// TODO this should be tested. Should services be invalidated?
				// Forbid enqueuing more operations.
				operationInProgress = true;
				// Clear queues, services are no longer valid.
				taskQueue.clear();
				initQueue = 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(BleManager.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 (batteryLevelNotificationCallback != null && isBatteryLevelCharacteristic(characteristic)) {
					batteryLevelNotificationCallback.notifyValueChanged(gatt.getDevice(), data);
				}
				// Notify the notification registered listener, if set
				final ValueChangedCallback request = valueChangedCallbacks.get(characteristic);
				if (request != null && request.matches(data)) {
					request.notifyValueChanged(gatt.getDevice(), data);
				}
				// If there is a value change request,
				if (awaitingRequest instanceof WaitForValueChangedRequest
						// registered for this characteristic
						&& awaitingRequest.characteristic == characteristic
						// and didn't have a trigger, or the trigger was started
						// (not necessarily completed)
						&& !awaitingRequest.isTriggerPending()) {
					final WaitForValueChangedRequest valueChangedRequest = (WaitForValueChangedRequest) awaitingRequest;
					if (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.
							awaitingRequest = null;
							if (valueChangedRequest.isTriggerCompleteOrNull()) {
								nextRequest(true);
							}
						}
					}
				}
				if (checkCondition()) {
					nextRequest(true);
				}
			}
		}

		@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) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "MTU changed to: " + mtu);
				BleManagerHandler.this.mtu = mtu;
				BleManagerHandler.this.onMtuChanged(gatt, mtu);
				if (request instanceof MtuRequest) {
					((MtuRequest) request).notifyMtuChanged(gatt.getDevice(), mtu);
					request.notifySuccess(gatt.getDevice());
				}
			} else {
				Log.e(TAG, "onMtuChanged error: " + status + ", mtu: " + mtu);
				if (request instanceof MtuRequest) {
					request.notifyFail(gatt.getDevice(), status);
					awaitingRequest = null;
				}
				onError(gatt.getDevice(), ERROR_MTU_REQUEST, status);
			}
			checkCondition();
			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
		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) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "Connection parameters updated " +
						"(interval: " + (interval * 1.25) + "ms," +
						" latency: " + latency + ", timeout: " + (timeout * 10) + "ms)");
				BleManagerHandler.this.onConnectionUpdated(gatt, interval, latency, timeout);

				// This callback may be called af any time, also when some other request is executed
				if (request instanceof ConnectionPriorityRequest) {
					((ConnectionPriorityRequest) request)
							.notifyConnectionPriorityChanged(gatt.getDevice(), interval, latency, timeout);
					request.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 (request instanceof ConnectionPriorityRequest) {
					request.notifyFail(gatt.getDevice(), status);
					awaitingRequest = 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 (request instanceof ConnectionPriorityRequest) {
					request.notifyFail(gatt.getDevice(), status);
					awaitingRequest = null;
				}
				postCallback(c -> c.onError(gatt.getDevice(), ERROR_CONNECTION_PRIORITY_REQUEST, status));
			}
			if (connectionPriorityOperationInProgress) {
				connectionPriorityOperationInProgress = false;
				checkCondition();
				nextRequest(true);
			}
		}

		@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) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "PHY updated (TX: " + ParserUtils.phyToString(txPhy) +
						", RX: " + ParserUtils.phyToString(rxPhy) + ")");
				if (request instanceof PhyRequest) {
					((PhyRequest) request).notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy);
					request.notifySuccess(gatt.getDevice());
				}
			} else {
				log(Log.WARN, "PHY updated failed with status " + status);
				if (request instanceof PhyRequest) {
					request.notifyFail(gatt.getDevice(), status);
					awaitingRequest = null;
				}
				postCallback(c -> c.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 (checkCondition() || request instanceof PhyRequest) {
				nextRequest(true);
			}
		}

		@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) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				log(Log.INFO, "PHY read (TX: " + ParserUtils.phyToString(txPhy) +
						", RX: " + ParserUtils.phyToString(rxPhy) + ")");
				if (request instanceof PhyRequest) {
					((PhyRequest) request).notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy);
					request.notifySuccess(gatt.getDevice());
				}
			} else {
				log(Log.WARN, "PHY read failed with status " + status);
				if (request instanceof PhyRequest) {
					request.notifyFail(gatt.getDevice(), status);
				}
				awaitingRequest = null;
				postCallback(c -> c.onError(gatt.getDevice(), ERROR_READ_PHY, status));
			}
			checkCondition();
			nextRequest(true);
		}

		@Override
		public final void onReadRemoteRssi(@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 (request instanceof ReadRssiRequest) {
					((ReadRssiRequest) request).notifyRssiRead(gatt.getDevice(), rssi);
					request.notifySuccess(gatt.getDevice());
				}
			} else {
				log(Log.WARN, "Reading remote RSSI failed with status " + status);
				if (request instanceof ReadRssiRequest) {
					request.notifyFail(gatt.getDevice(), status);
				}
				awaitingRequest = null;
				postCallback(c -> c.onError(gatt.getDevice(), ERROR_READ_RSSI, status));
			}
			checkCondition();
			nextRequest(true);
		}
	};

	private int mapDisconnectStatusToReason(final int status) {
		switch (status) {
			case GattError.GATT_SUCCESS:
				return ConnectionObserver.REASON_SUCCESS;
			case GattError.GATT_CONN_TERMINATE_LOCAL_HOST:
				return ConnectionObserver.REASON_TERMINATE_LOCAL_HOST;
			case GattError.GATT_CONN_TERMINATE_PEER_USER:
				return ConnectionObserver.REASON_TERMINATE_PEER_USER;
			case GattError.GATT_CONN_TIMEOUT:
				return ConnectionObserver.REASON_TIMEOUT;
			default:
				return ConnectionObserver.REASON_UNKNOWN;
		}
	}

	final void onCharacteristicReadRequest(@NonNull final BluetoothGattServer server,
										   @NonNull final BluetoothDevice device,
										   final int requestId, final int offset,
										   @NonNull final BluetoothGattCharacteristic characteristic) {
		log(Log.DEBUG, "[Server callback] Read request for characteristic " + characteristic.getUuid()
				+ " (requestId=" + requestId + ", offset: " + offset + ")");
		if (offset == 0)
			log(Log.INFO, "[Server] READ request for characteristic " + characteristic.getUuid() + " received");

		byte[] data = characteristicValues == null || !characteristicValues.containsKey(characteristic)
				? characteristic.getValue() : characteristicValues.get(characteristic);

		WaitForReadRequest waitForReadRequest = null;
		// First, try to get the data from the WaitForReadRequest if the request awaits,
		if (awaitingRequest instanceof WaitForReadRequest
				// is registered for this characteristic
				&& awaitingRequest.characteristic == characteristic
				// and didn't have a trigger, or the trigger was started
				// (not necessarily completed)
				&& !awaitingRequest.isTriggerPending()) {
			waitForReadRequest = (WaitForReadRequest) awaitingRequest;
			waitForReadRequest.setDataIfNull(data);
			data = waitForReadRequest.getData(mtu);
		}
		// If data are longer than MTU - 1, cut the array. Only ATT_MTU - 1 bytes can be sent in Long Read.
		if (data != null && data.length > mtu - 1) {
			data = Bytes.copy(data, offset, mtu - 1);
		}

		sendResponse(server, device, BluetoothGatt.GATT_SUCCESS, requestId, offset, data);

		if (waitForReadRequest != null) {
			waitForReadRequest.notifyPacketRead(device, data);

			// If the request is complete, start next one.
			if (!waitForReadRequest.hasMore() && (data == null || data.length < mtu - 1)) {
				waitForReadRequest.notifySuccess(device);
				awaitingRequest = null;
				nextRequest(true);
			}
		} else if (checkCondition()) {
			nextRequest(true);
		}
	}

	final void onCharacteristicWriteRequest(@NonNull final BluetoothGattServer server,
											@NonNull final BluetoothDevice device, final int requestId,
											@NonNull final BluetoothGattCharacteristic characteristic,
											final boolean preparedWrite, final boolean responseNeeded,
											final int offset, @NonNull final byte[] value) {
		log(Log.DEBUG, "[Server callback] Write " + (responseNeeded ? "request" : "command")
				+ " to characteristic " + characteristic.getUuid()
				+ " (requestId=" + requestId + ", prepareWrite=" + preparedWrite + ", responseNeeded="
				+ responseNeeded + ", offset: " + offset + ", value=" + ParserUtils.parseDebug(value) + ")");
		if (offset == 0) {
			final String type = responseNeeded ? "WRITE REQUEST" : "WRITE COMMAND";
			final String option = preparedWrite ? "Prepare " : "";
			log(Log.INFO, "[Server] " + option + type + " for characteristic " + characteristic.getUuid()
					+ " received, value: " + ParserUtils.parse(value));
		}

		if (responseNeeded) {
			sendResponse(server, device, BluetoothGatt.GATT_SUCCESS, requestId, offset, value);
		}

		// If Prepare Write or Long Write is sent, store the data in a temporary queue until it's executed.
		if (preparedWrite) {
			if (preparedValues == null) {
				preparedValues = new LinkedList<>();
			}
			if (offset == 0) {
				// Add new value to the operations.
				preparedValues.offer(new Pair<>(characteristic, value));
			} else {
				// Concatenate the value to the end of previous value, if the previous request was
				// also for the same characteristic.
				final Pair<Object, byte[]> last = preparedValues.peekLast();
				if (last != null && characteristic.equals(last.first)) {
					preparedValues.pollLast();
					preparedValues.offer(new Pair<>(characteristic, Bytes.concat(last.second, value, offset)));
				} else {
					prepareError = BluetoothGatt.GATT_INVALID_OFFSET;
				}
			}
		} else {
			// Otherwise, save the data immediately.
			if (assignAndNotify(device, characteristic, value) || checkCondition()) {
				nextRequest(true);
			}
		}
	}

	final void onDescriptorReadRequest(@NonNull final BluetoothGattServer server,
									   @NonNull final BluetoothDevice device, final int requestId, final int offset,
									   @NonNull final BluetoothGattDescriptor descriptor) {
		log(Log.DEBUG, "[Server callback] Read request for descriptor " + descriptor.getUuid() + " (requestId=" + requestId + ", offset: " + offset + ")");
		if (offset == 0)
			log(Log.INFO, "[Server] READ request for descriptor " + descriptor.getUuid() + " received");

		byte[] data = descriptorValues == null || !descriptorValues.containsKey(descriptor)
				? descriptor.getValue() : descriptorValues.get(descriptor);

		WaitForReadRequest waitForReadRequest = null;
		// First, try to get the data from the WaitForReadRequest if the request awaits,
		if (awaitingRequest instanceof WaitForReadRequest
				// is registered for this descriptor
				&& awaitingRequest.descriptor == descriptor
				// and didn't have a trigger, or the trigger was started
				// (not necessarily completed)
				&& !awaitingRequest.isTriggerPending()) {
			waitForReadRequest = (WaitForReadRequest) awaitingRequest;
			waitForReadRequest.setDataIfNull(data);
			data = waitForReadRequest.getData(mtu);
		}
		// If data are longer than MTU - 1, cut the array. Only ATT_MTU - 1 bytes can be sent in Long Read.
		if (data != null && data.length > mtu - 1) {
			data = Bytes.copy(data, offset, mtu - 1);
		}

		sendResponse(server, device, BluetoothGatt.GATT_SUCCESS, requestId, offset, data);

		if (waitForReadRequest != null) {
			waitForReadRequest.notifyPacketRead(device, data);

			// If the request is complete, start next one.
			if (!waitForReadRequest.hasMore() && (data == null || data.length < mtu - 1)) {
				waitForReadRequest.notifySuccess(device);
				awaitingRequest = null;
				nextRequest(true);
			}
		} else if (checkCondition()) {
			nextRequest(true);
		}
	}

	final void onDescriptorWriteRequest(@NonNull final BluetoothGattServer server,
										@NonNull final BluetoothDevice device, final int requestId,
										@NonNull final BluetoothGattDescriptor descriptor,
										final boolean preparedWrite, final boolean responseNeeded,
										final int offset, @NonNull final byte[] value) {
		log(Log.DEBUG, "[Server callback] Write " + (responseNeeded ? "request" : "command")
				+ " to descriptor " + descriptor.getUuid()
				+ " (requestId=" + requestId + ", prepareWrite=" + preparedWrite + ", responseNeeded="
				+ responseNeeded + ", offset: " + offset + ", value=" + ParserUtils.parseDebug(value) + ")");
		if (offset == 0) {
			final String type = responseNeeded ? "WRITE REQUEST" : "WRITE COMMAND";
			final String option = preparedWrite ? "Prepare " : "";
			log(Log.INFO, "[Server] " + option + type + " request for descriptor " + descriptor.getUuid()
					+ " received, value: " + ParserUtils.parse(value));
		}

		if (responseNeeded) {
			sendResponse(server, device, BluetoothGatt.GATT_SUCCESS, requestId, offset, value);
		}

		// If Prepare Write or Long Write is sent, store the data in a temporary queue until it's executed.
		if (preparedWrite) {
			if (preparedValues == null) {
				preparedValues = new LinkedList<>();
			}
			if (offset == 0) {
				// Add new value to the operations.
				preparedValues.offer(new Pair<>(descriptor, value));
			} else {
				// Concatenate the value to the end of previous value, if the previous request was
				// also for the same descriptor.
				final Pair<Object, byte[]> last = preparedValues.peekLast();
				if (last != null && descriptor.equals(last.first)) {
					preparedValues.pollLast();
					preparedValues.offer(new Pair<>(descriptor, Bytes.concat(last.second, value, offset)));
				} else {
					prepareError = BluetoothGatt.GATT_INVALID_OFFSET;
				}
			}
		} else {
			// Otherwise, save the data immediately.
			if (assignAndNotify(device, descriptor, value) || checkCondition()) {
				nextRequest(true);
			}
		}
	}

	final void onExecuteWrite(@NonNull final BluetoothGattServer server,
							  @NonNull final BluetoothDevice device, final int requestId,
							  final boolean execute) {
		log(Log.DEBUG, "[Server callback] Execute write request (requestId=" + requestId + ", execute=" + execute + ")");
		if (execute) {
			final Deque<Pair<Object, byte[]>> values = preparedValues;
			log(Log.INFO, "[Server] Execute write request received");
			preparedValues = null;
			if (prepareError != 0) {
				sendResponse(server, device, prepareError, requestId, 0, null);
				prepareError = 0;
				return;
			}
			sendResponse(server, device, BluetoothGatt.GATT_SUCCESS, requestId, 0, null);

			if (values == null || values.isEmpty()) {
				return;
			}
			boolean startNextRequest = false;
			for (final Pair<Object, byte[]> value: values) {
				if (value.first instanceof BluetoothGattCharacteristic) {
					final BluetoothGattCharacteristic characteristic = (BluetoothGattCharacteristic) value.first;
					startNextRequest = assignAndNotify(device, characteristic, value.second) || startNextRequest;
				} else if (value.first instanceof BluetoothGattDescriptor){
					final BluetoothGattDescriptor descriptor = (BluetoothGattDescriptor) value.first;
					startNextRequest = assignAndNotify(device, descriptor, value.second) || startNextRequest;
				}
			}
			if (checkCondition() || startNextRequest) {
				nextRequest(true);
			}
		} else {
			log(Log.INFO, "[Server] Cancel write request received");
			preparedValues = null;
			sendResponse(server, device, BluetoothGatt.GATT_SUCCESS, requestId, 0, null);
		}
	}

	final void onNotificationSent(@NonNull final BluetoothGattServer server,
								  @NonNull final BluetoothDevice device, final int status) {
		log(Log.DEBUG, "[Server callback] Notification sent (status=" + status + ")");
		if (status == BluetoothGatt.GATT_SUCCESS) {
			notifyNotificationSent(device);
		} else {
			Log.e(TAG, "onNotificationSent error " + status);
			if (request instanceof WriteRequest) {
				request.notifyFail(device, status);
			}
			awaitingRequest = null;
			onError(device, ERROR_NOTIFY, status);
		}
		checkCondition();
		nextRequest(true);
	}

	@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
	final void onMtuChanged(@NonNull final BluetoothGattServer server,
							@NonNull final BluetoothDevice device,
							final int mtu) {
		log(Log.INFO, "[Server] MTU changed to: " + mtu);
		BleManagerHandler.this.mtu = mtu;
		checkCondition();
		nextRequest(false);
	}

	private void notifyNotificationSent(@NonNull final BluetoothDevice device) {
		if (request instanceof WriteRequest) {
			final WriteRequest wr = (WriteRequest) request;
			switch (wr.type) {
				case NOTIFY:
					log(Log.INFO, "[Server] Notification sent");
					break;
				case INDICATE:
					log(Log.INFO, "[Server] Indication sent");
					break;
			}
			//noinspection ConstantConditions
			wr.notifyPacketSent(device, wr.characteristic.getValue());
			if (wr.hasMore()) {
				enqueueFirst(wr);
			} else {
				wr.notifySuccess(device);
			}
		}
	}

	private boolean assignAndNotify(@NonNull final BluetoothDevice device,
									@NonNull final BluetoothGattCharacteristic characteristic,
									@NonNull final byte[] value) {
		final boolean isShared = characteristicValues == null || !characteristicValues.containsKey(characteristic);
		if (isShared) {
			characteristic.setValue(value);
		} else {
			characteristicValues.put(characteristic, value);
		}
		// Notify listener
		ValueChangedCallback callback;
		if ((callback = valueChangedCallbacks.get(characteristic)) != null) {
			callback.notifyValueChanged(device, value);
		}

		// Check if a request awaits,
		if (awaitingRequest instanceof WaitForValueChangedRequest
				// is registered for this characteristic
				&& awaitingRequest.characteristic == characteristic
				// and didn't have a trigger, or the trigger was started
				// (not necessarily completed)
				&& !awaitingRequest.isTriggerPending()) {
			final WaitForValueChangedRequest waitForWrite = (WaitForValueChangedRequest) awaitingRequest;
			if (waitForWrite.matches(value)) {
				// notify that new data was received.
				waitForWrite.notifyValueChanged(device, value);

				// If no more data are expected
				if (!waitForWrite.hasMore()) {
					// notify success,
					waitForWrite.notifySuccess(device);
					// 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.
					awaitingRequest = null;
					return waitForWrite.isTriggerCompleteOrNull();
				}
			}
		}
		return false;
	}

	private boolean assignAndNotify(@NonNull final BluetoothDevice device,
									@NonNull final BluetoothGattDescriptor descriptor,
									@NonNull final byte[] value) {
		final boolean isShared = descriptorValues == null || !descriptorValues.containsKey(descriptor);
		if (isShared) {
			descriptor.setValue(value);
		} else {
			descriptorValues.put(descriptor, value);
		}
		// Notify listener
		ValueChangedCallback callback;
		if ((callback = valueChangedCallbacks.get(descriptor)) != null) {
			callback.notifyValueChanged(device, value);
		}

		// Check if a request awaits,
		if (awaitingRequest instanceof WaitForValueChangedRequest
				// is registered for this descriptor
				&& awaitingRequest.descriptor == descriptor
				// and didn't have a trigger, or the trigger was started
				// (not necessarily completed)
				&& !awaitingRequest.isTriggerPending()) {
			final WaitForValueChangedRequest waitForWrite = (WaitForValueChangedRequest) awaitingRequest;
			if (waitForWrite.matches(value)) {
				// notify that new data was received.
				waitForWrite.notifyValueChanged(device, value);

				// If no more data are expected
				if (!waitForWrite.hasMore()) {
					// notify success,
					waitForWrite.notifySuccess(device);
					// 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.
					awaitingRequest = null;
					return waitForWrite.isTriggerCompleteOrNull();
				}
			}
		}
		return false;
	}

	private void sendResponse(@NonNull final BluetoothGattServer server,
							  @NonNull final BluetoothDevice device, final int status,
							  final int requestId, final int offset,
							  @Nullable final byte[] response) {
		String msg;
		switch (status) {
			case BluetoothGatt.GATT_SUCCESS: 				msg = "GATT_SUCCESS"; break;
			case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED: 	msg = "GATT_REQUEST_NOT_SUPPORTED"; break;
			case BluetoothGatt.GATT_INVALID_OFFSET: 		msg = "GATT_INVALID_OFFSET"; break;
			default: throw new InvalidParameterException();
		}
		log(Log.DEBUG, "server.sendResponse(" + msg + ", offset=" + offset + ", value=" + ParserUtils.parseDebug(response) + ")");
		server.sendResponse(device, requestId, status, offset, response);
		log(Log.VERBOSE, "[Server] Response sent");
	}

	private boolean checkCondition() {
		if (awaitingRequest instanceof ConditionalWaitRequest) {
			final ConditionalWaitRequest<?> cwr = (ConditionalWaitRequest<?>) awaitingRequest;
			if (cwr.isFulfilled()) {
				cwr.notifySuccess(bluetoothDevice);
				awaitingRequest = null;
				return true;
			}
		}
		return false;
	}

	/**
	 * Executes the next request. If the last element from the initialization queue has
	 * been executed the {@link #onDeviceReady()} callback is called.
	 */
	@SuppressWarnings("ConstantConditions")
	private synchronized void nextRequest(final boolean force) {
		if (force && operationInProgress) {
			operationInProgress = awaitingRequest != null;
		}

		if (operationInProgress) {
			return;
		}
		final BluetoothDevice bluetoothDevice = this.bluetoothDevice;

		// Get the first request from the init queue
		Request request = null;
		try {
			// If Request set is present, try taking next request from it
			if (requestQueue != null) {
				if (requestQueue.hasMore()) {
					request = requestQueue.getNext().setRequestHandler(this);
				} else {
					// Set is completed
					requestQueue.notifySuccess(bluetoothDevice);
					requestQueue = null;
				}
			}
			// Request wasn't obtained from the request set? Take next one from the queue.
			if (request == null) {
				request = initQueue != null ? initQueue.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 (initQueue != null) {
				initQueue = null; // release the queue

				// Set the 'operation in progress' flag, so any request made in onDeviceReady()
				// will not start new nextRequest() call.
				operationInProgress = true;
				ready = true;
				onDeviceReady();
				if (bluetoothDevice != null) {
					postCallback(c -> c.onDeviceReady(bluetoothDevice));
					postConnectionStateChange(o -> o.onDeviceReady(bluetoothDevice));
				}
				if (connectRequest != null) {
					connectRequest.notifySuccess(connectRequest.getDevice());
					connectRequest = null;
				}
			}
			// If so, we can continue with the task queue
			try {
				request = taskQueue.remove();
			} catch (final Exception e) {
				// No more tasks to perform
				operationInProgress = false;
				this.request = null;
				onManagerReady();
				return;
			}
		}

		boolean result = false;
		operationInProgress = true;
		this.request = request;

		if (request instanceof AwaitingRequest) {
			final AwaitingRequest<?> r = (AwaitingRequest<?>) request;

			// The WAIT_FOR_* request types may override the request with a trigger.
			// This is to ensure that the trigger is done after the awaitingRequest was set.
			int requiredProperty = 0;
			switch (request.type) {
				case WAIT_FOR_NOTIFICATION:
					requiredProperty = BluetoothGattCharacteristic.PROPERTY_NOTIFY;
					break;
				case WAIT_FOR_INDICATION:
					requiredProperty = BluetoothGattCharacteristic.PROPERTY_INDICATE;
					break;
				case WAIT_FOR_READ:
					requiredProperty = BluetoothGattCharacteristic.PROPERTY_READ;
					break;
				case WAIT_FOR_WRITE:
					requiredProperty = BluetoothGattCharacteristic.PROPERTY_WRITE
							| BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
							| BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE;
					break;
			}
			result = connected && bluetoothDevice != null
					&& (r.characteristic == null ||
					   (r.characteristic.getProperties() & requiredProperty) != 0);
			if (result) {
				if (r instanceof ConditionalWaitRequest) {
					final ConditionalWaitRequest<?> cwr = (ConditionalWaitRequest<?>) r;
					if (cwr.isFulfilled()) {
						cwr.notifyStarted(bluetoothDevice);
						cwr.notifySuccess(bluetoothDevice);
						nextRequest(true);
						return;
					}
				}
				awaitingRequest = r;

				if (r.getTrigger() != null) {
					// Call notifyStarted for the awaiting request.
					r.notifyStarted(bluetoothDevice);

					// If the request has another request set as a trigger, update the
					// request with the trigger.
					this.request = request = r.getTrigger();
				}
			}
		}
		// Call notifyStarted on the request before it's executed.
		if (request.type == Request.Type.CONNECT) {
			// When the Connect Request is started, the bluetoothDevice 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 (bluetoothDevice != null) {
				request.notifyStarted(bluetoothDevice);
			} else {
				// The device wasn't connected before. Target is unknown.
				request.notifyInvalidRequest();

				awaitingRequest = null;
				nextRequest(true);
				return;
			}
		}

		switch (request.type) {
			case CONNECT: {
				final ConnectRequest cr = (ConnectRequest) request;
				connectRequest = cr;
				this.request = 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: {
				requestQueue = (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(mtu));
					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(mtu));
				}
				result = internalWriteDescriptor(descriptor);
				break;
			}
			case NOTIFY:
			case INDICATE: {
				final WriteRequest wr = (WriteRequest) request;
				final BluetoothGattCharacteristic characteristic = request.characteristic;
				if (characteristic != null) {
					characteristic.setValue(wr.getData(mtu));
					if (characteristicValues != null && characteristicValues.containsKey(characteristic))
						characteristicValues.put(characteristic, characteristic.getValue());
				}
				result = internalSendNotification(request.characteristic, request.type == Request.Type.INDICATE);
				break;
			}
			case SET_VALUE: {
				final SetValueRequest svr = (SetValueRequest) request;
				if (svr.characteristic != null) {
					if (characteristicValues != null && characteristicValues.containsKey(svr.characteristic))
						characteristicValues.put(svr.characteristic, svr.getData(mtu));
					else
						svr.characteristic.setValue(svr.getData(mtu));
					result = true;
					svr.notifySuccess(bluetoothDevice);
					nextRequest(true);
				}
				break;
			}
			case SET_DESCRIPTOR_VALUE: {
				final SetValueRequest svr = (SetValueRequest) request;
				if (svr.descriptor != null) {
					if (descriptorValues != null && descriptorValues.containsKey(svr.descriptor))
						descriptorValues.put(svr.descriptor, svr.getData(mtu));
					else
						svr.descriptor.setValue(svr.getData(mtu));
					result = true;
					svr.notifySuccess(bluetoothDevice);
					nextRequest(true);
				}
				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) {
					this.request.notifySuccess(bluetoothDevice);
					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 (mtu != mr.getRequiredMtu()
						&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
					result = internalRequestMtu(mr.getRequiredMtu());
				} else {
					result = connected;
					if (result) {
						mr.notifyMtuChanged(bluetoothDevice, mtu);
						mr.notifySuccess(bluetoothDevice);
						nextRequest(true);
						return;
					}
				}
				break;
			}
			case REQUEST_CONNECTION_PRIORITY: {
				final ConnectionPriorityRequest cpr = (ConnectionPriorityRequest) request;
				connectionPriorityOperationInProgress = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
				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.
					// Note:
					// According to https://github.com/NordicSemiconductor/Android-BLE-Library/issues/186
					// some Android 8+ phones don't call this callback. Let's make sure it will be
					// called in any case.
					if (result) {
						postDelayed(() -> {
							if (cpr.notifySuccess(bluetoothDevice)) {
								connectionPriorityOperationInProgress = false;
								nextRequest(true);
							}
						}, 200);
					} else {
						connectionPriorityOperationInProgress = false;
					}
				}
				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 = connected;
					if (result) {
						pr.notifyLegacyPhy(bluetoothDevice);
						pr.notifySuccess(bluetoothDevice);
						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 = connected;
					if (result) {
						pr.notifyLegacyPhy(bluetoothDevice);
						pr.notifySuccess(bluetoothDevice);
						nextRequest(true);
						return;
					}
				}
				break;
			}
			case READ_RSSI: {
				final Request r = request;
				result = internalReadRssi();
				if (result) {
					postDelayed(() -> {
						// This check makes sure that only the failed request will be notified,
						// not some subsequent one.
						if (this.request == r) {
							r.notifyFail(bluetoothDevice, FailCallback.REASON_TIMEOUT);
							nextRequest(true);
						}
					}, 1000);
				}
				break;
			}
			case REFRESH_CACHE: {
				final Request r = request;
				result = internalRefreshDeviceCache();
				if (result) {
					postDelayed(() -> {
						log(Log.INFO, "Cache refreshed");
						r.notifySuccess(bluetoothDevice);
						this.request = null;
						if (awaitingRequest != null) {
							awaitingRequest.notifyFail(bluetoothDevice, FailCallback.REASON_NULL_ATTRIBUTE);
							awaitingRequest = null;
						}
						taskQueue.clear();
						initQueue = null;
						if (connected) {
							// Invalidate all services and characteristics
							onDeviceDisconnected();
							// And discover services again
							log(Log.VERBOSE, "Discovering Services...");
							log(Log.DEBUG, "gatt.discoverServices()");
							bluetoothGatt.discoverServices();
						}
					}, 200);
				}
				break;
			}
			case SLEEP: {
				final SleepRequest sr = (SleepRequest) request;
				log(Log.DEBUG, "sleep(" + sr.getDelay() + ")");
				postDelayed(() -> {
					sr.notifySuccess(bluetoothDevice);
					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) {
			this.request.notifyFail(bluetoothDevice,
					connected ?
							FailCallback.REASON_NULL_ATTRIBUTE :
							BluetoothAdapter.getDefaultAdapter().isEnabled() ?
									FailCallback.REASON_DEVICE_DISCONNECTED :
									FailCallback.REASON_BLUETOOTH_DISABLED);
			awaitingRequest = null;
			connectionPriorityOperationInProgress = false;
			nextRequest(true);
		}
	}

	// Helper methods

	/**
	 * 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 &&
				BleManager.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 &&
				BleManager.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 &&
				BleManager.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(@Nullable final BluetoothGattDescriptor descriptor) {
		return descriptor != null &&
				BleManager.CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID.equals(descriptor.getUuid());
	}

//	private boolean isReliableWriteSupported(@Nullable final BluetoothGattCharacteristic characteristic) {
//		if (characteristic == null)
//			return false;
//		final BluetoothGattDescriptor cep = characteristic.getDescriptor(BleServerManager.CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR_UUID);
//		return cep != null && cep.getValue() != null && cep.getValue().length >= 2 && (cep.getValue()[0] & 0x01) != 0;
//	}

	private void log(final int priority, @NonNull final String message) {
		manager.log(priority, message);
	}
}