@@ -107,7 +107,7 @@ sealed class ConnectGattResult {
107
107
108
108
<sup >1</sup > _ Suspends until ` STATE_CONNECTED ` or non-` GATT_SUCCESS ` is received._ <br />
109
109
<sup >2</sup > _ Suspends until ` STATE_DISCONNECTED ` or non-` GATT_SUCCESS ` is received._ <br />
110
- <sup >3</sup > _ Throws ` RemoteException ` if underlying ` BluetoothGatt ` call returns ` false ` ._
110
+ <sup >3</sup > _ Throws [ ` RemoteException ` ] if underlying [ ` BluetoothGatt ` ] call returns ` false ` ._
111
111
112
112
### Details
113
113
@@ -117,6 +117,128 @@ function. This extension function acts as a replacement for Android's
117
117
[ ` BluetoothDevice.connectGatt(context: Context, autoConnect: Boolean, callback: BluetoothCallback): BluetoothGatt? ` ]
118
118
method (which relies on a [ ` BluetoothGattCallback ` ] ).
119
119
120
+ ### Prerequisites
121
+
122
+ ** Able** expects that Android Bluetooth Low Energy is supported
123
+ ([ ` BluetoothAdapter.getDefaultAdapter() ` ] returns non-` null ` ) and usage prerequisites
124
+ (e.g. [ bluetooth permissions] ) are satisfied prior to use; failing to do so will result in
125
+ [ ` RemoteException ` ] for most ** Able** methods.
126
+
127
+ ## Structured Concurrency
128
+
129
+ Kotlin Coroutines ` 0.26.0 ` introduced [ structured concurrency] .
130
+
131
+ When establishing a connection (e.g. via
132
+ ` BluetoothDevice.connectGatt(context: Context, autoConnect: Boolean): ConnectGattResult ` extension
133
+ function), if the Coroutine is cancelled then the in-flight connection attempt will be cancelled and
134
+ corresponding [ ` BluetoothGatt ` ] will be closed:
135
+
136
+ ``` kotlin
137
+ class ExampleActivity : AppCompatActivity (), CoroutineScope {
138
+
139
+ protected lateinit var job: Job
140
+ override val coroutineContext: CoroutineContext
141
+ get() = job + Dispatchers .Main
142
+
143
+ override fun onCreate (savedInstanceState : Bundle ? ) {
144
+ super .onCreate(savedInstanceState)
145
+ job = Job ()
146
+
147
+ val bluetoothDevice: BluetoothDevice = TODO (" Retrieve a `BluetoothDevice` from a scan." )
148
+
149
+ findViewById<Button >(R .id.connect_button).setOnClickListener {
150
+ launch {
151
+ // If Activity is destroyed during connection attempt, then `result` will contain
152
+ // `ConnectGattResult.Canceled`.
153
+ val result = bluetoothDevice.connectGatt(this @ExampleActivity, autoConnect = false )
154
+
155
+ // ...
156
+ }
157
+ }
158
+ }
159
+
160
+ override fun onDestroy () {
161
+ super .onDestroy()
162
+ job.cancel()
163
+ }
164
+ }
165
+ ```
166
+
167
+ In the above example, the connection process is tied to the Activity lifecycle. If the Activity is
168
+ destroyed (e.g. due to device rotation or navigating away from the Activity) then the connection
169
+ attempt will be cancelled. If it is desirable that a connection attempt proceed beyond the Activity
170
+ lifecycle then the [ ` launch ` ] can be executed in the global scope via ` GlobalScope.launch ` , in which
171
+ case the [ ` Job ` ] that [ ` launch ` ] returns can be used to manually cancel the connection process (when
172
+ desired).
173
+
174
+ Alternatively, if (for example) an app has an Activity specifically designed to handle the
175
+ connection process, then Android Architecture Component's [ ` ViewModel ` ] can be scoped (via
176
+ ` CoroutineScope ` interface) allowing connection attempts to be tied to the ` ViewModel ` 's lifecycle:
177
+
178
+ ``` kotlin
179
+ class ExampleViewModel (application : Application ) : AndroidViewModel(application), CoroutineScope {
180
+
181
+ private val job = Job ()
182
+ override val coroutineContext: CoroutineContext
183
+ get() = job + Dispatchers .Main
184
+
185
+ fun connect (bluetoothDevice : BluetoothDevice ) {
186
+ launch {
187
+ // If ViewModel is destroyed during connection attempt, then `result` will contain
188
+ // `ConnectGattResult.Canceled`.
189
+ val result = bluetoothDevice.connectGatt(getApplication(), autoConnect = false )
190
+
191
+ // ...
192
+ }
193
+ }
194
+
195
+ override fun onCleared () {
196
+ job.cancel()
197
+ }
198
+ }
199
+ ```
200
+
201
+ Similar to the connection process, after a connection has been established, if a Coroutine is
202
+ cancelled then any ` Gatt ` operation executing within the Coroutine will be cancelled.
203
+
204
+ However, unlike the ` connectGatt ` cancellation handling, an established ` Gatt ` connection will
205
+ ** not** automatically disconnect or close when the Coroutine executing a ` Gatt ` operation is
206
+ canceled. Special care must be taken to ensure that Bluetooth Low Energy connections are properly
207
+ closed when no longer needed, for example:
208
+
209
+ ``` kotlin
210
+ val gatt: Gatt = TODO (" Acquire Gatt via `BluetoothDevice.connectGatt` extension function." )
211
+
212
+ launch {
213
+ try {
214
+ gatt.discoverServices()
215
+ // todo: Assign desired characteristic to `characteristic` variable.
216
+ val value = gatt.readCharacteristic(characteristic).value
217
+ gatt.disconnect()
218
+ } finally {
219
+ gatt.close()
220
+ }
221
+ }
222
+ ```
223
+
224
+ The ` Gatt ` interface adheres to [ ` Closeable ` ] which can simplify the above example by using [ ` use ` ] :
225
+
226
+ ``` kotlin
227
+ val gatt: Gatt = TODO (" Acquire Gatt via `BluetoothDevice.connectGatt` extension function." )
228
+
229
+ gatt.use { // Will close `gatt` if any failures or cancellation occurs.
230
+ gatt.discoverServices()
231
+ // todo: Assign desired characteristic to `characteristic` variable.
232
+ val value = gatt.readCharacteristic(characteristic).value
233
+ gatt.disconnect()
234
+ }
235
+ ```
236
+
237
+ It may be desirable to manage Bluetooth Low Energy connections entirely manually. In which case,
238
+ Coroutine [ ` GlobalScope ` ] can be used for both the connection process and operations performed while
239
+ connected. In which case, the returned ` Gatt ` object (after successful connection) can be stored and
240
+ later used to disconnect ** and** close the underlying ` BluetoothGatt ` .
241
+
120
242
## Example
121
243
122
244
The following code snippet makes a connection to a [ ` BluetoothDevice ` ] , reads a characteristic,
@@ -179,6 +301,16 @@ limitations under the License.
179
301
[ `BluetoothGattCallback` ] : https://developer.android.com/reference/android/bluetooth/BluetoothGattCallback.html
180
302
[ `BluetoothDevice` ] : https://developer.android.com/reference/android/bluetooth/BluetoothDevice.html
181
303
[ `BluetoothDevice.connectGatt(context: Context, autoConnect: Boolean, callback: BluetoothCallback): BluetoothGatt?` ] : https://developer.android.com/reference/android/bluetooth/BluetoothDevice.html#connectGatt(android.content.Context,%20boolean,%20android.bluetooth.BluetoothGattCallback)
304
+ [ `RemoteException` ] : https://developer.android.com/reference/android/os/RemoteException
182
305
[ Coroutines ] : https://kotlinlang.org/docs/reference/coroutines.html
306
+ [ `Closeable` ] : https://docs.oracle.com/javase/7/docs/api/java/io/Closeable.html
307
+ [ `GlobalScope` ] : https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-global-scope/
183
308
[ suspension functions ] : https://kotlinlang.org/docs/reference/coroutines.html#suspending-functions
309
+ [ structured concurrency ] : https://medium.com/@elizarov/structured-concurrency-722d765aa952
310
+ [ bluetooth permissions ] : https://developer.android.com/guide/topics/connectivity/bluetooth#Permissions
311
+ [ `Job` ] : https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-job/index.html
312
+ [ `launch` ] : https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/launch.html
313
+ [ `ViewModel` ] : https://developer.android.com/topic/libraries/architecture/viewmodel
314
+ [ `use` ] : https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/use.html
315
+ [ `BluetoothAdapter.getDefaultAdapter()` ] : https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#getDefaultAdapter()
184
316
[ Recipes ] : documentation/RECIPES.md
0 commit comments