@SuppressLint("MissingPermission")
	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()) {
					//noinspection DataFlowIssue
					request = requestQueue.getNext().setRequestHandler(this);
				} else {
					if (requestQueue instanceof final ReliableWriteRequest rwr) {
						if (rwr.isCancelled()) {
							requestQueue.notifyFail(bluetoothDevice, FailCallback.REASON_CANCELLED);
						}
					}
					// Set is completed. This is a NOOP if the request has failed.
					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
		}

		// 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;
				manager.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;
				manager.onManagerReady();
				return;
			}
		}

		// If the request has already been cancelled, proceed to the next one.
		if (request.finished) {
			nextRequest(false);
			return;
		}

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

		if (request instanceof final AwaitingRequest<?> r) {
			// 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 = switch (request.type) {
				case WAIT_FOR_NOTIFICATION -> BluetoothGattCharacteristic.PROPERTY_NOTIFY;
				case WAIT_FOR_INDICATION -> BluetoothGattCharacteristic.PROPERTY_INDICATE;
				case WAIT_FOR_READ -> BluetoothGattCharacteristic.PROPERTY_READ;
				case WAIT_FOR_WRITE -> BluetoothGattCharacteristic.PROPERTY_WRITE
						| BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
						| BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE;
				default -> 0;
			};
			result = connected && bluetoothDevice != null
					&& (r.characteristic == null ||
					   (r.characteristic.getProperties() & requiredProperty) != 0);
			if (result) {
				if (r instanceof final ConditionalWaitRequest<?> cwr) {
					log(Log.VERBOSE, () -> "Waiting for fulfillment of condition...");
					if (cwr.isFulfilled()) {
						cwr.notifyStarted(bluetoothDevice);
						log(Log.INFO, () -> "Condition fulfilled");
						cwr.notifySuccess(bluetoothDevice);
						nextRequest(true);
						return;
					}
				}
				if (r instanceof WaitForReadRequest) {
					log(Log.VERBOSE, () -> "Waiting for read request...");
				}
				if (r instanceof WaitForValueChangedRequest) {
					log(Log.VERBOSE, () -> "Waiting for value change...");
				}
				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 instanceof final ConnectRequest cr) {
			// 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()
			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;
			}
		}

		// At this point the bluetoothDevice is either null, and the request is a ConnectRequest,
		// or not a null.
		assert bluetoothDevice != null || request.type == Request.Type.CONNECT;

		switch (request.type) {
			case CONNECT: {
				//noinspection DataFlowIssue
				final ConnectRequest cr = (ConnectRequest) request;
				connectRequest = cr;
				this.request = null;
				result = internalConnect(cr.getDevice(), cr);
				break;
			}
			case DISCONNECT: {
				internalDisconnect(ConnectionObserver.REASON_SUCCESS);
				// If a disconnect request failed, it has already been notified at this point,
				// therefore result is a success (true).
				result = true;
				break;
			}
			case ENSURE_BOND: {
				result = internalCreateBond(true);
				break;
			}
			case CREATE_BOND: {
				result = internalCreateBond(false);
				break;
			}
			case REMOVE_BOND: {
				result = internalRemoveBond();
				break;
			}
			case SET: {
				//noinspection DataFlowIssue
				requestQueue = (RequestQueue) request;
				nextRequest(true);
				return;
			}
			case READ: {
				result = internalReadCharacteristic(request.characteristic);
				break;
			}
			case WRITE: {
				//noinspection DataFlowIssue
				final WriteRequest wr = (WriteRequest) request;
				result = internalWriteCharacteristic(wr.characteristic, wr.getData(mtu), wr.getWriteType());
				break;
			}
			case READ_DESCRIPTOR: {
				result = internalReadDescriptor(request.descriptor);
				break;
			}
			case WRITE_DESCRIPTOR: {
				//noinspection DataFlowIssue
				final WriteRequest wr = (WriteRequest) request;
				result = internalWriteDescriptor(wr.descriptor, wr.getData(mtu));
				break;
			}
			case NOTIFY:
			case INDICATE: {
				//noinspection DataFlowIssue
				final WriteRequest wr = (WriteRequest) request;
				final byte[] data = wr.getData(mtu);
				if (wr.characteristic != null) {
					wr.characteristic.setValue(data);
					if (characteristicValues != null && characteristicValues.containsKey(wr.characteristic))
						characteristicValues.put(wr.characteristic, data);
				}
				result = internalSendNotification(wr.characteristic, request.type == Request.Type.INDICATE, data);
				break;
			}
			case SET_VALUE: {
				//noinspection DataFlowIssue
				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: {
				//noinspection DataFlowIssue
				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: {
				//noinspection DataFlowIssue
				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: {
				//noinspection DataFlowIssue
				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: {
				//noinspection DataFlowIssue
				final PhyRequest pr = (PhyRequest) request;
				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
					result = internalSetPreferredPhy(
							pr.getPreferredTxPhy(),
							pr.getPreferredRxPhy(),
							pr.getPreferredPhyOptions()
					);
					if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) {
						// There seems to be a bug on Android 13, where there is no callback
						// for setPreferredPhy(...). onPhyUpdate(...) should be called, but it is not.
						// See: https://github.com/NordicSemiconductor/Android-BLE-Library/issues/414
						// However, the operation seems to complete successfully.
						// As a workaround, let's read the PHYs and notify the user.
						handler.postDelayed(() -> {
							if (!pr.finished) {
								log(Log.WARN, () -> "Callback not received in 1000 ms");
								internalReadPhy();
							}
						}, 1000);
					}
				} else {
					result = connected;
					if (result) {
						pr.notifyLegacyPhy(bluetoothDevice);
						pr.notifySuccess(bluetoothDevice);
						nextRequest(true);
						return;
					}
				}
				break;
			}
			case READ_PHY: {
				//noinspection DataFlowIssue
				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;
						}
						emptyTasks(FailCallback.REASON_NULL_ATTRIBUTE);
						final BluetoothGatt bluetoothGatt = this.bluetoothGatt;
						if (connected && bluetoothGatt != null) {
							// Invalidate all services and characteristics
							manager.onServicesInvalidated();
							onDeviceDisconnected();
							// And discover services again
							serviceDiscoveryRequested = true;
							servicesDiscovered = false;
							log(Log.VERBOSE, () -> "Discovering Services...");
							log(Log.DEBUG, () -> "gatt.discoverServices()");
							bluetoothGatt.discoverServices();
						}
					}, 200);
				}
				break;
			}
			case SLEEP: {
				//noinspection DataFlowIssue
				final SleepRequest sr = (SleepRequest) request;
				log(Log.DEBUG, () -> "sleep(" + sr.timeout + ")");
				result = true;
				// The Sleep request will timeout after the given time,
				// and onRequestTimeout will be called.
				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 && bluetoothDevice != null) {
			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);
		}
	}
Executes the next request. If the last element from the initialization queue has
been executed the {@link #onDeviceReady()} callback is called.
ble/src/main/java/no/nordicsemi/android/ble/BleManagerHandler.java