Android 藍(lán)牙開發(fā)

近期的項(xiàng)目涉及到藍(lán)牙通訊,于是就整理了一下藍(lán)牙的通訊機(jī)制的知識點(diǎn)学歧。
藍(lán)牙通訊主要是配對和連接兩個過程罩引。

配對和連接是兩個不同的概念,請不要混為一談枝笨,配對上的設(shè)備不代表已經(jīng)連接袁铐。

首先我們需要權(quán)限

 <manifest ... > 
<uses-permission android:name="android.permission.BLUETOOTH" /> ...
</manifest>
BluetoothAdapter

代表本地藍(lán)牙適配器(藍(lán)牙無線電)。BluetoothAdapter是所有藍(lán)牙交互的入口伺帘。使用這個你可以發(fā)現(xiàn)其他藍(lán)牙設(shè)備昭躺,查詢已配對的設(shè)備列表,使用一個已知的MAC地址來實(shí)例化一個BluetoothDevice伪嫁。

//通常我們使用該方法獲得藍(lán)牙的本地屬性领炫。
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
        // 代表設(shè)備不支持藍(lán)牙
    }
adapter部分方法
BluetoothDevice

代表一個遠(yuǎn)程藍(lán)牙設(shè)備,使用這個來請求一個與遠(yuǎn)程設(shè)備的BluetoothSocket連接张咳,或者查詢關(guān)于設(shè)備名稱帝洪、地址似舵、類和連接狀態(tài)等設(shè)備信息。

//通過mac地址來獲得遠(yuǎn)程藍(lán)牙設(shè)備葱峡,通常我們也使用查找設(shè)備的廣播來獲得遠(yuǎn)程藍(lán)牙設(shè)備砚哗,稍后會介紹
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(macAddress);
//該類的方法與adapter類似
開啟藍(lán)牙

(以下的mBluetoothAdapter表示本地藍(lán)牙設(shè)備,mBluetoothDevice表示遠(yuǎn)程藍(lán)牙設(shè)備)
方法一:

     //利用系統(tǒng)設(shè)置開啟藍(lán)牙
     Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
     discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 3600);
     startActivity(discoverableIntent);
    
    if (!mBluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }

方法二

mBluetoothAdapter.enable()
//需要權(quán)限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
藍(lán)牙搜尋廣播
Action 靜態(tài)注冊
<intent-filter>
<action android:name="android.bluetooth.device.action.FOUND" />
</intent-filter>
動態(tài)注冊
    // 定義一個廣播 for ACTION_FOUND
    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            // 當(dāng)搜尋到設(shè)備
            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                //獲得遠(yuǎn)程設(shè)備信息
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                // 保存設(shè)備的mac地址與藍(lán)牙名稱
                mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
            }
        }
    };
    // 動態(tài)注冊代碼
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    registerReceiver(mReceiver, filter);
    //不要忘了在onDestroy注銷廣播喲  
掃描代碼
mBluetoothAdapter.startDiscovery();//開始掃描
//藍(lán)牙掃描是非常耗費(fèi)資源與時間的砰奕,當(dāng)我們掃描到需要操作的設(shè)備的時候蛛芥,我們需要停止掃描來獲得更好的連接效率。
mBluetoothAdapter.cancelDiscovery();//取消掃描

設(shè)置藍(lán)牙可被搜索

Intent discoverableIntent = newIntent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);//后面的參數(shù)是可被發(fā)現(xiàn)的時間军援,最大支持3600秒
startActivity(discoverableIntent);

藍(lán)牙配對

如果只是單純的配對仅淑,我們可以利用反射:
(這里將會介紹兩張配對方式,反射是第一種胸哥,第二種是通過連接來配對涯竟,這里的配對都不需要輸入pin碼的)

 try {  
    //檢查是否處于未配對狀態(tài)
     if (mBluetoothDevice.getBondState() == BluetoothDevice.BOND_NONE) {  
        Method creMethod = BluetoothDevice.class.getMethod("createBond");  
        Log.e("TAG", "開始配對");  
        creMethod.invoke(mBluetoothDevice);  
     } 
 } catch (Exception e) {  
    // TODO: handle exception  
    //DisplayMessage("無法配對!");  
    e.printStackTrace();  
} 
取消配對
 static public boolean removeBond(Class btClass, BluetoothDevice btDevice)  
            throws Exception  
    {  
        Method removeBondMethod = btClass.getMethod("removeBond");  
        Boolean returnValue = (Boolean) removeBondMethod.invoke(btDevice);  
        return returnValue.booleanValue();  
    }  
連接與配對

