1. 程式人生 > >Android6.0 藍芽通訊的實現

Android6.0 藍芽通訊的實現

 

 

       因專案需要做一個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);
    }
};