因項目需要做一個Android 的藍牙app來通過手機藍牙傳輸數據以及控制飛行器,在此,我對這段時間里寫的藍牙app的代碼進行知識梳理和出現錯誤的總結。
該應用的Compile Sdk Version 和targetSdkVersion均為26,Min Sdk Version為22,基於Android studio平台開發。
一、聲明藍牙權限
首先,要在新建項目中的AndroidManifest.xml中聲明兩個權限:BLUETOOTH權限和BLUETOOTH_ADMIN權限。其中,BLUETOOTH權限用於請求連接和傳送數據;BLUETOOTH_ADMIN權限用於啟動設備、發現或進行藍牙設置,如果要擁有該權限,必須現擁有BLUETOOTH權限。
其次,因為android 6.0之后采用新的權限機制來保護用戶的隱私,如果我們設置的targetSdkVersion大於或等於23,則需要另外添加ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION權限,否則,可能會出現搜索不到藍牙設備的問題。
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
二、 啟動和關閉藍牙
1.首先,要獲取BluetoothAdapter藍牙適配器的對象,然后檢測設備是否支持藍牙。
BluetoothAdapter blueadapter = BluetoothAdapter.getDefaultAdapter(); //獲取藍牙適配器
if(blueadapter==bull) //表示手機不支持藍牙 return;
2.啟動藍牙功能:isEnable()方法用來檢查藍牙當前狀態,如果方法返回false,則藍牙沒啟動。enable()方法用來打開本地藍牙適配器。
if (!blueadapter.isEnabled()) //判斷本機藍牙是否打開 {//如果沒打開,則打開藍牙 blueadapter.enable(); }
3.使用disable()可以關閉本地藍牙適配器。
三、發現藍牙設備
1.開啟當前藍牙的可見性
Android 設備默認是不能被搜索的,如果想要本機設備可被搜索,可以以BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE動作為startActivity()方法的參數,這個方法會提交一個開啟藍牙可見的請求。默認的情況下,設備在120秒內可以被搜索,也可以自定義一個間隔時間,但是規定的最大值為300秒,0秒則表示設備可以一直被搜索,自定義時間通過EXTRA_DISCOVERABLE_DURATION來定義,代碼如下。
if (blueadapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) //不在可被搜索的范圍 { Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);//設置本機藍牙在300秒內可見 startActivity(discoverableIntent); }
2.調用startDiscover()搜索藍牙
開啟藍牙后,調用startDiscover()方法搜索藍牙,注意,只有開啟了藍牙可見性的設備才會響應。該搜索過程為異步操作,調用后講以廣播的機制返回搜索到的對象,搜索的過程一般為12秒,搜索過程頁面會顯示搜索到的設備。
public void doDiscovry() { if (blueadapter.isDiscovering()) { //判斷藍牙是否正在掃描,如果是調用取消掃描方法;如果不是,則開始掃描 blueadapter.cancelDiscovery(); } else blueadapter.startDiscovery(); }
3.注冊廣播
通過blueadapter.startDiscovery()來搜索藍牙設備,要獲取到搜索的結果需要注冊廣播。
定義一個列表
public ArrayAdapter adapter; ListView listView = (ListView) findViewById(R.id.list);//控件 列表
//定義一個列表,存藍牙設備的地址。 public ArrayList<String> arrayList=new ArrayList<>(); //定義一個列表,存藍牙設備地址,用於顯示。 public ArrayList<String> deviceName=new ArrayList<>();
將搜索到的顯示在控件列表上
adapter = new ArrayAdapter(this, android.R.layout.simple_expandable_list_item_1, deviceName); listView.setAdapter(adapter);
定義廣播和處理廣播消息
IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);//注冊廣播接收信號 registerReceiver(bluetoothReceiver, intentFilter);//用BroadcastReceiver 來取得結果 private final BroadcastReceiver bluetoothReceiver = new 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); deviceName.add("設備名:"+device.getName()+"\n" +"設備地址:"+device.getAddress() + "\n");//將搜索到的藍牙名稱和地址添加到列表。 arrayList.add( device.getAddress());//將搜索到的藍牙地址添加到列表。 adapter.notifyDataSetChanged();//更新 } } };
搜索完設備后,要記得注銷廣播。注冊后的廣播對象在其他地方有強引用,如果不取消,activity會釋放不了資源 。
protected void onDestroy(){ super.onDestroy();//解除注冊 unregisterReceiver(bluetoothReceiver); }
4.了解targetSdkVersion是否大於或等於23
若是大於或等於23,除了添加了藍牙權限外,還要動態獲取位置權限,才能將搜索到的藍牙設備顯示出來。若是小於,則不需要動態獲取權限。
動態申請權限,網上例子如下。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_ENABLE_BT) { if (resultCode == RESULT_OK) { textView.setText("打開藍牙成功"); } if (resultCode == RESULT_CANCELED) { textView.setText("放棄打開藍牙"); } } else { textView.setText("藍牙異常"); } } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case PERMISSION_REQUEST_COARSE_LOCATION: if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { } break; } }
四、配對藍牙設備
藍牙的配對和連接有兩種方式。一種是每個設備作為一個客戶端去連接一個服務端,向對方發起連接。另一種則是作為服務端來接收客戶端發來連接的消息。藍牙之間的數據傳輸采用的是和TCP傳輸類似的傳輸機制。
1.作為客戶端連接
首先要獲取一個代表遠程設備BluetoothDevice的對象,然后使用該BluetoothDevice的對象來獲取一個BluetoothSocket對象。BluetoothSocket對象調用connect()可以建立連接。
藍牙連接整個過程需要在子線程中執行的,並且要將 scoket.connect()放在一個新的子線程中,因為如果將這個方法也放在同一個子線程中解決的話,就會永遠報錯read failed, socket might closed or timeout, read ret: -1;借鑒網上的方法:再開一個子線程專門執行socket.connect()方法,問題可以解決;
另外,借鑒網上方法和建議,在獲得socket的時候 ,盡量不使用uuid方式;因為這樣雖然能夠獲取到socket 但是不能進行自動,所以使用的前提是已經配對了的設備連接;
使用反射的方式,能夠自動提示配對,也適合手機間通信。
final BluetoothSocket socket = (BluetoothSocket) device.getClass().getDeclaredMethod("createRfcommSocket", new Class[]{int.class}).invoke(device, 1);
代碼中的device需要把注冊廣播時的device作為參數傳進線程中。注意,傳進來的device的值要為遠程設備的地址,若不是或有出入,則可能會出現NullPointerException異常,並提示嘗試調用一個空的對象。為了解決這個問題,可以把顯示獲得的device名字、地址和傳入線程的device的地址分在不同的集合類。傳入線程的device使用只有設備地址的集合類。
在連接藍牙之前,還要先取消藍牙設備的掃描,否則容易連接失敗。
adapter.cancelDiscovery();//adapter為獲取到的藍牙適配器 socket.connect();//連接
2.作為服務端連接
服務端接收連接需要使用BluetoothServerSocket類,它的作用是監聽進來的連接,在一個連接被接收之后,會返回一個BluetoothSocket對象,這個對象可以用來和客戶端進行通信。
與客戶端一樣,服務端也要在子線程中實現。通過調用listenUsingRfcommWithServiceRecord(String,UUID)方法可以得到一個BluetoothServerSocket的對象,然后再用這個對象來調用accept()來返回一個BluetoothSocket對象。由於accept()是個阻塞的方法,它會直到接收到一個連接或異常之后才會返回,所以要放在子線程中。
bluetoothServerSocket=bluetoothAdapter.listenUsingRfcommWithServiceRecord(bluetoothAdapter.getDefaultAdapter().getName(), UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")); //bluetoothServerSocket= (BluetoothServerSocket) bluetoothAdapter.getClass().getMethod("listenUsingRfcommOn",new Class[]{int.class}).invoke(bluetoothAdapter,10); socket=bluetoothServerSocket.accept();//接收連接
代碼中注釋掉的內容是通過反射的方式來接收,由於我使用時出現了異常,所以暫時不考慮這個方法。
還有,與TCP不同的是,這個連接時只允許一個客戶端連接,因此在BluetoothServerSocket對象接收到一個連接請求時就要立刻調用close()方法把服務端關閉。
五、客戶端發送數據
當兩個設備成功連接之后,雙方都會有一個BluetoothSocket對象,這時,就可以在設備之間傳送數據了。
1.使用getOutputStream()方法來獲取輸出流來處理傳輸。
2.調用write()。
os = socket.getOutputStream();//獲取輸出流 if (os != null) {//判斷輸出流是否為空 os.write(message.getBytes("UTF-8")); } os.flush();//將輸出流的數據強制提交 os.close();//關閉輸出流 }
將輸出流中的數據提交后,要記得關閉輸出流,否則,可能會造成只能發送一次數據。
六、服務端接收數據
1.使用getInputStream()方法來獲取輸入流來處理傳輸。
2.調用read()。
InputStream im=null; im=bluetoothSocket.getInputStream(); byte buf[] = new byte[1024]; if (is != null) { is.read(buf, 0, buf.length);//讀取發來的數據 String message = new String(buf);//把發來的數據轉化為String類型 BuletoothMainActivity.UpdateRevMsg(message);//更新信息在顯示文本框 is.close();//關閉輸入流
使用服務端接收數據時,要先從客戶端向服務端發起連接,只有接收到連接請求之后,才會返回一個BluetoothSocket對象。有BluetoothSocket對象才能獲取到輸入流。
下面是將接收到數據顯示在界面的方法:
在Activity中定義Handler類的對象handler。
public static void UpdateRevMsg(String revMsg) { mRevMsg=revMsg; handler.post(RefreshTextView); } private static Runnable RefreshTextView=new Runnable() { @Override public void run() { textView2.setText(mRevMsg); } };