是的藍(lán)牙的連接類似TCP的socket連接空厌,但是不同的是藍(lán)牙的BluetoothSock不是new出來的庐船,而是通過一個靜態(tài)方法根據(jù)UUID生成的,而不是端口號嘲更。同一時間一個通道只允許一個socket連接通訊(多線程或許能解決這個問題)筐钟,但是大多藍(lán)牙情景都是一對一的連接。
為了在兩臺設(shè)備上創(chuàng)建一個連接赋朦,你必須實(shí)現(xiàn)服務(wù)器端和客戶端兩頭的機(jī)制盗棵,因?yàn)橐粋€設(shè)備必須打開一個服務(wù)器socket,而另一個設(shè)備初始化創(chuàng)建(使用服務(wù)器設(shè)備的MAC地址來初始化一個連接)北发。當(dāng)他們在相同的RFCOMM通道上有一個已連接的 BluetoothSocket 時纹因,服務(wù)器和客戶被認(rèn)為是互相連接了。
**注意:如果兩臺設(shè)備之前沒有配對過琳拨,那么Android框架將會自動顯示一個請求配對的通知或?qū)υ捒虿t恰。因此,?dāng)嘗試連接設(shè)備時狱庇,你的應(yīng)用不需要考慮設(shè)備是否配對過惊畏。你的RFCOMM連接嘗試將會阻塞,知道用戶成功配對密任,或者用戶拒絕失敗時颜启,或者配對失敗,或者超時
**

BluetoothSocket

代表一個藍(lán)牙socket的接口(和TCP Socket類似)浪讳。這是一個連接點(diǎn)缰盏,它允許一個應(yīng)用與其他藍(lán)牙設(shè)備通過InputStreamOutputStream交換數(shù)據(jù)。

BluetoothServerSocket secure = null;    
if(sdk>=10){
secure = adapter.listenUsingRfcommWithServiceRecord(app_name, SECURE_UUID);
}else{
secure = adapter.listenUsingRfcommWithServiceRecord(app_name, SECURE_UUID);
}
//UUID可以參考網(wǎng)上內(nèi)容自己生成
UUID

一個全局唯一的標(biāo)識符(UUID)是一個標(biāo)準(zhǔn)的128-bit格式的string ID,它被用于唯一標(biāo)識信息口猜。一個UUID的關(guān)鍵點(diǎn)是它非常大以至于你可以隨機(jī)選擇而不會發(fā)生崩潰负溪。在這種情況下,它被用于唯一地指定你的應(yīng)用中的藍(lán)牙服務(wù)济炎。為了得到一個UUID以在你的應(yīng)用中使用川抡,你可以使用網(wǎng)絡(luò)上的任何一種隨機(jī)UUID產(chǎn)生器,然后使用fromString(String)初始化一個UUID须尚。

BluetoothServerSocket

代表一個開放的服務(wù)器socket崖堤,它監(jiān)聽接受的請求(與TCP ServerSocket類似)。為了連接兩臺Android設(shè)備耐床,一個設(shè)備必須使用這個類開啟一個服務(wù)器socket倘感。當(dāng)一個遠(yuǎn)程藍(lán)牙設(shè)備開始一個和該設(shè)備的連接請求,BluetoothServerSocket將會返回一個已連接的BluetoothSocket咙咽,接受該連接。


BluetoothSocket socket;
if (sdk < 10) {
    socket = device.createRfcommSocketToServiceRecord(BlueToothControl.SECURE_UUID);
} else {//sdk >= 10
    socket = device.createInsecureRfcommSocketToServiceRecord(BlueToothControl.INSECURE_UUID);
}
//UUID可以參考網(wǎng)上內(nèi)容自己生成

下面我會舉一個栗子淤年,并且說明是如何進(jìn)行通訊的

服務(wù)端

