package com.polidea.rxandroidble2.internal.operations; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.os.DeadObjectException; import androidx.annotation.NonNull; import com.polidea.rxandroidble2.RxBleConnection; import com.polidea.rxandroidble2.exceptions.BleDisconnectedException; import com.polidea.rxandroidble2.exceptions.BleException; import com.polidea.rxandroidble2.exceptions.BleGattCallbackTimeoutException; import com.polidea.rxandroidble2.exceptions.BleGattOperationType; import com.polidea.rxandroidble2.internal.QueueOperation; import com.polidea.rxandroidble2.internal.connection.BluetoothGattProvider; import com.polidea.rxandroidble2.internal.connection.ConnectionStateChangeListener; import com.polidea.rxandroidble2.internal.connection.RxBleGattCallback; import com.polidea.rxandroidble2.internal.logger.LoggerUtil; import com.polidea.rxandroidble2.internal.serialization.QueueReleaseInterface; import com.polidea.rxandroidble2.internal.util.BleConnectionCompat; import java.util.concurrent.Callable; import bleshadow.javax.inject.Inject; import bleshadow.javax.inject.Named; import io.reactivex.ObservableEmitter; import io.reactivex.Single; import io.reactivex.SingleEmitter; import io.reactivex.SingleOnSubscribe; import io.reactivex.SingleTransformer; import io.reactivex.functions.Action; import io.reactivex.functions.Predicate; import io.reactivex.observers.DisposableSingleObserver; import static com.polidea.rxandroidble2.RxBleConnection.RxBleConnectionState.CONNECTED; import static com.polidea.rxandroidble2.RxBleConnection.RxBleConnectionState.CONNECTING; import static com.polidea.rxandroidble2.internal.DeviceModule.CONNECT_TIMEOUT; import static com.polidea.rxandroidble2.internal.connection.ConnectionComponent.NamedBooleans.AUTO_CONNECT; import static com.polidea.rxandroidble2.internal.util.DisposableUtil.disposableSingleObserverFromEmitter; public class ConnectOperation extends QueueOperation<BluetoothGatt> { final BluetoothDevice bluetoothDevice; final BleConnectionCompat connectionCompat; final RxBleGattCallback rxBleGattCallback; final BluetoothGattProvider bluetoothGattProvider; final TimeoutConfiguration connectTimeout; final boolean autoConnect; final ConnectionStateChangeListener connectionStateChangedAction; @Inject ConnectOperation( BluetoothDevice bluetoothDevice, BleConnectionCompat connectionCompat, RxBleGattCallback rxBleGattCallback, BluetoothGattProvider bluetoothGattProvider, @Named(CONNECT_TIMEOUT) TimeoutConfiguration connectTimeout, @Named(AUTO_CONNECT) boolean autoConnect, ConnectionStateChangeListener connectionStateChangedAction) { this.bluetoothDevice = bluetoothDevice; this.connectionCompat = connectionCompat; this.rxBleGattCallback = rxBleGattCallback; this.bluetoothGattProvider = bluetoothGattProvider; this.connectTimeout = connectTimeout; this.autoConnect = autoConnect; this.connectionStateChangedAction = connectionStateChangedAction; } @Override protected void protectedRun(final ObservableEmitter<BluetoothGatt> emitter, final QueueReleaseInterface queueReleaseInterface) { final Action queueReleaseAction = new Action() { @Override public void run() { queueReleaseInterface.release(); } }; final DisposableSingleObserver<BluetoothGatt> disposableGattObserver = getConnectedBluetoothGatt() .compose(wrapWithTimeoutWhenNotAutoconnecting()) // when there are no subscribers there is no point of continuing work -> next will be disconnect operation .doFinally(queueReleaseAction) .subscribeWith(disposableSingleObserverFromEmitter(emitter)); emitter.setDisposable(disposableGattObserver); if (autoConnect) { // with autoConnect the connection may be established after a really long time queueReleaseInterface.release(); } } private SingleTransformer<BluetoothGatt, BluetoothGatt> wrapWithTimeoutWhenNotAutoconnecting() { return new SingleTransformer<BluetoothGatt, BluetoothGatt>() { @Override public Single<BluetoothGatt> apply(Single<BluetoothGatt> bluetoothGattSingle) { return autoConnect ? bluetoothGattSingle : bluetoothGattSingle .timeout(connectTimeout.timeout, connectTimeout.timeoutTimeUnit, connectTimeout.timeoutScheduler, prepareConnectionTimeoutError()); } }; } @NonNull Single<BluetoothGatt> prepareConnectionTimeoutError() { return Single.fromCallable(new Callable<BluetoothGatt>() { @Override public BluetoothGatt call() { throw new BleGattCallbackTimeoutException(bluetoothGattProvider.getBluetoothGatt(), BleGattOperationType.CONNECTION_STATE); } }); } /** * Emits BluetoothGatt and completes after connection is established. * * @return BluetoothGatt after connection reaches {@link com.polidea.rxandroidble2.RxBleConnection.RxBleConnectionState#CONNECTED} * state. * @throws com.polidea.rxandroidble2.exceptions.BleDisconnectedException if connection was disconnected/failed before * it was established. */ @NonNull private Single<BluetoothGatt> getConnectedBluetoothGatt() { // start connecting the BluetoothGatt // note: Due to different Android BLE stack implementations it is not certain whether `connectGatt()` or `BluetoothGattCallback` // will emit BluetoothGatt first return Single.create(new SingleOnSubscribe<BluetoothGatt>() { @Override public void subscribe(final SingleEmitter<BluetoothGatt> emitter) { final DisposableSingleObserver<BluetoothGatt> disposableGattObserver = getBluetoothGattAndChangeStatusToConnected() // when the connected state will be emitted bluetoothGattProvider should contain valid Gatt .delaySubscription( rxBleGattCallback .getOnConnectionStateChange() .filter(new Predicate<RxBleConnection.RxBleConnectionState>() { @Override public boolean test(RxBleConnection.RxBleConnectionState rxBleConnectionState) { return rxBleConnectionState == CONNECTED; } }) ) // disconnect may happen even if the connection was not established yet .mergeWith(rxBleGattCallback.<BluetoothGatt>observeDisconnect().firstOrError()) .firstOrError() .subscribeWith(disposableSingleObserverFromEmitter(emitter)); emitter.setDisposable(disposableGattObserver); connectionStateChangedAction.onConnectionStateChange(CONNECTING); /* * Apparently the connection may be established fast enough to introduce a race condition so the subscription * must be established first before starting the connection. * https://github.com/Polidea/RxAndroidBle/issues/178 * */ final BluetoothGatt bluetoothGatt = connectionCompat .connectGatt(bluetoothDevice, autoConnect, rxBleGattCallback.getBluetoothGattCallback()); /* * Update BluetoothGatt when connection is initiated. It is not certain * if this or RxBleGattCallback.onConnectionStateChange will be first. * */ bluetoothGattProvider.updateBluetoothGatt(bluetoothGatt); } }); } Single<BluetoothGatt> getBluetoothGattAndChangeStatusToConnected() { return Single.fromCallable( new Callable<BluetoothGatt>() { @Override public BluetoothGatt call() { connectionStateChangedAction.onConnectionStateChange(CONNECTED); return bluetoothGattProvider.getBluetoothGatt(); } }); } @Override protected BleException provideException(DeadObjectException deadObjectException) { return new BleDisconnectedException(deadObjectException, bluetoothDevice.getAddress(), BleDisconnectedException.UNKNOWN_STATUS); } @Override public String toString() { return "ConnectOperation{" + LoggerUtil.commonMacMessage(bluetoothDevice.getAddress()) + ", autoConnect=" + autoConnect + '}'; } }