[Android Studio]藍牙通信(Fragment中實現)
UUID
UUID(Universally Unique Identifier):通用唯一識別碼,軟件建構的標準。其保證對同一時空中的所有機器都唯一。其目的是讓分佈式系統中的所有元素,都能有唯一的辨識資訊,而不需要透過中央控制端來做辨識資訊的指定。
藍牙RFCOMM數據通信
一個藍牙設備可提供多種服務,每種服務對應一個UUID。
服務有:藍牙音頻傳輸 A2DP
,免提 HEADFREE
,電話本 PBAP
,串口通信 SPP
。
可通過BluetoothSocket類建立有關藍牙通信的套接字socket,通過MAC地址來識別遠程設備,建立完通信連接RFCOMM通道後以輸入、輸出流方式通信。
public BluetoothSocket createRfcommSocketToServiceRecord(UUID uuid)
其中的UUID參數指定socket的端口。
藍牙通信分server服務器端和client客戶端,它們用BluetoothSocket類的不同方法來獲取數據。
server端:
private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
BluetoothServerSocket tmp = null;
try {
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) {
e.printStackTrace();
}
mmServerSocket = tmp;
}
@Override
public void run() {
setName("AcceptThread");
BluetoothSocket socket = null;
while (mState != STATE_CONNECTED) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
e.printStackTrace();
}
if (socket != null) {
synchronized (BluetoothChatService.this) {
switch (mState) {
case STATE_LISTEN:
case STATE_CONNECTING:
connected(socket, socket.getRemoteDevice());
break;
case STATE_NONE:
case STATE_CONNECTED:
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
}
}
}
public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
client端:
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
mmDevice = device;
BluetoothSocket tmp = null;
try {
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
e.printStackTrace();
}
mmSocket = tmp;
}
@Override
public void run() {
setName("ConnectThread");
mAdapter.cancelDiscovery();
try {
mmSocket.connect();
} catch (IOException e) {
connectionFailed();
try {
mmSocket.close();
} catch (IOException ex) {
ex.printStackTrace();
}
BluetoothChatService.this.start();
return;
}
synchronized (BluetoothChatService.this) {
mConnectThread = null;
}
connected(mmSocket, mmDevice);
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
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;
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
@Override
public void run() {
byte[] buffer = new byte[1024];
int bytes;
while (true) {
try {
bytes = mmInStream.read(buffer);
mHandler.obtainMessage(homeFragment.MESSAGE_READ, bytes, -1, buffer).sendToTarget();
} catch (IOException e) {
connectionLost();
break;
}
}
}
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
mHandler.obtainMessage(homeFragment.MESSAGE_WRITE, -1, -1, buffer).sendToTarget();
} catch (IOException e) {
e.printStackTrace();
}
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
synchronized
:同步方法實現排隊調用。
藍牙相關權限
AndroidManifest.xml中添加:
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH" />
在Fragment中添加請求權限操作。
佈局
Toolbar Menu
我使用的Android Studio的版本是3.5.3,對於toolbar還是直接使用AS自帶的比較好。若是寫出v7版本的toolbar,則會出現一些版本不支持的問題。
Toolbar:點擊如圖右上角圖標顯示下拉列表,下拉列表爲option_menu.xml佈局。
option_menu.xml:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/scan"
android:icon="@android:drawable/ic_menu_myplaces"
android:title="@string/connect" />
<item android:id="@+id/discoverable"
android:icon="@android:drawable/ic_menu_view"
android:title="@string/discoverable" />
<item android:id="@+id/back"
android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:title="@string/back" />
</menu>
在Fragment中使用Toolbar和在Activity使用Toolbar有所不同。toolbar可作爲獨立控件, 在Fragment中無需加上
((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);
,
但需要加上setHasOptionsMenu(true)
,表示有menu待使用。
一開始這裏沒寫正確,應用根本打不開,只有閃退。
homeFragment.java:
Toolbar toolbar = (Toolbar) view.findViewById(R.id.toolbar);
setHasOptionsMenu(true);
// ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);
toolbar.inflateMenu(R.menu.option_menu);
TextView ArrayAdapter
發送消息和藍牙設備搜索顯示這兩部分有用到了ArrayAdapter,如:
mPairedDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name);
這種ArrayAdapter的定義使用到layout,其佈局的根節點必須爲TextView(如下代碼),否則報錯:ArrayAdapter requires the resource ID to be a TextView。
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp">
</TextView>
藍牙搜索鏈接
在點擊了option_menu.xml中的“我的好友”之後,隨後打開新的界面進行查看已鏈接過的設備和搜索發現的設備。以廣播Broadcast的形式來實現接收掃描到的藍牙設備。
DeviceList.java
private final BroadcastReceiver 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);
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
if (mNewDevicesArrayAdapter.getCount() == 0) {
String noDevices = getResources().getText(R.string.none_found).toString();
mNewDevicesArrayAdapter.add(noDevices);
}
}
}
};
private void init() {
Button scanButton = findViewById(R.id.button_scan);
scanButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Toast.makeText(DeviceList.this, R.string.scanning, Toast.LENGTH_LONG).show();
doDiscovery(); //搜索藍牙設備
}
});
mPairedDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name);
mNewDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name);
//已配對藍牙設備列表
ListView pairedListView = findViewById(R.id.paired_devices);
pairedListView.setAdapter(mPairedDevicesArrayAdapter);
pairedListView.setOnItemClickListener(mPaireDeviceClickListener);
//未配對藍牙設備列表
ListView newDevicesListView = findViewById(R.id.new_devices);
newDevicesListView.setAdapter(mNewDevicesArrayAdapter);
newDevicesListView.setOnItemClickListener(mNewDeviceClickListener);
//動態註冊廣播接收者
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter);
filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
registerReceiver(mReceiver, filter);
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE);
for (BluetoothDevice device : pairedDevices) {
mPairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
} else {
String noDevices = getResources().getText(R.string.none_paired).toString();
mPairedDevicesArrayAdapter.add(noDevices);
}
}
Hander傳遞消息
使用Hander對象實現主線程與子線程之間傳遞消息,同時在“微信”tab界面顯示當前鏈接狀態。
homeFragment.java:
mChatService = new BluetoothChatService(view.getContext(), mHandler);
private final Handler mHandler = new Handler() { //消息處理
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_STATE_CHANGE:
switch (msg.arg1) {
case BluetoothChatService.STATE_CONNECTED:
mTitle.setText(R.string.title_connected_to);
mTitle.append(mConnectedDeviceName);
mConversationArrayAdapter.clear();
break;
case BluetoothChatService.STATE_CONNECTING:
mTitle.setText(R.string.title_connecting);
break;
case BluetoothChatService.STATE_LISTEN:
case BluetoothChatService.STATE_NONE:
mTitle.setText(R.string.title_not_connected);
break;
}
break;
case MESSAGE_WRITE:
byte[] writeBuf = (byte[]) msg.obj;
String writeMessage = new String(writeBuf);
mConversationArrayAdapter.add("我: " + writeMessage);
break;
case MESSAGE_READ:
byte[] readBuf = (byte[]) msg.obj;
String readMessage = new String(readBuf, 0, msg.arg1);
mConversationArrayAdapter.add(mConnectedDeviceName + ": "
+ readMessage);
break;
case MESSAGE_DEVICE_NAME:
mConnectedDeviceName = msg.getData().getString(DEVICE_NAME);
Toast.makeText(getActivity().getApplicationContext(),"鏈接到 " + mConnectedDeviceName, Toast.LENGTH_SHORT).show();
break;
case MESSAGE_TOAST:
Toast.makeText(getActivity().getApplicationContext(), msg.getData().getString(TOAST), Toast.LENGTH_SHORT).show();
break;
}
}
};
參閱:
- 對Android藍牙UUID的理解
- 關於藍牙通信文檔 Android建立藍牙RFCOMM通信
- fragment中toolbar的一些坑
- ArrayAdapter requires the resource ID to be a TextView問題
- 掌握手機Bluetooth的使用和Android對Bluetooth的支持
- Android Developers | Bluetooth
源碼已上傳至 GitHub。