private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;
 
    public AcceptThread() {
        // 使用一個臨時對象來標(biāo)志mmServerSocket,
        // 因?yàn)閙mServerSocket是final類型
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID 是應(yīng)用的 UUID string, 同樣也在客戶端使用
            tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) { }
        mmServerSocket = tmp;
    }
 
    public void run() {
        BluetoothSocket socket = null;
        // 保持監(jiān)聽直到建立連接或者發(fā)生異常
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }
            // 如果接收到連接
            if (socket != null) {
                // 用一個線程去做一些連接的管理工作
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }
//該方法用于中斷監(jiān)聽并且斷開連接
public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) { }
    }
}
  • listenUsingRfcommWithServiceRecord(String, UUID)得到一個BluetoothServerSocket钧敞。這個String是你的服務(wù)的標(biāo)志名稱,系統(tǒng)將會把它寫入設(shè)備中的一個新的服務(wù)發(fā)現(xiàn)協(xié)議(SDP)數(shù)據(jù)庫條目中(名字是任意的麸粮,并且可以只是你應(yīng)用的名字)溉苛。UUID同樣被包含在SDP條目中,并且將會成為和客戶端設(shè)備連接協(xié)議的基礎(chǔ)弄诲。也就是說愚战,當(dāng)客戶端嘗試連接這個設(shè)備時,它將會攜帶一個UUID用于唯一指定它想要連接的服務(wù)器齐遵。這些UUIDs必須匹配以便該連接可以被接受(在下一步中)寂玲。 通過調(diào)用accept()開始監(jiān)聽連接請求。
  • 通過調(diào)用accept()開始監(jiān)聽連接請求梗摇。這一個阻塞調(diào)用拓哟。在一個連接被接受或一個異常出現(xiàn)時,它將會返回伶授。只有當(dāng)一個遠(yuǎn)程設(shè)備使用一個UUID發(fā)送了一個連接請求断序,并且該UUID和正在監(jiān)聽的服務(wù)器socket注冊的UUID相匹配時,一個連接才會被接受糜烹。成功后违诗,accept() 將會返回一個已連接的 BluetoothSocket
  • 調(diào)用close()疮蹦,除非你想要接受更多的連接诸迟。這將釋放服務(wù)器socket和它所有的資源,但是不會關(guān)閉 accept()返回的已連接的 BluetoothSocket。不同于TCP/IP亮蒋,RFCOMM僅僅允許每一個通道上在某一時刻只有一個已連接的客戶端扣典,因此在大多數(shù)情況下在接受一個已連接的socket后,在BluetoothServerSocket上調(diào)用 close()是非常必要的慎玖。
  • accept() 不應(yīng)該再主活動UI線程上執(zhí)行贮尖,因?yàn)樗且粋€阻塞調(diào)用,并且將會阻止任何與應(yīng)用的交互行為趁怔。它通常在你的應(yīng)用管理的一個新的線程中使用一個BluetoothServerSocketBluetoothSocket 來完成所有工作湿硝。為了中止一個阻塞調(diào)用,例如accept()润努,從你的其他線程里在BluetoothServerSocket (或 BluetoothSocket) 上調(diào)用close() 关斜,然后阻塞調(diào)用就會立即返回。注意在 BluetoothServerSocketBluetoothSocket 上所有的方法都是線程安全的铺浇。

客戶端

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;
 
    public ConnectThread(BluetoothDevice device) {
        //使用臨時變量來標(biāo)志mmSocket,
        // 因?yàn)?mmSocket 是 final
        BluetoothSocket tmp = null;
        mmDevice = device;
 
        // 通過得到的BluetoothDevice 獲取 BluetoothSocket來連接
        try {
            // MY_UUID 是應(yīng)用的UUID string, 同樣也用于服務(wù)端
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }
 
    public void run() {
        // 你應(yīng)該在連接前總是這樣做痢畜,而不需要考慮是否真的有在執(zhí)行查詢?nèi)蝿?wù)(但是如果你想要檢查,調(diào)用 isDiscovering())
        //檢查會很大程度影響效率
        mBluetoothAdapter.cancelDiscovery();
 
        try {
            // 通過socket.connect()來連接. 同時也會阻塞線程
            // 直到連接成功或者拋出異常
            mmSocket.connect();
        } catch (IOException connectException) {
            // 無法連接鳍侣,關(guān)閉socket并退出
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }
 
        //做一些管理socket的工作
        manageConnectedSocket(mmSocket);
    }
 
  
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

為了和一個遠(yuǎn)程設(shè)備(一個持有服務(wù)器socket的設(shè)備)初始化一個連接丁稀,你必須首先得到一個BluetoothDevice 對象來表示這個遠(yuǎn)程設(shè)備。(上面的課程Finding Devices講述了如何得到一個 BluetoothDevice )倚聚。然后你必須使用BluetoothDevice來得到一個 BluetoothSocket 线衫,然后初始化該連接。
下面是基本的過程:

  • 使用 BluetoothDevice惑折,通過調(diào)用createRfcommSocketToServiceRecord(UUID)來得到一個 BluetoothSocket 授账。T這將初始化一個BluetoothSocket,它連接到該BluetoothDevice惨驶。這里傳遞的UUID必須和服務(wù)器設(shè)備開啟它的 BluetoothServerSocket時使用的UUID相匹配白热。
  • 通過調(diào)用connect()初始化一個連接。執(zhí)行這個調(diào)用時粗卜,系統(tǒng)將會在遠(yuǎn)程設(shè)備上執(zhí)行一個SDP查找工作棘捣,來匹配UUID。如果查找成功休建,并且遠(yuǎn)程設(shè)備接受了連接乍恐,它將會在連接過程中分享RFCOMM通道,而 connect()將會返回测砂。這個方法是阻塞的茵烈。如果,處于任何原因砌些,該連接失敗了或者connect()超時了(大約12秒以后)呜投,那么它將會拋出一個異常加匈。
    因?yàn)?code>connect()是一個阻塞調(diào)用,這個連接過程應(yīng)該總是在一個單獨(dú)的線程中執(zhí)行仑荐。
