/
RxBluetooth.java
370 lines (343 loc) · 15.4 KB
/
RxBluetooth.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
package com.andydennie.rxbluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import com.plackemacher.rxreceiver.RxReceiver;
import java.lang.reflect.Method;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Func1;
public final class RxBluetooth {
private static final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
/**
* Enables Bluetooth (asynchronously) and returns an Observable that emits updates to the Bluetooth state. The
* emitted values will be one of {@link BluetoothAdapter#STATE_OFF}, {@link BluetoothAdapter#STATE_ON},
* or {@link BluetoothAdapter#STATE_TURNING_ON}. The stream will terminate when the state changes to either
* STATE_ON or STATE_OFF.
* <p/>
* The returned Observable will emit an error if Bluetooth is already enabled or cannot be enabled.
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<Integer> enableBluetooth(final Context context) {
return Observable.create(new Observable.OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> subscriber) {
if (bluetoothAdapter.isEnabled()) {
subscriber.onError(new IllegalStateException("bluetooth is already enabled"));
}
observeBluetoothState(context)
.takeUntil(new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer state) {
return state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_OFF;
}
})
.subscribe(subscriber);
if (!bluetoothAdapter.enable()) {
subscriber.onError(new IllegalStateException("cannot enable bluetooth (maybe airplane mode " +
"is on?"));
}
}
});
}
/**
* Disables Bluetooth (asynchronously) and returns an Observable that emits updates to the Bluetooth state. The
* emitted values will be one of {@link BluetoothAdapter#STATE_OFF}, {@link BluetoothAdapter#STATE_ON}, or
* {@link BluetoothAdapter#STATE_TURNING_OFF}. The stream will terminate when the state changes to either
* STATE_ON or STATE_OFF.
* <p/>
* The returned Observable will emit an error if Bluetooth is already disabled or cannot be disabled.
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<Integer> disableBluetooth(final Context context) {
return Observable.create(new Observable.OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> subscriber) {
if (!bluetoothAdapter.isEnabled()) {
subscriber.onError(new IllegalStateException("bluetooth is already disabled"));
}
observeBluetoothState(context)
.takeUntil(new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer state) {
return state == BluetoothAdapter.STATE_ON || state == BluetoothAdapter.STATE_OFF;
}
}).subscribe(subscriber);
if (!bluetoothAdapter.disable()) {
subscriber.onError(new IllegalStateException("cannot disable bluetooth"));
}
}
});
}
/**
* Returns an Observable that emits changes to the Bluetooth state.
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<Integer> observeBluetoothState(@NonNull Context context) {
return RxReceiver.fromBroadcast(context, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
.map(new Func1<Intent, Integer>() {
@Override
public Integer call(Intent intent) {
return intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0);
}
});
}
/**
* Returns an Observable that emits Bluetooth state changes to BluetoothAdapter.STATE_ON and STATE_OFF,
* (only), filtering out redundant changes (i.e. STATE_ON->STATE_ON or STATE_OFF->STATE_OFF)
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<Integer> observeBluetoothStateOnOff(@NonNull Context context) {
return observeBluetoothState(context)
.filter(new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer state) {
return state == BluetoothAdapter.STATE_OFF || state == BluetoothAdapter.STATE_ON;
}
})
.distinctUntilChanged();
}
/**
* Returns an Observable that emits a BluetoothDevice whenever one is connected.
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<BluetoothDevice> observeBluetoothConnections(@NonNull Context context) {
return RxReceiver.fromBroadcast(context, new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED))
.map(new Func1<Intent, BluetoothDevice>() {
@Override
public BluetoothDevice call(Intent intent) {
return intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
}
});
}
/**
* Returns an Observable that emits a BluetoothDevice whenever one is disconnected.
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<BluetoothDevice> observeBluetoothDisconnections(@NonNull Context context) {
return RxReceiver.fromBroadcast(context, new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED))
.map(new Func1<Intent, BluetoothDevice>() {
@Override
public BluetoothDevice call(Intent intent) {
return intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
}
});
}
/**
* Starts the Bluetooth device discovery process and returns an Observable that emits
* BluetoothDevices until discovery ends, and then completes.
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<BluetoothDevice> discoverDevices(@NonNull final Context context) {
return Observable.create(new Observable.OnSubscribe<BluetoothDevice>() {
@Override
public void call(final Subscriber<? super BluetoothDevice> subscriber) {
observeDiscoveredDevices(context)
.takeUntil(observeDeviceDiscoveryFinish(context))
.subscribe(subscriber);
bluetoothAdapter.startDiscovery();
}
});
}
/**
* Returns an Observable that emits the first BluetoothAdapter.ACTION_DISCOVERY_STARTED that is broadcast after
* subscription, followed by the ensuing BluetoothAdapter.ACTION_DISCOVERY_FINISHED, and then completes. Note
* that is this observable should be subscribed prior to initiating discovery, otherwise the
* ACTION_DISCOVERY_STARTED broadcast may be missed, causing this Observable to wait in vain for it.
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<String> observeDeviceDiscovery(@NonNull final Context context) {
return observeDeviceDiscoveryStatus(context)
.skipWhile(new Func1<String, Boolean>() {
@Override
public Boolean call(String s) {
return s.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
}
})
.takeUntil(new Func1<String, Boolean>() {
@Override
public Boolean call(String s) {
return s.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
}
});
}
/**
* Returns an Observable that emits the first BluetoothAdapter.ACTION_DISCOVERY_FINISHED that is broadcast
* following its subscrition, and then completes. Note that is this observable should be subscribed prior to
* initiating discovery, otherwise the ACTION_DISCOVERY_FINISHED broadcast may be missed, causing this Observable
* to wait in vain for it.
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<String> observeDeviceDiscoveryFinish(@NonNull final Context context) {
return observeDeviceDiscoveryStatus(context)
.takeFirst(new Func1<String, Boolean>() {
@Override
public Boolean call(String s) {
return s.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
}
});
}
/**
* Returns an Observable that emits BluetoothAdapter.ACTION_DISCOVERY_STARTED and
* BluetoothAdapter.ACTION_DISCOVERY_FINISHED whenever discovery starts and ends. Note that this observable
* emits these actions indefinitely -- the stream does not complete after an
* ACTION_DISCOVERY_FINISHED.
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<String> observeDeviceDiscoveryStatus(@NonNull final Context context) {
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
return RxReceiver.fromBroadcast(context, filter)
.map(new Func1<Intent, String>() {
@Override
public String call(Intent intent) {
return intent.getAction();
}
});
}
/**
* Returns an Observable that emits discovered BluetoothDevices, indefinitely.
*
* @param context an Android Context
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<BluetoothDevice> observeDiscoveredDevices(@NonNull Context context) {
return RxReceiver.fromBroadcast(context, new IntentFilter(BluetoothDevice.ACTION_FOUND))
.map(new Func1<Intent, BluetoothDevice>() {
@Override
public BluetoothDevice call(Intent intent) {
return intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
}
})
.distinct(new Func1<BluetoothDevice, String>() {
@Override
public String call(BluetoothDevice device) {
return device.getAddress();
}
});
}
/**
* Initiates and monitors bonding (pairing) of the specified BluetoothDevice and returns an Observable that emits
* BluetoothDevice.BOND_BONDING, followed by either BluetoothDevice.BOND_BONDED or BluetoothDevice.BOND_NONE, and
* then completes. If the specified BluetoothDevice is already bonded or in the process of bonding, the
* Observer's onError method is invoked.
*
* @param context an Android Context
* @param device the BluetoothDevice
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<Integer> bondDevice(@NonNull final Context context, @NonNull final BluetoothDevice device) {
return Observable.create(new Observable.OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> subscriber) {
if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
subscriber.onError(new IllegalStateException("device is already bonded"));
}
if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
subscriber.onError(new IllegalStateException("device is already in the process of bonding"));
}
observeDeviceBonding(context, device).subscribe(subscriber);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
device.createBond();
} else {
try {
Method method = device.getClass().getMethod("createBond", (Class[]) null);
method.invoke(device, (Object[]) null);
} catch (Exception e) {
subscriber.onError(e);
}
}
}
});
}
/**
* Returns an Observable that emits the first BluetoothDevice.BOND_BONDING that is broadcast after
* subscription, followed by the ensuing BluetoothDevice.BOND_BONDED or BluetoothDevice.BOND_NONE, and then
* completes. Note that is this Observable should be subscribed prior to initiating the bonding process,
* otherwise the BOND_BONDING broadcast may be missed, causing this Observable to wait in vain for it.
*
* @param context an Android Context
* @param deviceToBond the BluetoothDevice whose bonding is to be monitored
* @return the Observable
*/
@CheckResult
@NonNull
public static Observable<Integer> observeDeviceBonding(@NonNull final Context context,
@NonNull final BluetoothDevice deviceToBond) {
return RxReceiver.fromBroadcast(context, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED))
.filter(new Func1<Intent, Boolean>() {
@Override
public Boolean call(Intent intent) {
BluetoothDevice bondingDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
return bondingDevice.equals(deviceToBond);
}
})
.map(new Func1<Intent, Integer>() {
@Override
public Integer call(Intent intent) {
return intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
}
})
.skipWhile(new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer state) {
return state != BluetoothDevice.BOND_BONDING;
}
})
.takeUntil(new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer state) {
return state == BluetoothDevice.BOND_BONDED || state == BluetoothDevice.BOND_NONE;
}
});
}
}