項目java文件3個:BluetoothChat:主界面,顯示聊天信息BluetoothChatService:裏面有3個主要線程類,AcceptThread:藍牙服務端socket監聽線程.。ConnectThread:藍牙socket連接線程。 ConnectedThread:連接後的通信線程DeviceListActivity:藍牙掃描選擇界面,負責傳回選擇連接的設備BluetoothChat:定義了很多常量,用於處理消息和請求:
// 調試用的日誌標誌TAG與是否打印日誌的標誌D
private static final String TAG = "BluetoothChat";
private static final boolean D = true;
// 從BluetoothChatService傳回來交給Handler處理的消息類型
public static final int MESSAGE_STATE_CHANGE = 1; // 藍牙socket狀態改變,居然有4個狀態監聽、正在連接、已連接、無。具體可以看Handler的handMessage方法
public static final int MESSAGE_READ = 2; // 這個消息類型被髮送回來是因爲藍牙服務socket在讀別的設備發來的內容
public static final int MESSAGE_WRITE = 3; // 這個消息類型被髮送回來是因爲藍牙socket在寫要發送的內容
public static final int MESSAGE_DEVICE_NAME = 4; // 這個消息發送回來是因爲連接到了一個設備,並且獲得了對方的名字,好像是手機的型號
public static final int MESSAGE_TOAST = 5; // 這個消息發送回來是有要用Toast控件廣播的內容
// 從BluetoothChatService的發送回來消息內容裏的鍵值
public static final String DEVICE_NAME = "device_name"; // 在發回設備名(MESSAGE_DEVICE_NAME)消息時,獲取設備名時的鍵值
public static final String TOAST = "toast"; // 同樣是鍵值,指向的是要Toast控件廣播的內容
// Intent的請求值,在startActivityForResult時使用
private static final int REQUEST_CONNECT_DEVICE = 1; // 在要請求去搜索設備的時候使用到
private static final int REQUEST_ENABLE_BT = 2; // 在請求要使藍牙可用的時候用到
控件不用理會,無非是一個顯示連接到某個設備的TextView,一個用於顯示對話的ListView,一個用於輸入聊天內容的EditText,一個發送按鈕Button
這裏我學到了EditText註冊了一個監聽器,然後實現了軟鍵盤按回車return鍵發送消息(一般我們在EditText裏按return鍵是換行)
// 用於監聽EditText的一個return鍵事件
private TextView.OnEditorActionListener mWriteListener =
new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView view, int actionId, KeyEvent event) {
// If the action is a key-up event on the return key, send the message
if (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_UP) {
String message = view.getText().toString();
sendMessage(message);
}
if(D) Log.i(TAG, "END onEditorAction");
return true;
}
};
然後是ListView從底下開始顯示:需要XML裏一句:
android:stackFromBottom="true"
然後我們就可以看到在onCreate方法裏有獲得BluetoothAdapter實例的語句:BluetoothAdapter代表的是一個本地藍牙設備
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
在onStart方法裏,有判斷本地藍牙是否可用的語句,不在的話想系統發送一個是否開啓藍牙的請求:
if (!mBluetoothAdapter.isEnabled()) { // 判斷藍牙是否可用
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); // 用於發送啓動請求的Intent
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
// 這裏會啓動系統的一個Activity,然後也會根據REQUEST_ENABLE_BT在OnActivityResult方法裏處理
} else { // 可用的情況下初始化界面和一些控件,比如ListView的適配器,BluetoothChatService實例
if (mChatService == null) setupChat();
}
在onResume方法裏,對BluetoothChatService實例進行判斷有無以及狀態,如果是‘無’狀態,即剛啓動,活stop了,就調用其start方法。
start方法作用就是開啓BluetoothChatService裏的AcceptThread線程,進行藍牙服務端socket監聽,開啓之前它會確保連接線程和通信線程是關閉的,不然就代碼強制關閉,因爲藍牙通信基本是點對點的。並且設置狀態爲監聽STATE_LISTEN。
此外,還有一個ensureDiscoverable方法,目的在於使本機藍牙可見,對應於菜單鍵menu的第二個選項
private void ensureDiscoverable() {
if(D) Log.d(TAG, "ensure discoverable");
if (mBluetoothAdapter.getScanMode() !=
BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { // 如果現在藍牙是不可見的模式
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); // 請求可見時間爲300秒
startActivity(discoverableIntent); // 同樣開始一個系統的Activity
}
}
菜單menu的第一個選項是掃描周圍可用的藍牙設備進行連接:這裏使用了請求結果的開啓Activity方法(startActivityForResult)開啓了DeviceListActivity。
這個Activity顯示出來是一個對話框(AlterDialog)的樣式,實現方法是在Mannifest.xml文件裏聲明這個Activity時定義它的主題爲
android:theme="@android:style/Theme.Dialog"
這個是用了android-sdk\platforms\android-x\data\res\values\styles.xml 的樣式
隨便一提的是menu裏也用了sdk裏的圖片文件:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/scan"
android:icon="@android:drawable/ic_menu_search" // 這裏
android:title="@string/connect" />
<item android:id="@+id/discoverable"
android:icon="@android:drawable/ic_menu_mylocation" // 這裏
android:title="@string/discoverable" />
</menu>
路徑都是android-sdk\platforms\android-x\data\res\drawable-x裏面的圖片
DeviceListActivity: 這Activity裏主要是ListView的顯示,以及關於藍牙搜索的一下方法。
在OnCreate方法裏,先適配器的實例化,然後是ListView的實例化,並設置適配器已經註冊Item點擊監聽器。監聽器事件裏處理了點擊搜索到的設備的名字,並將其傳回到BluetoothChat那個Activity裏做後續處理:
private OnItemClickListener mDeviceClickListener = new OnItemClickListener() {
public void onItemClick(AdapterView<?> av, View v, int arg2, long arg3) {
// Cancel discovery because it's costly and we're about to connect
mBtAdapter.cancelDiscovery(); // 本地設備取消掃描
// 獲取掃描到的設備的MAC地址,是隨後的17個字符
String info = ((TextView) v).getText().toString();
String address = info.substring(info.length() - 17);
// 建立包含MAC地址的Intent,用於傳回BluetoothChat那個Activity
Intent intent = new Intent();
intent.putExtra(EXTRA_DEVICE_ADDRESS, address);
// 設置結果並關閉當前Activity
setResult(Activity.RESULT_OK, intent);
finish();
}
};
在搜索設備中,還需要顯示之前匹配過的設備,獲取之前匹配過的語句:
Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices();
關鍵是搜索的過程:
這裏用到了接受者,處理搜索過程中的兩個廣播:BluetoothDevice.ACTION_FOUND 和 BluetoothDevice.ACTION_DISCOVERY_FINISHED
第一個廣播是在搜索到一個設備後就發送一個廣播,第二個是搜索完成後發送的廣播。隨便提一下BluetoothDevice類代表的是遠程設備。
接受者代碼:
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// 當找到一個設備
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// 從Intent裏面獲取一個BluetoothDevice對象
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// 如果它是已經匹配過的,那就不再新設備的那個列表顯示了,因爲在已經匹配過的列表已經有顯示了
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
// 當掃描完成以後,改變Title,這個Activity是一個帶圓形進度條的Activity
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
setProgressBarIndeterminateVisibility(false); // 關閉進度條
setTitle(R.string.select_device);
if (mNewDevicesArrayAdapter.getCount() == 0) { // 如果掃描不到設備
String noDevices = getResources().getText(R.string.none_found).toString();
mNewDevicesArrayAdapter.add(noDevices); //顯示無設備的字符串
}
}
}
};
在選擇設備,通過關閉Activity返回數據後,BluetoothCaht裏通過
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
獲得遠程設備對象
然後調用BluetoothChatService實例的connect方法進行連接操作。這個方法實際是開啓的ConnectThread線程,之前同樣要對連接線程和通信線程進行停止。理由同AcceptThread。之後再改變狀態爲連接中STATE_CONNECTING。
BluetoothChatService:
這個類的話關鍵就是3個線程類:
AcceptThread:藍牙服務端socket監聽線程.:
private class AcceptThread extends Thread {
// 本地的服務端socket
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
BluetoothServerSocket tmp = null;
// 創建一個用於監聽的服務端socket,通過下面這個方法,NAME參數沒關係,MY_UUID是確定唯一通道的標示符,用於連接的socket也要通過它產生
try {
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) {
Log.e(TAG, "listen() failed", e);
}
mmServerSocket = tmp;
}
public void run() {
if (D) Log.d(TAG, "BEGIN mAcceptThread" + this);
setName("AcceptThread");
BluetoothSocket socket = null;
// 不斷的監聽,直到狀態被改變成已連接狀態
while (mState != STATE_CONNECTED) {
try {
// 這是一個會產生阻塞的方法accept,要不就是成功地建立一個連接,要不就是返回一個異常
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "accept() failed", e);
break;
}
// 連接已經建立成功
if (socket != null) {
synchronized (BluetoothChatService.this) { // 同步塊,同一時間,只有一個線程可以訪問該區域
switch (mState) {
case STATE_LISTEN:
case STATE_CONNECTING:
// 狀態正常,開始進行線程通信,實際就是開啓通信線程ConnectedThread
connected(socket, socket.getRemoteDevice());
break;
case STATE_NONE:
case STATE_CONNECTED:
// 未準備或已連接狀態. 關閉新建的這個socket.
try {
socket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close unwanted socket", e);
}
break;
}
}
}
}
if (D) Log.i(TAG, "END mAcceptThread");
}
public void cancel() { //關閉服務端的socket
if (D) Log.d(TAG, "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of server failed", e);
}
}
}
ConnectThread:藍牙socket連接線程:
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
mmDevice = device;
BluetoothSocket tmp = null;
// 通過遠程設備以及唯一的UUID創建一個用於連接的socket
try {
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
Log.e(TAG, "create() failed", e);
}
mmSocket = tmp;
}
public void run() {
Log.i(TAG, "BEGIN mConnectThread");
setName("ConnectThread");
// 一定要停止掃描,不然會減慢連接速度
mAdapter.cancelDiscovery();
// 連接到服務端的socket
try {
// connect方法也會造成阻塞,直到成功連接,或返回一個異常
mmSocket.connect();
} catch (IOException e) {
connectionFailed(); //連接失敗發送要Toast的消息
// 關閉socket
try {
mmSocket.close();
} catch (IOException e2) {
Log.e(TAG, "unable to close() socket during connection failure", e2);
}
// 連接失敗了,把軟件變成監聽模式,可以讓別的設備來連接
BluetoothChatService.this.start();
return;
}
// 重置連接線程,因爲我們已經完成了
synchronized (BluetoothChatService.this) {
mConnectThread = null;
}
// 開始進行線程通信,實際就是開啓通信線程ConnectedThread
connected(mmSocket, mmDevice);
}
public void cancel() { // 關閉連接用的socket
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
ConnectedThread:連接後的通信線程:
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket) {
Log.d(TAG, "create ConnectedThread");
mmSocket = socket; // 這個是之前的用於連接的socket
InputStream tmpIn = null;
OutputStream tmpOut = null;
// 從連接的socket裏獲取InputStream和OutputStream
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
Log.i(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[1024];
int bytes;
// 已經連接上以後持續從通道中監聽輸入流的情況
while (true) {
try {
// 從通道的輸入流InputStream中讀取數據到buffer數組中
bytes = mmInStream.read(buffer);
// 將獲取到數據的消息發送到UI界面,同時也把內容buffer發過去顯示
mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "disconnected", e);
connectionLost(); // 連接異常斷開的時候發送一個需要Toast的消息,讓軟件進行Toast
break;
}
}
}
/**
* Write to the connected OutStream.
* @param buffer The bytes to write
*/
public void write(byte[] buffer) { // 這個方法用於把發送內容寫到通道的OutputStream中,會在發信息是被調用
try {
mmOutStream.write(buffer); //將buffer內容寫進通道
// 用於將自己發送給對方的內容也在UI界面顯示
mHandler.obtainMessage(BluetoothChat.MESSAGE_WRITE, -1, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "Exception during write", e);
}
}
public void cancel() { //關閉socket,即關閉通道
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
另外就是在讀別人發過來的數據的時候,由於別人發過來的是一個byte數組, 然後數組裏面不是每個元素都是有效數據,所以要自己對數據進行String再構造處理
String readMessage = new String(readBuf, 0, msg.arg1);
第一個參數是字節數組,第二個爲偏移量,(內容是從第一個位置寫入的),第三個參數是長度。