注意:你應(yīng)該總是確保在你調(diào)用connect()時設(shè)備沒有執(zhí)行設(shè)備查找工作雕拼。如果正在查找設(shè)備,那么連接嘗試將會很大程度的減緩粘招,并且很有可能會失敗啥寇。

**當(dāng)你使用完你的 BluetoothSocket后,總是調(diào)用close()來清除資源洒扎。這樣做將會立即關(guān)閉已連接的socket辑甜,然后清除所有的內(nèi)部資源
**

讀寫流

private class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;
 
    public ConnectedThread(BluetoothSocket socket) {
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;
 
        // 獲得socket的流信息
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }
 
        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }
 
    public void run() {
        byte[] buffer = new byte[1024];  //緩沖字符數(shù)組
 
        // 保持通訊
        while (true) {
            try {
                // Read from the InputStream
                bytes = mmInStream.read(buffer);
                // 發(fā)送包含的信息給UI
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }
 
    /* 調(diào)用該方法去發(fā)送信息 */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }
 
    /* 調(diào)用該方法關(guān)閉連接*/
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

**當(dāng)然,實(shí)現(xiàn)的細(xì)節(jié)需要考慮袍冷。首先并且最重要的是磷醋,你應(yīng)該為所有輸入和輸出的數(shù)據(jù)流使用一個專屬的線程。這是十分重要的胡诗,因?yàn)?code>read(byte[]) 和 write(byte[])方法都是阻塞調(diào)用邓线。 read(byte[])將會發(fā)生阻塞知道送數(shù)據(jù)流中讀取到了一些東西。write(byte[])不經(jīng)常發(fā)生阻塞煌恢,但是當(dāng)遠(yuǎn)程設(shè)備沒有足夠迅速地調(diào)用read(byte[])而中間緩沖區(qū)已經(jīng)負(fù)載時可以阻塞骇陈。因此,你的線程中的主要循環(huán)應(yīng)該是專門從InputStream中讀取數(shù)據(jù)的症虑。一個單獨(dú)的公共方法可以被用于初始化向OutputStream中寫入數(shù)據(jù)。
**

如文中有錯誤归薛,歡迎指出谍憔。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市主籍,隨后出現(xiàn)的幾起案子习贫,更是在濱河造成了極大的恐慌,老刑警劉巖千元,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件苫昌,死亡現(xiàn)場離奇詭異,居然都是意外死亡幸海,警方通過查閱死者的電腦和手機(jī)祟身,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來物独,“玉大人袜硫,你說我怎么就攤上這事〉猜ǎ” “怎么了婉陷?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵帚称,是天一觀的道長。 經(jīng)常有香客問我秽澳,道長闯睹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任担神,我火速辦了婚禮楼吃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘杏瞻。我一直安慰自己所刀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布捞挥。 她就那樣靜靜地躺著浮创,像睡著了一般。 火紅的嫁衣襯著肌膚如雪砌函。 梳的紋絲不亂的頭發(fā)上平道,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天草雕,我揣著相機(jī)與錄音,去河邊找鬼。 笑死棍丐,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的娄猫。 我是一名探鬼主播昏鹃,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贩疙!你這毒婦竟也來了讹弯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤这溅,失蹤者是張志新(化名)和其女友劉穎组民,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體悲靴,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡臭胜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了癞尚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耸三。...
    茶點(diǎn)故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖浇揩,靈堂內(nèi)的尸體忽然破棺而出吕晌,到底是詐尸還是另有隱情,我是刑警寧澤临燃,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布睛驳,位于F島的核電站烙心,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏乏沸。R本人自食惡果不足惜淫茵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蹬跃。 院中可真熱鬧匙瘪,春花似錦、人聲如沸蝶缀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翁都。三九已至碍论,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間柄慰,已是汗流浹背鳍悠。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坐搔,地道東北人藏研。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像概行,于是被迫代替她去往敵國和親蠢挡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容