設置藍牙
我們都知道,在手機的設置-藍牙中,可以進行藍牙設置的相關操作。
其實可以不離開自己的APP,直接完成藍牙設置的主要操作,可以結合自己的業務需求,相應地提示用戶開啓相關設置,提升用戶體驗。
首先要知道,藍牙連接需要知道待連接設備的MAC地址。
已配對設備的MAC地址是已知的,只要對方開啓了藍牙並在連接範圍內,就能連接成功。
未配對設備則需要通過搜索才能知道MAC地址,知道MAC地址後如果直接請求和對方建立連接 ,則系統會提示先進行配對,不需要我們再對此做處理。
下面開始操作藍牙,先添加藍牙權限:
<manifest ... >
<uses-permission android:name="android.permission.BLUETOOTH" />
...
</manifest>
如果希望應用啓動設備發現或操作藍牙設置,則還必須聲明 BLUETOOTH_ADMIN 權限。
再獲取藍牙適配器:
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
如果設備沒有藍牙,mBluetoothAdapter
將爲null
開啓藍牙
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
這將顯示對話框,請求用戶允許啓用藍牙。
在 onActivityResult()
回調中將收到請求結果
注意:啓用可檢測性將會自動啓用藍牙。
獲取已配對設備
mBluetoothAdapter.getBondedDevices();
獲取設備後就可以連接設備了。
搜索藍牙
搜索設備對於藍牙適配器而言是一個非常繁重的操作過程,並且會消耗大量資源。
因此要及時使用 cancelDiscovery() 停止發現。
if (mAdapter.isDiscovering()) {
mAdapter.cancelDiscovery();
}
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
if (mReceiver == null) {
mReceiver = 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);
//根據需求進行相應操作
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
//根據需求進行相應操作
}
}
};
}
mActivity.registerReceiver(mReceiver, filter);
mAdapter.startDiscovery();
這裏app將會在發現設備和搜索結束時收到廣播,可以實現自己的業務需求。
開啓可檢測性
如果尚未在設備上啓用藍牙,則啓用設備可檢測性將會自動啓用藍牙。
Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
以上代碼將顯示對話框,請求用戶允許將設備設爲可檢測到,時間持續 300 秒。
通過EXTRA_DISCOVERABLE_DURATION
來定義不同的持續時間。 應用可以設置的最大持續時間爲 3600 秒,值爲 0 則表示設備始終可檢測到。 任何小於 0 或大於 3600 的值都會自動設爲 120 秒。
藍牙通信
原理
服務端
一臺設備保持開放的 BluetoothServerSocket 來充當服務器,偵聽傳入的連接請求,並在接受一個請求後提供已連接的 BluetoothSocket(與 TCP Socket 相似)。 從 BluetoothServerSocket 獲取 BluetoothSocket 後,可以(並且應該)捨棄 BluetoothServerSocket,除非需要接受更多連接。
BluetoothServerSocket mServerSocket= mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
這將獲取一個BluetoothServerSocket
。
BluetoothSocket socket = mServerSocket.accept();
調用 accept() 開始偵聽連接請求,成功後返回一個BluetoothSocket
。
這是一個阻塞調用。它將在連接被接受或發生異常時返回。
客戶端
另一臺設備作爲客戶端,獲取遠程設備的 BluetoothDevice 對象後,與服務端建立連接:
BluetoothSocket mSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
這將獲取BluetoothSocket
,這裏的UUID必須和遠端設備一樣,後續的連接才能成功。
通用唯一標識符 (UUID) 是用於唯一標識信息的字符串 ID 的 128 位標準化格式。
可以使用網絡上的衆多隨機 UUID 生成器之一,然後使用 fromString(String) 初始化一個 UUID。
接着調用
mSocket.connect();
該方法將會阻塞,直至成功或拋出異常。
如果成功,則兩臺設備的藍牙藍牙連接已經建立,每臺設備都可以從BluetoothSocket
獲取輸入輸出流,進行數據傳輸。
藍牙聊天app淺析
這個app是Google的一個官方sample,地址點這裏
在兩臺設備上都裝了這個app後,就可以通過藍牙進行聊天了。
因爲是一個app,所以這個app既包含服務端代碼,也包含客戶端代碼。
連接狀態
// Constants that indicate the current connection state
public static final int STATE_NONE = 0; // we're doing nothing
public static final int STATE_LISTEN = 1; // now listening for incoming connections
public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
public static final int STATE_CONNECTED = 3; // now connected to a remote device
這是sample中的4個連接狀態,分別表示空狀態、監聽狀態、連接ing狀態、已連接狀態。
剛進入app爲空狀態,如果藍牙開啓則開啓服務端並進入監聽狀態,客戶端進行連接時進入連接ing狀態,連接成功則進入已連接狀態。
退出app時設爲空狀態,關閉socket,清空所有工作線程(下面會講到的3個線程),此時會造成讀取異常,但不應再連接藍牙設備。
偵聽線程
線程名AcceptThread
,進入app後就開啓該線程,用來偵聽傳入的連接請求,線程阻塞直至異常或接入成功。
偵聽異常則重啓服務端。
接入成功則開啓“管理連接線程”,同時設置連接狀態爲已連接,此時可以(並且應該)捨棄 BluetoothServerSocket,除非需要接受更多連接。
連接線程
線程名ConnectThread
,客戶端開啓該線程來連接服務端。
連接失敗則重啓服務端。
連接成功也是開啓“管理連接線程”,同時設置狀態爲已連接。
管理連接線程
線程名ConnectedThread
,這是所有流式傳輸讀取和寫入操作的專門線程。
因爲 read(byte[]) 和 write(byte[]) 方法都是阻塞調用。
read(byte[]) 將會阻塞,直至從流式傳輸中讀取內容。
write(byte[]) 通常不會阻塞,但如果遠程設備沒有足夠快地調用 read(byte[]),並且中間緩衝區已滿,則其可能會保持阻塞狀態以實現流量控制。
因此,線程中的主無限循環專門用於讀取 InputStream,如果讀取異常,說明藍牙中斷,此時可以提示用戶,並進行重連(app退出應關閉socket,也會造成讀取異常,此時不應重連)。
可使用線程中單獨的公共方法來發起對 OutputStream 的寫入操作。
序列化和反序列化
藍牙傳輸中,輸入輸出流傳輸的是字節,字節和String的轉換容易,但有時候不能很方便地獲取數據細節。
如果發送方把對象轉換爲字節傳送,接收方再把字節轉換爲對象,即實現序列化和反序列化,就很方便了。
下面介紹如何實現這樣的需求:
ObjectOutputStream
首先,收發雙方定義實體類A,實現Serializable
接口。
注意:實體類A在收發雙方的路徑必須相同,否則ObjectOutputStream
會轉換 失敗!
接着雙方就可以在輸出輸入流進行轉換,示例代碼如下:
A a = new A();
發送方
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
} catch (IOException e) {
e.printStackTrace();
}
mOutStream.write(baos.toByteArray());
接收方
mInStream.read(buffer);
ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
A a = null;
try {
ObjectInputStream ois = new ObjectInputStream(bais);
try {
a = (BluetoothPos) ois.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
Json
類似於後臺與客戶端的網絡通信,來實現序列化和反序列化。
優點:沒有“實體類A在收發雙方的路徑必須相同”這一限制,更加靈活方便!
發送方
通過Gson將實體對象轉換爲json字符串,再以UTF-8
編碼得到字節矩陣,最後流式輸出。
String json = new Gson().toJson(a);
try {
mOutStream.write(json.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
接收方
mInStream.read(buffer);
String json = "";
try {
json = new String(buffer, "UTF-8").trim();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
JsonReader reader = new JsonReader(new StringReader(json));
reader.setLenient(true);
A a= new Gson().fromJson(reader, A.class);
注意:這裏的json字符串必須調用trim()
方法去除空格並調用JsonReader
處理,不然反序列化時會報異常MalformedJsonException
。
參考資料:https://developer.android.com/guide/topics/connectivity/bluetooth.html#ConnectingDevices