Skip to content

Commit 0b4c8c8

Browse files
author
Vincent Masselis
authored
Merge pull request #4 from VincentMasselis/chore/callback-refactor
Chore/callback refactor
2 parents 2063674 + a391f46 commit 0b4c8c8

File tree

19 files changed

+656
-584
lines changed

19 files changed

+656
-584
lines changed

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ If you're not familiar with the Bluetooth Low Energy API or if you want to try t
9393
## Decorator patter
9494
⚠ Before reading this part, you must know how a [Decorator design pattern](https://en.wikipedia.org/wiki/Decorator_pattern) works and how to make a new one.
9595

96-
On Android, communicating with a Bluetooth device requires an instance of `BluetoothGatt` and an instance of `BluetoothGattCallback`. RxBluetoothKotlin wraps both of theses types into `RxBluetoothGatt` and `RxBluetoothGatt.Callback` types to add some reactive touch to the system objects. Because `RxBluetoothGatt` is an interface and `BluetoothGattCallback` an abstract class, calling `connectRxGatt` will return a default implentation for both of them. You are free to wrap the returned `RxBluetoothGatt` implementation and update the original object by adding you own behavior, you only have to follow the Decorator rules.
96+
On Android, communicating with a Bluetooth device requires an instance of `BluetoothGatt` and an instance of `BluetoothGattCallback`. RxBluetoothKotlin wraps both of theses types into `RxBluetoothGatt` and `RxBluetoothGatt.Callback` types to add some reactive touch to the system objects. Both `RxBluetoothGatt` and `RxBluetoothGatt.Callback` are interfaces, calling `connectRxGatt` will return a default implementation for both of them but you are free to wrap the returned implementation by your own implementation to add you own behavior, you only have to follow the Decorator rules.
9797

9898
### Decorate RxBluetoothGatt
9999
The following diagram will show you which classes are used to create the decorator pattern:
@@ -114,7 +114,7 @@ On this website `https://yuml.me/diagram/scruffy/class/draw`
114114

115115
As you can see, to create a decorator, you only have to subclass `SimpleRxBluetoothGatt`. If you want to decorate `RxBluetoothGatt.Callback` just subclass `SimpleRxBluetoothGattCallback` like you do with `SimpleRxBluetoothGatt` from the previous example.
116116

117-
When your decorators are written you can send them to RxBluetoothKotlin by adding `callbackConstructor` and `rxGattConstructor` parameters when calling `connectRxGatt`. Defaults implentation of RxBluetoothKotlin uses `RxBluetoothGattImpl` and `RxBluetoothGattCallbackImpl`, by using you own decorator you can change the way RxBluetoothKotlin is communicating with the Android SDK in order to match your own requirements.
117+
When your decorators are written you can send them to RxBluetoothKotlin by setting the `rxGattBuilder` and `rxCallbackBuilder` parameters when calling `connectRxGatt`. Defaults implementation of RxBluetoothKotlin uses `RxBluetoothGattImpl` and `RxBluetoothGattCallbackImpl`, by using you own decorator you can change the way RxBluetoothKotlin is communicating with the Android SDK in order to match your own requirements.
118118

119119
## Links
120120
Report an issue by using [github](https://github.com/VincentMasselis/RxBluetoothKotlin/issues)
@@ -125,10 +125,6 @@ Discover our [Equisense sensors](https://equisense.com)
125125

126126
//TODO
127127

128-
- More test
129-
130128
- Getting started
131129

132-
- Example app
133-
134130
- Licence

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@ task clean(type: Delete) {
3636
ext {
3737
versionCode = "$System.env.BITRISE_BUILD_NUMBER"
3838
groupId = 'com.vincentmasselis.rxbluetoothkotlin'
39-
libVersion = "1.2.2"
39+
libVersion = "1.3.0"
4040
}

demo-app/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
android:name="android.hardware.bluetooth_le"
77
android:required="true" />
88

9-
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
9+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
1010

1111
<application
1212
android:allowBackup="true"

dev-app/build.gradle

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ apply plugin: 'kotlin-android'
33
apply plugin: 'kotlin-android-extensions'
44

55
android {
6-
compileSdkVersion 28
6+
compileSdkVersion 29
77
defaultConfig {
88
applicationId "com.vincentmasselis.app"
99
minSdkVersion 18
10-
targetSdkVersion 28
10+
targetSdkVersion 29
1111
versionCode 1
1212
versionName "1.0"
1313

@@ -27,13 +27,13 @@ dependencies {
2727
testImplementation 'junit:junit:4.12'
2828
testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
2929
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
30-
testImplementation 'io.reactivex.rxjava2:rxjava:2.2.11'
30+
testImplementation 'io.reactivex.rxjava2:rxjava:2.2.13'
3131

3232
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
3333
androidTestImplementation 'androidx.test:runner:1.2.0'
3434
androidTestImplementation 'com.android.support.test:rules:1.0.2'
3535
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
36-
androidTestImplementation 'io.reactivex.rxjava2:rxjava:2.2.11'
36+
androidTestImplementation 'io.reactivex.rxjava2:rxjava:2.2.13'
3737
androidTestImplementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
3838
androidTestImplementation 'no.nordicsemi.android.support.v18:scanner:1.4.0'
3939

@@ -42,7 +42,7 @@ dependencies {
4242
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
4343
implementation 'androidx.appcompat:appcompat:1.1.0'
4444
implementation 'androidx.core:core-ktx:1.1.0'
45-
implementation 'io.reactivex.rxjava2:rxjava:2.2.11'
45+
implementation 'io.reactivex.rxjava2:rxjava:2.2.13'
4646
implementation 'no.nordicsemi.android.support.v18:scanner:1.4.0'
4747
implementation 'com.vincentmasselis.rxuikotlin:rxuikotlin-core:1.2.0'
4848

dev-app/src/androidTest/java/com/vincentmasselis/devapp/DisconnectionTests.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class DisconnectionTests {
4040
gatt.whenConnectionIsReady().map { gatt }
4141
}
4242
.doOnSuccess { activity.setMessage("Discovering services") }
43+
.delay(600, TimeUnit.MILLISECONDS)
4344
.flatMap { gatt -> gatt.discoverServices().doOnSubscribe { Logger.v(TAG, "Subscribing to fetch services") }.map { gatt } }
4445
.timeout(20, TimeUnit.SECONDS)
4546
.doOnError { Logger.e(TAG, "Failed, reason :$it") }
@@ -67,6 +68,7 @@ class DisconnectionTests {
6768
gatt.whenConnectionIsReady().map { gatt }
6869
}
6970
.doOnSuccess { activity.setMessage("Discovering services") }
71+
.delay(600, TimeUnit.MILLISECONDS)
7072
.flatMap { gatt -> gatt.discoverServices().doOnSubscribe { Logger.v(TAG, "Subscribing to fetch services") }.map { gatt } }
7173
.timeout(20, TimeUnit.SECONDS)
7274
.doOnError { Logger.e(TAG, "Failed, reason :$it") }
@@ -110,7 +112,7 @@ class DisconnectionTests {
110112
fun disconnection5sTest() {
111113
val activity = mainActivityRule.launchActivity(null)
112114
bluetoothPreconditions(activity)
113-
val gatt = (activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager)
115+
(activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager)
114116
.rxScan()
115117
.doOnSubscribe { activity.setMessage("Please wakeup your device") }
116118
.filter { it.device.address == DEVICE_MAC } // Write the mac address for your own device here
@@ -122,6 +124,7 @@ class DisconnectionTests {
122124
gatt.whenConnectionIsReady().map { gatt }
123125
}
124126
.doOnSuccess { activity.setMessage("Discovering services") }
127+
.delay(600, TimeUnit.MILLISECONDS)
125128
.flatMap { gatt -> gatt.discoverServices().doOnSubscribe { Logger.v(TAG, "Subscribing to fetch services") }.map { gatt } }
126129
.flatMapCompletable { it.listenDisconnection().doOnSubscribe { Logger.v(TAG, "Listening for disconnection") } }
127130
.timeout(20, TimeUnit.SECONDS)
@@ -147,6 +150,7 @@ class DisconnectionTests {
147150
.flatMapSingleElement { it.device.connectRxGatt(logger = Logger) }
148151
.flatMap { gatt -> gatt.whenConnectionIsReady().map { gatt } }
149152
.doOnSuccess { activity.setMessage("Discovering services") }
153+
.delay(600, TimeUnit.MILLISECONDS)
150154
.flatMap { gatt -> gatt.discoverServices().doOnSubscribe { Logger.v(TAG, "Subscribing to fetch services"); gatt.disconnect().subscribe() }.map { gatt } }
151155
.timeout(20, TimeUnit.SECONDS)
152156
.doOnError { Logger.e(TAG, "Failed, reason :$it") }

dev-app/src/androidTest/java/com/vincentmasselis/devapp/EnqueueUnitTest.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class EnqueueUnitTest {
4141
.doOnSuccess { activity.setMessage("Connecting") }
4242
.flatMapSingleElement { it.device.connectRxGatt(logger = Logger) }
4343
.flatMap { gatt -> gatt.whenConnectionIsReady().map { gatt } }
44+
.delay(600, TimeUnit.MILLISECONDS)
4445
.doOnSuccess { activity.setMessage("Discovering services") }
4546
.flatMap { gatt -> gatt.discoverServices().map { gatt } }
4647
.doOnSuccess { activity.setMessage("Running tests") }
@@ -58,6 +59,7 @@ class EnqueueUnitTest {
5859
}
5960
.doOnComplete { throw IllegalStateException("Should not complete here") }
6061
.doOnError { Logger.e(TAG, "Failed, reason :$it") }
62+
.timeout(20L, TimeUnit.SECONDS)
6163
.test()
6264
.await()
6365
.assertValueCount(1)
@@ -83,9 +85,10 @@ class EnqueueUnitTest {
8385
.doOnSuccess { activity.setMessage("Connecting") }
8486
.flatMapSingleElement { it.device.connectRxGatt(logger = Logger) }
8587
.flatMap { gatt -> gatt.whenConnectionIsReady().map { gatt } }
86-
.delay(7, TimeUnit.SECONDS) // Small delay to force the sensor switch into 500ms connection interval
8788
.doOnSuccess { activity.setMessage("Discovering services") }
89+
.delay(600, TimeUnit.MILLISECONDS)
8890
.flatMap { gatt -> gatt.discoverServices().map { gatt } }
91+
.delay(7, TimeUnit.SECONDS) // Small delay to force the sensor switch into 500ms connection interval
8992
.doOnSuccess { activity.setMessage("Running tests") }
9093
.flatMap { gatt ->
9194
Maybes
@@ -104,6 +107,7 @@ class EnqueueUnitTest {
104107
}
105108
.doOnSuccess { throw IllegalStateException("Should not succeed here, It should complete with because of the gatt.disconnect() call") }
106109
.doOnError { Logger.e(TAG, "Failed, reason :$it") }
110+
.timeout(20L, TimeUnit.SECONDS)
107111
.test()
108112
.await()
109113
.assertComplete()
@@ -127,6 +131,7 @@ class EnqueueUnitTest {
127131
.flatMap { gatt -> gatt.whenConnectionIsReady().map { gatt } }
128132
.doOnSuccess { it.disconnect().subscribe() }
129133
.doOnSuccess { activity.setMessage("Discovering services") }
134+
.delay(600, TimeUnit.MILLISECONDS)
130135
.flatMap { gatt -> gatt.discoverServices().map { gatt } }
131136
.doOnSuccess { throw IllegalStateException("Should not succeed here, It should complete with because of the gatt.disconnect() call") }
132137
.doOnError { Logger.e(TAG, "Failed, reason :$it") }
@@ -194,7 +199,7 @@ class EnqueueUnitTest {
194199
.doOnSubscribe { Logger.e(TAG, "I/O Subscription") }
195200
.doOnDispose { Logger.e(TAG, "I/O Dispose") }
196201
.doOnEvent { t1, t2 -> Logger.e(TAG, "I/O t1 $t1, t2 $t2") }
197-
.doOnSubscribe { activity.postForUI(20L to TimeUnit.MILLISECONDS) { gatt.disconnect().subscribe() } }
202+
.doOnSubscribe { activity.postForUI(10L to TimeUnit.MILLISECONDS) { gatt.disconnect().subscribe() } }
198203
}
199204
.doOnError { Logger.e(TAG, "Failed, reason :$it") }
200205
.timeout(20L, TimeUnit.SECONDS)

dev-app/src/androidTest/java/com/vincentmasselis/devapp/Utils.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import com.vincentmasselis.rxbluetoothkotlin.internal.toObservable
88
import io.reactivex.Observable
99
import java.util.*
1010

11-
fun bluetoothPreconditions(context: Context) {
12-
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
11+
fun bluetoothPreconditions(activity: TestActivity) {
12+
activity.setMessage("Disabling BLE")
13+
val bluetoothManager = activity.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
1314
bluetoothManager.adapter.disable()
1415
Thread.sleep(1000)
1516
bluetoothManager.adapter.enable()
1617
IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
17-
.toObservable(context)
18+
.toObservable(activity)
1819
.map { (_, intent) -> intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) }
1920
.startWith(Observable.fromCallable {
2021
if (bluetoothManager.adapter.isEnabled) BluetoothAdapter.STATE_ON
@@ -23,7 +24,10 @@ fun bluetoothPreconditions(context: Context) {
2324
.filter { it == BluetoothAdapter.STATE_ON }
2425
.blockingFirst()
2526

26-
Thread.sleep(200)
27+
28+
activity.setMessage("Enabling BLE")
29+
30+
Thread.sleep(1000)
2731
}
2832

2933
val BATTERY_CHARACTERISTIC: UUID = UUID.fromString("00002A19-0000-1000-8000-00805F9B34FB")

rxbluetoothkotlin-core/build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ apply plugin: 'maven-publish'
66

77

88
android {
9-
compileSdkVersion 28
9+
compileSdkVersion 29
1010
buildToolsVersion "28.0.3"
1111

1212
defaultConfig {
1313
minSdkVersion 18
14-
targetSdkVersion 28
14+
targetSdkVersion 29
1515
versionCode versionCode
1616
versionName libVersion
1717
}
@@ -33,7 +33,7 @@ dokka {
3333
dependencies {
3434
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
3535
implementation 'androidx.appcompat:appcompat:1.1.0'
36-
implementation "io.reactivex.rxjava2:rxjava:2.2.11"
36+
implementation "io.reactivex.rxjava2:rxjava:2.2.13"
3737
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
3838
implementation 'no.nordicsemi.android.support.v18:scanner:1.4.0'
3939
}

rxbluetoothkotlin-core/src/main/kotlin/com/vincentmasselis/rxbluetoothkotlin/BluetoothDevice+rx.kt

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ private const val TAG = "BluetoothDevice+rx"
1818
* listen the [io.reactivex.MaybeObserver.onSuccess] event from the [Maybe] returned by
1919
* [whenConnectionIsReady] method.
2020
*
21-
* @param autoConnect similar to "autoConnect" from the [BluetoothDevice.connectGatt] method. Use it wisely.
2221
* @param logger Set a [logger] to log every event which occurs from the BLE API (connections, writes, notifications, MTU, missing permissions, etc...).
23-
* @param rxGattConstructor Defaults uses a [RxBluetoothGattImpl] instance but you can fill you own. It can be useful if you want to add some business logic between the default
24-
* [RxBluetoothGatt] and the system.
25-
* @param callbackConstructor Defaults uses a [RxBluetoothGattCallbackImpl] instance but you can fill you own. It can be useful if you want to add some business logic between the default
26-
* [RxBluetoothGatt.Callback] and the system.
22+
* @param rxGattBuilder Defaults uses a [RxBluetoothGattImpl] instance but you can fill you own. It can be useful if you want to add some business logic between the default
23+
* [RxBluetoothGatt] implementation and the system.
24+
* @param connectGattWrapper Default calls [BluetoothDevice.connectGatt]. If you want to use an other variant of [BluetoothDevice.connectGatt] regarding to your requirements,
25+
* replace the default implementation by your own.
26+
* @param rxCallbackBuilder Defaults uses a [RxBluetoothGattCallbackImpl] instance but you can fill you own. It can be useful if you want to add some business logic between the
27+
* default [RxBluetoothGatt.Callback] implementation and the system.
2728
*
2829
* @return
2930
* onSuccess with a [BluetoothGatt] when a [BluetoothGatt] instance is returned by the system API.
@@ -35,10 +36,17 @@ private const val TAG = "BluetoothDevice+rx"
3536
*/
3637
@Suppress("UNCHECKED_CAST")
3738
fun <T : RxBluetoothGatt.Callback, E : RxBluetoothGatt> BluetoothDevice.connectTypedRxGatt(
38-
autoConnect: Boolean = false,
3939
logger: Logger? = null,
40-
callbackConstructor: (() -> T) = { RxBluetoothGattCallbackImpl().let { concrete -> logger?.let { CallbackLogger(it, concrete) } ?: concrete } as T },
41-
rxGattConstructor: ((BluetoothGatt, T) -> E) = { gatt, callbacks -> RxBluetoothGattImpl(logger, gatt, callbacks) as E }
40+
rxCallbackBuilder: () -> T = {
41+
RxBluetoothGattCallbackImpl().let { concrete -> logger?.let { CallbackLogger(it, concrete) } ?: concrete } as T
42+
},
43+
connectGattWrapper: (Context, BluetoothGattCallback) -> BluetoothGatt? = { context, callback ->
44+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) connectGatt(context, false, callback, BluetoothDevice.TRANSPORT_LE)
45+
else connectGatt(context, false, callback)
46+
},
47+
rxGattBuilder: (BluetoothGatt, T) -> E = { gatt, callbacks ->
48+
RxBluetoothGattImpl(logger, gatt, callbacks) as E
49+
}
4250
): Single<E> = Single
4351
.fromCallable {
4452

@@ -48,32 +56,46 @@ fun <T : RxBluetoothGatt.Callback, E : RxBluetoothGatt> BluetoothDevice.connectT
4856
}
4957

5058
val btState =
51-
if ((ContextHolder.context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter.isEnabled) BluetoothAdapter.STATE_ON else BluetoothAdapter.STATE_OFF
59+
if ((ContextHolder.context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter.isEnabled)
60+
BluetoothAdapter.STATE_ON
61+
else
62+
BluetoothAdapter.STATE_OFF
5263

5364
if (btState == BluetoothAdapter.STATE_OFF) {
5465
logger?.v(TAG, "Bluetooth is off")
5566
throw BluetoothIsTurnedOff()
5667
}
5768

58-
val callbacks = callbackConstructor()
69+
val callbacks = rxCallbackBuilder()
5970

60-
logger?.v(TAG, "connectGatt with autoConnect $autoConnect")
61-
val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) connectGatt(ContextHolder.context, autoConnect, callbacks, BluetoothDevice.TRANSPORT_LE)
62-
else connectGatt(ContextHolder.context, autoConnect, callbacks)
71+
val gatt = connectGattWrapper(ContextHolder.context, callbacks.source)
6372

6473
if (gatt == null) {
6574
logger?.v(TAG, "connectGatt method returned null")
6675
throw NullBluetoothGatt()
6776
}
6877

69-
return@fromCallable rxGattConstructor(gatt, callbacks)
78+
return@fromCallable rxGattBuilder(gatt, callbacks)
7079
}
7180
.subscribeOn(AndroidSchedulers.mainThread())
7281

7382
/** @see connectTypedRxGatt */
7483
fun BluetoothDevice.connectRxGatt(
75-
autoConnect: Boolean = false,
7684
logger: Logger? = null,
77-
callbackConstructor: (() -> RxBluetoothGatt.Callback) = { RxBluetoothGattCallbackImpl().let { concrete -> logger?.let { CallbackLogger(it, concrete) } ?: concrete } },
78-
rxGattConstructor: ((BluetoothGatt, RxBluetoothGatt.Callback) -> RxBluetoothGatt) = { gatt, callbacks -> RxBluetoothGattImpl(logger, gatt, callbacks) }
79-
) = connectTypedRxGatt(autoConnect, logger, callbackConstructor, rxGattConstructor)
85+
rxCallbackBuilder: (() -> RxBluetoothGatt.Callback) = {
86+
RxBluetoothGattCallbackImpl().let { concrete -> logger?.let { CallbackLogger(it, concrete) } ?: concrete }
87+
},
88+
connectGattWrapper: (Context, BluetoothGattCallback) -> BluetoothGatt? = { context, callback ->
89+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) connectGatt(context, false, callback, BluetoothDevice.TRANSPORT_LE)
90+
else connectGatt(context, false, callback)
91+
},
92+
rxGattBuilder: ((BluetoothGatt, RxBluetoothGatt.Callback) -> RxBluetoothGatt) = { gatt, callbacks ->
93+
RxBluetoothGattImpl(logger, gatt, callbacks)
94+
}
95+
) =
96+
connectTypedRxGatt(
97+
logger,
98+
rxCallbackBuilder,
99+
connectGattWrapper,
100+
rxGattBuilder
101+
)

0 commit comments

Comments
 (0)