描述一段背景:前年我找工作时,总碰到一个问题。
面试官问:“你会蓝牙开发吗?”。
我说:“不会”。
面试官答:“那,很抱歉。我们商量了一下,觉得你不适合这个岗位。”
于是我就走了,心里想:“就应为一个蓝牙通讯技术不会,就把我给cut了,这面试官好有想象力。”
我一个同学,都没做过编程,我半年时间都带到android开发道上了。我仅仅蓝牙没做过,研究蓝牙无非就是三两天的时间,难吗?
于是,我周末窝在家里,查阅了大量资料,实践和总结,研究透了蓝牙技术。
有些同学可能会说,蓝牙简单,无非就是扫描设备,配对,和socket通讯。
没错,是这些,但是还有很多坑你不知道,还有很多奇葩代码。废话少说,一起分享吧。
想使用蓝牙呢,首先得看手机是否支持,有些低配手机,可能就没有内置蓝牙模块。当然,一般都会有,我们可以得到唯一的蓝牙适配器,进行其他操作。
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
然后,我们可做n多的事情。不要着急,后面会附上我的项目源码,大牛见笑了。
/** * 开启蓝牙 * * @param activity 上下文 * @return 是否开启成功 */ public static boolean openBluetooth(Activity activity) { //确认开启蓝牙 if (!getInstance().isEnabled()) { //=默认120秒============================================================== //使蓝牙设备可见,方便配对 //Intent in = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); //in.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); //activity.startActivityForResult(in,Activity.RESULT_OK); //=1============================================================= //请求用户开启,需要提示 //Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); //startActivityForResult(intent, RESULT_FIRST_USER); //=2============================================================= //程序直接开启,不经过提示 getInstance().enable(); } //T.showLong(context, "已经开启蓝牙"); return getInstance().isEnabled(); } //关闭蓝牙 public static boolean closeBluetooth() { return getInstance().disable(); } /** * 扫描已经配对的设备 * * @return */ public static ArrayList<BluetoothDevice> scanPairs() { ArrayList<BluetoothDevice> list = null; Set<BluetoothDevice> deviceSet = getInstance().getBondedDevices(); if (deviceSet.size() > 0) { //存在已经配对过的蓝牙设备 list = new ArrayList<>(); list.addAll(deviceSet); } return list; } //开始扫描 public static void scan() { getInstance().startDiscovery(); } //取消扫描 public static void cancelScan() { if (getInstance().isDiscovering()) getInstance().cancelDiscovery(); } //蓝牙配对 @TargetApi(Build.VERSION_CODES.KITKAT) public static boolean createBond(BluetoothDevice device) { return bond(device, "createBond"); /*if (device.createBond()) { return device.setPairingConfirmation(true); } return false;*/ } //解除配对 public static boolean removeBond(BluetoothDevice device) { return bond(device, "removeBond"); } @TargetApi(Build.VERSION_CODES.KITKAT) private static boolean bond(BluetoothDevice device, String methodName) { Boolean returnValue = false; if (device != null && device.getBondState() == BluetoothDevice.BOND_NONE) { try { device.setPairingConfirmation(false); cancelPairingUserInput(device); Method removeBondMethod = BluetoothDevice.class.getMethod(methodName); returnValue = (Boolean) removeBondMethod.invoke(device); } catch (Exception e) { e.printStackTrace(); } } return returnValue; } //取消配对 public static boolean cancelBondProcess(BluetoothDevice device) { try { Method cancelBondMethod = BluetoothDevice.class.getMethod("cancelBondProcess"); Boolean returnValue = (Boolean) cancelBondMethod.invoke(device); return returnValue.booleanValue(); } catch (Exception e) { e.printStackTrace(); } return false; } //取消用户输入 public static boolean cancelPairingUserInput(BluetoothDevice device) { try { Method cancelPairingUserInputMethod = BluetoothDevice.class.getMethod("cancelPairingUserInput"); Boolean returnValue = (Boolean) cancelPairingUserInputMethod.invoke(device); return returnValue.booleanValue(); } catch (Exception e) { e.printStackTrace(); } return false; }
通过上面这段代码,应道知道了吧,获取到适配器后,可以得到当前手机已经配对的设备。同时可以开启扫描(这个时间大概是12秒,异步的),扫描到设备和扫描完成系统会发广播。故广播接收代码:
//注册蓝牙接收广播 if (!hasRegister) { hasRegister = true; //扫描结束广播 IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); //找到设备广播 filter.addAction(BluetoothDevice.ACTION_FOUND); filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); filter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED); filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST); registerReceiver(mMyReceiver, filter); }
private class MyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BluetoothDevice.ACTION_FOUND.equals(action)) { //搜索到新设备 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); //搜索没有配过对的蓝牙设备 if (device.getBondState() != BluetoothDevice.BOND_BONDED) { mListData.add(device); dataAdapter.refreshData(mListData); } else { T.showLong(TwoActivity.this, device.getName() + '\n' + device.getAddress() + " > 已发现"); } } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { //搜索结束 if (mListData.size() == 0) { T.showLong(TwoActivity.this, "没有发现任何蓝牙设备"); } progressDialog.dismiss(); scan.setText("重新扫描"); } else if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(action)) { if (TwoActivity.this.position != -1) { final BluetoothDevice device = mListData.get(TwoActivity.this.position); com.dk.bluetooth.tools.T.showLong(TwoActivity.this, device + " 配对成功"); EventBus.getDefault().post(new com.dk.bluetooth.tools.MyEvent()); } } } }
以上都不是重点,核心在线程通讯。首先得了解socket,java有这个,蓝牙里面也有,是BluetoothServerSocket和BluetoothSocket两个,一个是服务器端的,一个是客户端的。不了解的朋友建议先去百度java socket用法,超级简单。
通讯需要建立信道,BluetoothServerSocket需要先启动,监听当前设备上的某UUID位置上的设备(阻塞到在此处),就跟windows的端口意思是一样的。然后BluetoothSocket再启动,根据对方的mac地址和对方监听的UUID位置,启动连接(也阻塞了),直到连上服务器了,就返回。服务器也一样,直到有人来连接了,就返回。都会返回一个BluetoothSocket,然后从这个socket里面获取input和output流。服务器端了input流是客户端的output流,另外一半也一样。剩下的收发消息就是流的读写了,简单吧。
下面贴出我的代码,应为服务器端和客户端启动的方法不一样,我分成了两个线程,由于读写的功能一样,我就共用了一套读写线程。根据这个思路看我的代码。
/** * 初始化及启动蓝牙socket * * @param handler UI消息传递对象 * @param securityType 连接的安全模式 * @param serverOrClient 客户端或服务端 * @param bluetoothDevice 服务器端设备 */ public BluetoothChatService(Handler handler, SecurityType securityType, ServerOrClient serverOrClient, BluetoothDevice bluetoothDevice) { if (securityType != null) this.mSecurityType = securityType; if (serverOrClient != null) this.mServerOrClient = serverOrClient; if (bluetoothDevice != null) this.mBluetoothDevice = bluetoothDevice; mAdapter = BluetoothAdapter.getDefaultAdapter(); mHandler = handler; start(); } /** * 多线程同步修改状态标识 * * @param state */ private synchronized void setState(int state) { mState = state; mHandler.obtainMessage(MESSAGE_TOAST_STATE_CHANGE, state, -1, null).sendToTarget(); } /** * 多线程同步读取状态标识 */ public synchronized int getState() { return mState; } /** * 启动服务 */ public void start() { start(null, null, null); } /** * 启动服务 * * @param securityType 连接的安全模式 * @param serverOrClient 客户端或服务端 * @param bluetoothDevice 服务器端设备 */ public void start(SecurityType securityType, ServerOrClient serverOrClient, BluetoothDevice bluetoothDevice) { if (securityType != null) this.mSecurityType = securityType; if (this.mSecurityType == null) { if (DEBUG) Log.e(TAG, "mSecurityType cannot be null"); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "mSecurityType cannot be null").sendToTarget(); return; } if (serverOrClient != null) this.mServerOrClient = serverOrClient; if (this.mServerOrClient == null) { if (DEBUG) Log.e(TAG, "mServerOrClient cannot be null"); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "mServerOrClient cannot be null").sendToTarget(); return; } if (bluetoothDevice != null) this.mBluetoothDevice = bluetoothDevice; if (this.mBluetoothDevice == null) { if (DEBUG) Log.e(TAG, "mBluetoothDevice cannot be null"); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "mBluetoothDevice cannot be null").sendToTarget(); return; } if (mState == STATE_NONE) { stop(); if (this.mServerOrClient == ServerOrClient.SERVER) { if (mServerConnectThread == null) { mServerConnectThread = new ServerConnectThread(this.mSecurityType); mServerConnectThread.start(); } } else if (this.mServerOrClient == ServerOrClient.CLIENT) { if (mClientConnectThread == null) { mClientConnectThread = new ClientConnectThread(this.mSecurityType); mClientConnectThread.start(); } } setState(STATE_LISTEN); } } /** * 停止服务 */ public synchronized void stop() { try { if (mReadWriteThread != null) { mReadWriteThread.cancel(); mReadWriteThread = null; } if (mServerConnectThread != null) { mServerConnectThread.cancel(); mServerConnectThread = null; } if (mClientConnectThread != null) { mClientConnectThread.cancel(); mClientConnectThread = null; } } catch (Exception e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "BluetoothChatService -> stop() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "BluetoothChatService -> stop() -> :failed").sendToTarget(); mReadWriteThread = null; mServerConnectThread = null; mClientConnectThread = null; } finally { setState(STATE_NONE); System.gc(); } } /** * 发送消息 * * @param out 数据参数 */ public void write(String out) { if (TextUtils.isEmpty(out)) { if (DEBUG) Log.e(TAG, "please write something now"); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "BluetoothChatService -> write() -> :failed").sendToTarget(); return; } ReadWriteThread r; synchronized (this) { if (mState != STATE_CONNECTED) return; r = mReadWriteThread; } r.write(out); } /** * 服务器端连接线程 */ @SuppressLint("NewApi") private class ServerConnectThread extends Thread { private BluetoothServerSocket mmServerSocket; private BluetoothSocket mmSocket = null; public ServerConnectThread(SecurityType securityType) { setName("ServerConnectionThread:" + securityType.getValue()); BluetoothServerSocket tmp = null; try { if (securityType == SecurityType.SECURE) { tmp = mAdapter.listenUsingRfcommWithServiceRecord(SecurityType.SECURE.getValue(), MY_UUID_SECURE); } else if (securityType == SecurityType.INSECURE) { tmp = mAdapter.listenUsingInsecureRfcommWithServiceRecord(SecurityType.INSECURE.getValue(), MY_UUID_INSECURE); } if (tmp != null) mmServerSocket = tmp; } catch (IOException e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "ServerConnectThread -> ServerConnectThread() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "ServerConnectThread -> ServerConnectThread() -> :failed").sendToTarget(); mmServerSocket = null; BluetoothChatService.this.stop(); } } public void run() { try { // 正在连接 setState(STATE_CONNECTING); //accept() 阻塞式的方法,群聊时,需要循环accept接收客户端 mmSocket = mmServerSocket.accept(); connected(mmSocket); } catch (Exception e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "ServerConnectThread -> run() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "ServerConnectThread -> run() -> :failed").sendToTarget(); BluetoothChatService.this.stop(); } } public void cancel() { try { if (mmSocket != null) { mmSocket.close(); mmSocket = null; } if (mmServerSocket != null) { mmServerSocket.close(); mmServerSocket = null; } } catch (IOException e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "ServerConnectThread -> cancel() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "ServerConnectThread -> cancel() -> :failed").sendToTarget(); mmSocket = null; mmServerSocket = null; BluetoothChatService.this.stop(); } } } // 客户端连接线程 private class ClientConnectThread extends Thread { private BluetoothSocket mmSocket; public ClientConnectThread(SecurityType securityType) { setName("ClientConnectThread:" + securityType.getValue()); BluetoothSocket tmp = null; try { if (securityType == SecurityType.SECURE) { tmp = mBluetoothDevice.createRfcommSocketToServiceRecord(MY_UUID_SECURE); //Method m = mBluetoothDevice.getClass().getMethod("createRfcommSocket", int.class); //tmp = (BluetoothSocket) m.invoke(mBluetoothDevice, 1); } else if (securityType == SecurityType.INSECURE) { tmp = mBluetoothDevice.createInsecureRfcommSocketToServiceRecord(MY_UUID_INSECURE); //Method m = mBluetoothDevice.getClass().getMethod("createRfcommSocket", int.class); //tmp = (BluetoothSocket) m.invoke(mBluetoothDevice, 1); } if (tmp != null) mmSocket = tmp; } catch (Exception e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "ClientConnectThread -> ClientConnectThread() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "ClientConnectThread -> ClientConnectThread() -> :failed").sendToTarget(); mmSocket = null; BluetoothChatService.this.stop(); } } public void run() { try { setState(STATE_CONNECTING); mmSocket.connect(); connected(mmSocket); } catch (IOException e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "ClientConnectThread -> run() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "ClientConnectThread -> run() -> :failed").sendToTarget(); BluetoothChatService.this.stop(); } } public void cancel() { try { if (mmSocket != null && mmSocket.isConnected()) { mmSocket.close(); } mmSocket = null; } catch (IOException e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "ClientConnectThread -> cancel() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "ClientConnectThread -> cancel() -> :failed").sendToTarget(); mmSocket = null; BluetoothChatService.this.stop(); } } } /** * 以获取socket,建立数据流线程 * * @param socket */ private synchronized void connected(BluetoothSocket socket) { if (mReadWriteThread != null) { mReadWriteThread.cancel(); mReadWriteThread = null; } mReadWriteThread = new ReadWriteThread(socket); mReadWriteThread.start(); } /** * 连接成功线程,可进行读写操作 */ private class ReadWriteThread extends Thread { private BluetoothSocket mmSocket; private DataInputStream mmInStream; private DataOutputStream mmOutStream; private boolean isRunning = true; public ReadWriteThread(BluetoothSocket socket) { mmSocket = socket; try { mmInStream = new DataInputStream(mmSocket.getInputStream()); mmOutStream = new DataOutputStream(mmSocket.getOutputStream()); // 连接建立成功 setState(STATE_CONNECTED); } catch (IOException e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "ReadWriteThread -> ReadWriteThread() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "ReadWriteThread -> ReadWriteThread() -> :failed").sendToTarget(); mmOutStream = null; mmInStream = null; BluetoothChatService.this.stop(); } } public void run() { byte[] buffer = new byte[1024]; int len; while (isRunning) { try { //readUTF(),read(buffer) 都是阻塞式的方法 //如果这儿用readUTF,那么写的地方得用writeUTF。对应 String receive_str = mmInStream.readUTF(); if (!TextUtils.isEmpty(receive_str)) mHandler.obtainMessage(MESSAGE_RECEIVE, -1, -1, receive_str).sendToTarget(); // len = mmInStream.read(buffer); // if(len > 0){ // String receive_str = new String(buffer,0,len); // if (!TextUtils.isEmpty(receive_str)) // mHandler.obtainMessage(MESSAGE_RECEIVE, -1, -1, receive_str).sendToTarget(); // } } catch (IOException e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "ReadWriteThread -> run() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "ReadWriteThread -> run() -> :failed").sendToTarget(); BluetoothChatService.this.stop(); } } } public void write(String str) { try { mmOutStream.writeUTF(str); mmOutStream.flush(); mHandler.obtainMessage(MESSAGE_TOAST_SEND, -1, -1, str).sendToTarget(); } catch (IOException e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "ReadWriteThread -> write() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "ReadWriteThread -> write() -> :failed").sendToTarget(); BluetoothChatService.this.stop(); } } public void cancel() { try { isRunning = false; if (mmInStream != null) { mmInStream.close(); mmInStream = null; } if (mmOutStream != null) { mmOutStream.close(); mmOutStream = null; } } catch (IOException e) { e.printStackTrace(); if (DEBUG) Log.e(TAG, "ReadWriteThread -> cancel() -> :failed " + e.getMessage()); mHandler.obtainMessage(MESSAGE_TOAST_ERROR, -1, -1, "ReadWriteThread -> cancel() -> :failed").sendToTarget(); mmInStream = null; mmOutStream = null; BluetoothChatService.this.stop(); } } }
sorry,不要蛋疼,不要骂娘,代码确实有这么多。以上代码使我在前辈的基础上优化改进了的。可以作为一个公共的蓝牙通讯工具类使用。
下面贴出我的项目功能,是一个聊天程序,只能单聊。我不明太网上有很多demo声称能群聊怎么实现的,据目前分析,服务器端和客户端的管道流是一一对应的,不是广播模式。如果能群聊,会在服务器端创建一个输入输出流管理的集合吧,服务器端没收到一条消息,在=再循环输出流集合,往各个客户端都发送消息。这样一来,我上面的这段代码不够用了。懒得改,故没有做群聊。
先贴图:
程序中,socket的连接方式有安全连接和不安全连接,我一直没有搞懂区别
在两个手机都连接了wifi的情况加,再使用我这种蓝牙通讯方式通讯时,io流连接上后会自动断开,很奇怪。查资料说蓝牙通讯的波段频率与路由器的冲突了。没辙,故在启动程序的时候关闭了wifi,下下策,望大家提供思路。
最后附上源码。有遗漏和错误,望大家指点。