-
Notifications
You must be signed in to change notification settings - Fork 670
/
BluetoothSerial.java
490 lines (387 loc) · 17.7 KB
/
BluetoothSerial.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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
package com.megster.cordova;
import android.Manifest;
import android.content.pm.PackageManager;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Message;
import android.provider.Settings;
import android.util.Log;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.PluginResult;
import org.apache.cordova.LOG;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Set;
/**
* PhoneGap Plugin for Serial Communication over Bluetooth
*/
public class BluetoothSerial extends CordovaPlugin {
// actions
private static final String LIST = "list";
private static final String CONNECT = "connect";
private static final String CONNECT_INSECURE = "connectInsecure";
private static final String DISCONNECT = "disconnect";
private static final String WRITE = "write";
private static final String AVAILABLE = "available";
private static final String READ = "read";
private static final String READ_UNTIL = "readUntil";
private static final String SUBSCRIBE = "subscribe";
private static final String UNSUBSCRIBE = "unsubscribe";
private static final String SUBSCRIBE_RAW = "subscribeRaw";
private static final String UNSUBSCRIBE_RAW = "unsubscribeRaw";
private static final String IS_ENABLED = "isEnabled";
private static final String IS_CONNECTED = "isConnected";
private static final String CLEAR = "clear";
private static final String SETTINGS = "showBluetoothSettings";
private static final String ENABLE = "enable";
private static final String DISCOVER_UNPAIRED = "discoverUnpaired";
private static final String SET_DEVICE_DISCOVERED_LISTENER = "setDeviceDiscoveredListener";
private static final String CLEAR_DEVICE_DISCOVERED_LISTENER = "clearDeviceDiscoveredListener";
private static final String SET_NAME = "setName";
private static final String SET_DISCOVERABLE = "setDiscoverable";
// callbacks
private CallbackContext connectCallback;
private CallbackContext dataAvailableCallback;
private CallbackContext rawDataAvailableCallback;
private CallbackContext enableBluetoothCallback;
private CallbackContext deviceDiscoveredCallback;
private BluetoothAdapter bluetoothAdapter;
private BluetoothSerialService bluetoothSerialService;
// Debugging
private static final String TAG = "BluetoothSerial";
private static final boolean D = true;
// Message types sent from the BluetoothSerialService Handler
public static final int MESSAGE_STATE_CHANGE = 1;
public static final int MESSAGE_READ = 2;
public static final int MESSAGE_WRITE = 3;
public static final int MESSAGE_DEVICE_NAME = 4;
public static final int MESSAGE_TOAST = 5;
public static final int MESSAGE_READ_RAW = 6;
// Key names received from the BluetoothChatService Handler
public static final String DEVICE_NAME = "device_name";
public static final String TOAST = "toast";
StringBuffer buffer = new StringBuffer();
private String delimiter;
private static final int REQUEST_ENABLE_BLUETOOTH = 1;
// Android 23 requires user to explicitly grant permission for location to discover unpaired
private static final String ACCESS_COARSE_LOCATION = Manifest.permission.ACCESS_COARSE_LOCATION;
private static final int CHECK_PERMISSIONS_REQ_CODE = 2;
private CallbackContext permissionCallback;
@Override
public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException {
LOG.d(TAG, "action = " + action);
if (bluetoothAdapter == null) {
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
if (bluetoothSerialService == null) {
bluetoothSerialService = new BluetoothSerialService(mHandler);
}
boolean validAction = true;
if (action.equals(LIST)) {
listBondedDevices(callbackContext);
} else if (action.equals(CONNECT)) {
boolean secure = true;
connect(args, secure, callbackContext);
} else if (action.equals(CONNECT_INSECURE)) {
// see Android docs about Insecure RFCOMM http://goo.gl/1mFjZY
boolean secure = false;
connect(args, secure, callbackContext);
} else if (action.equals(DISCONNECT)) {
connectCallback = null;
bluetoothSerialService.stop();
callbackContext.success();
} else if (action.equals(WRITE)) {
byte[] data = args.getArrayBuffer(0);
bluetoothSerialService.write(data);
callbackContext.success();
} else if (action.equals(AVAILABLE)) {
callbackContext.success(available());
} else if (action.equals(READ)) {
callbackContext.success(read());
} else if (action.equals(READ_UNTIL)) {
String interesting = args.getString(0);
callbackContext.success(readUntil(interesting));
} else if (action.equals(SUBSCRIBE)) {
delimiter = args.getString(0);
dataAvailableCallback = callbackContext;
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
} else if (action.equals(UNSUBSCRIBE)) {
delimiter = null;
// send no result, so Cordova won't hold onto the data available callback anymore
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
dataAvailableCallback.sendPluginResult(result);
dataAvailableCallback = null;
callbackContext.success();
} else if (action.equals(SUBSCRIBE_RAW)) {
rawDataAvailableCallback = callbackContext;
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
} else if (action.equals(UNSUBSCRIBE_RAW)) {
rawDataAvailableCallback = null;
callbackContext.success();
} else if (action.equals(IS_ENABLED)) {
if (bluetoothAdapter.isEnabled()) {
callbackContext.success();
} else {
callbackContext.error("Bluetooth is disabled.");
}
} else if (action.equals(IS_CONNECTED)) {
if (bluetoothSerialService.getState() == BluetoothSerialService.STATE_CONNECTED) {
callbackContext.success();
} else {
callbackContext.error("Not connected.");
}
} else if (action.equals(CLEAR)) {
buffer.setLength(0);
callbackContext.success();
} else if (action.equals(SETTINGS)) {
Intent intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
cordova.getActivity().startActivity(intent);
callbackContext.success();
} else if (action.equals(ENABLE)) {
enableBluetoothCallback = callbackContext;
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
cordova.startActivityForResult(this, intent, REQUEST_ENABLE_BLUETOOTH);
} else if (action.equals(DISCOVER_UNPAIRED)) {
if (cordova.hasPermission(ACCESS_COARSE_LOCATION)) {
discoverUnpairedDevices(callbackContext);
} else {
permissionCallback = callbackContext;
cordova.requestPermission(this, CHECK_PERMISSIONS_REQ_CODE, ACCESS_COARSE_LOCATION);
}
} else if (action.equals(SET_DEVICE_DISCOVERED_LISTENER)) {
this.deviceDiscoveredCallback = callbackContext;
} else if (action.equals(CLEAR_DEVICE_DISCOVERED_LISTENER)) {
this.deviceDiscoveredCallback = null;
} else if (action.equals(SET_NAME)) {
String newName = args.getString(0);
bluetoothAdapter.setName(newName);
callbackContext.success();
} else if (action.equals(SET_DISCOVERABLE)) {
int discoverableDuration = args.getInt(0);
Intent discoverIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, discoverableDuration);
cordova.getActivity().startActivity(discoverIntent);
} else {
validAction = false;
}
return validAction;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_ENABLE_BLUETOOTH) {
if (resultCode == Activity.RESULT_OK) {
Log.d(TAG, "User enabled Bluetooth");
if (enableBluetoothCallback != null) {
enableBluetoothCallback.success();
}
} else {
Log.d(TAG, "User did *NOT* enable Bluetooth");
if (enableBluetoothCallback != null) {
enableBluetoothCallback.error("User did not enable Bluetooth");
}
}
enableBluetoothCallback = null;
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (bluetoothSerialService != null) {
bluetoothSerialService.stop();
}
}
private void listBondedDevices(CallbackContext callbackContext) throws JSONException {
JSONArray deviceList = new JSONArray();
Set<BluetoothDevice> bondedDevices = bluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : bondedDevices) {
deviceList.put(deviceToJSON(device));
}
callbackContext.success(deviceList);
}
private void discoverUnpairedDevices(final CallbackContext callbackContext) throws JSONException {
final CallbackContext ddc = deviceDiscoveredCallback;
final BroadcastReceiver discoverReceiver = new BroadcastReceiver() {
private JSONArray unpairedDevices = new JSONArray();
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
try {
JSONObject o = deviceToJSON(device);
unpairedDevices.put(o);
if (ddc != null) {
PluginResult res = new PluginResult(PluginResult.Status.OK, o);
res.setKeepCallback(true);
ddc.sendPluginResult(res);
}
} catch (JSONException e) {
// This shouldn't happen, log and ignore
Log.e(TAG, "Problem converting device to JSON", e);
}
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
callbackContext.success(unpairedDevices);
cordova.getActivity().unregisterReceiver(this);
}
}
};
Activity activity = cordova.getActivity();
activity.registerReceiver(discoverReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
activity.registerReceiver(discoverReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));
bluetoothAdapter.startDiscovery();
}
private JSONObject deviceToJSON(BluetoothDevice device) throws JSONException {
JSONObject json = new JSONObject();
json.put("name", device.getName());
json.put("address", device.getAddress());
json.put("id", device.getAddress());
if (device.getBluetoothClass() != null) {
json.put("class", device.getBluetoothClass().getDeviceClass());
}
return json;
}
private void connect(CordovaArgs args, boolean secure, CallbackContext callbackContext) throws JSONException {
String macAddress = args.getString(0);
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(macAddress);
if (device != null) {
connectCallback = callbackContext;
bluetoothSerialService.connect(device, secure);
buffer.setLength(0);
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
} else {
callbackContext.error("Could not connect to " + macAddress);
}
}
// The Handler that gets information back from the BluetoothSerialService
// Original code used handler for the because it was talking to the UI.
// Consider replacing with normal callbacks
private final Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_READ:
buffer.append((String)msg.obj);
if (dataAvailableCallback != null) {
sendDataToSubscriber();
}
break;
case MESSAGE_READ_RAW:
if (rawDataAvailableCallback != null) {
byte[] bytes = (byte[]) msg.obj;
sendRawDataToSubscriber(bytes);
}
break;
case MESSAGE_STATE_CHANGE:
if(D) Log.i(TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1);
switch (msg.arg1) {
case BluetoothSerialService.STATE_CONNECTED:
Log.i(TAG, "BluetoothSerialService.STATE_CONNECTED");
notifyConnectionSuccess();
break;
case BluetoothSerialService.STATE_CONNECTING:
Log.i(TAG, "BluetoothSerialService.STATE_CONNECTING");
break;
case BluetoothSerialService.STATE_LISTEN:
Log.i(TAG, "BluetoothSerialService.STATE_LISTEN");
break;
case BluetoothSerialService.STATE_NONE:
Log.i(TAG, "BluetoothSerialService.STATE_NONE");
break;
}
break;
case MESSAGE_WRITE:
// byte[] writeBuf = (byte[]) msg.obj;
// String writeMessage = new String(writeBuf);
// Log.i(TAG, "Wrote: " + writeMessage);
break;
case MESSAGE_DEVICE_NAME:
Log.i(TAG, msg.getData().getString(DEVICE_NAME));
break;
case MESSAGE_TOAST:
String message = msg.getData().getString(TOAST);
notifyConnectionLost(message);
break;
}
}
};
private void notifyConnectionLost(String error) {
if (connectCallback != null) {
connectCallback.error(error);
connectCallback = null;
}
}
private void notifyConnectionSuccess() {
if (connectCallback != null) {
PluginResult result = new PluginResult(PluginResult.Status.OK);
result.setKeepCallback(true);
connectCallback.sendPluginResult(result);
}
}
private void sendRawDataToSubscriber(byte[] data) {
if (data != null && data.length > 0) {
PluginResult result = new PluginResult(PluginResult.Status.OK, data);
result.setKeepCallback(true);
rawDataAvailableCallback.sendPluginResult(result);
}
}
private void sendDataToSubscriber() {
String data = readUntil(delimiter);
if (data != null && data.length() > 0) {
PluginResult result = new PluginResult(PluginResult.Status.OK, data);
result.setKeepCallback(true);
dataAvailableCallback.sendPluginResult(result);
sendDataToSubscriber();
}
}
private int available() {
return buffer.length();
}
private String read() {
int length = buffer.length();
String data = buffer.substring(0, length);
buffer.delete(0, length);
return data;
}
private String readUntil(String c) {
String data = "";
int index = buffer.indexOf(c, 0);
if (index > -1) {
data = buffer.substring(0, index + c.length());
buffer.delete(0, index + c.length());
}
return data;
}
@Override
public void onRequestPermissionResult(int requestCode, String[] permissions,
int[] grantResults) throws JSONException {
for(int result:grantResults) {
if(result == PackageManager.PERMISSION_DENIED) {
LOG.d(TAG, "User *rejected* location permission");
this.permissionCallback.sendPluginResult(new PluginResult(
PluginResult.Status.ERROR,
"Location permission is required to discover unpaired devices.")
);
return;
}
}
switch(requestCode) {
case CHECK_PERMISSIONS_REQ_CODE:
LOG.d(TAG, "User granted location permission");
discoverUnpairedDevices(permissionCallback);
break;
}
}
}