Android平臺藍芽程式設計之藍芽聊天分析(二)
接著上一篇沒有完成的任務,我們繼續分析這個藍芽聊天程式的實現,本文主要包括以下兩個部分的內容:其一,分析掃描裝置部分DeviceListActivity,其二,分析具體的聊天過程的完整通訊方案,包括埠監聽、連結配對、訊息傳送和接收等,如果有對上一篇文章不太熟悉的,可以返回去在過一次,這樣會有利於本文的理解。
裝置掃描(DeviceListActivity)
在上一篇文章的介紹中,當用戶點選了掃描按鈕之後,則會執行如下程式碼:
// 啟動DeviceListActivity檢視裝置並掃描 Intent serverIntent = new Intent(this, DeviceListActivity.class); startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
該程式碼將跳轉到DeviceListActivity進行裝置的掃描,並且通過REQUEST_CONNECT_DEVICE來請求連結掃描到的裝置。從AndroidManifest.xml檔案中我們知道DeviceListActivity將為定義為一個對話方塊的風格,下圖是該應用程式中,掃描藍芽裝置的截圖。
其中DeviceListActivity則為圖中對話方塊部分,其介面的佈局如下程式碼所示。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" > <!-- 已經配對的裝置 --> <TextView android:id="@+id/title_paired_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/title_paired_devices" android:visibility="gone" android:background="#666" android:textColor="#fff" android:paddingLeft="5dp" /> <!-- 已經配對的裝置資訊 --> <ListView android:id="@+id/paired_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:stackFromBottom="true" android:layout_weight="1" /> <!-- 掃描出來沒有經過配對的裝置 --> <TextView android:id="@+id/title_new_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/title_other_devices" android:visibility="gone" android:background="#666" android:textColor="#fff" android:paddingLeft="5dp" /> <!-- 掃描出來沒有經過配對的裝置資訊 --> <ListView android:id="@+id/new_devices" android:layout_width="match_parent" android:layout_height="wrap_content" android:stackFromBottom="true" android:layout_weight="2" /> <!-- 掃描按鈕 --> <Button android:id="@+id/button_scan" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/button_scan" /> </LinearLayout>
該佈局整體由一個線性佈局LinearLayout組成,其中包含了兩個textview中來顯示已經配對的裝置和信掃描出來的裝置(還沒有經過配對)和兩個ListView分別用於顯示已經配對和沒有配對的裝置的相關資訊。按鈕則用於執行掃描過程用,整個結構很簡單,下面我們開始分析如何編碼實現了。
同樣開始之前,我們先確定該類中的變數的作用,定義如下:
public class DeviceListActivity extends Activity { // Debugging private static final String TAG = "DeviceListActivity"; private static final boolean D = true; // Return Intent extra public static String EXTRA_DEVICE_ADDRESS = "device_address"; // 藍芽介面卡 private BluetoothAdapter mBtAdapter; //已經配對的藍芽裝置 private ArrayAdapter<String> mPairedDevicesArrayAdapter; //新的藍芽裝置 private ArrayAdapter<String> mNewDevicesArrayAdapter;
其中Debugging部分,同樣用於除錯,這裡定義了一個EXTRA_DEVICE_ADDRESS,用於在通過Intent傳遞資料時的附加資訊,即裝置的地址,當掃描出來之後,返回到BluetoothChat中的onActivityResult函式的REQUEST_CONNECT_DEVICE命令,這是我們就需要通過DeviceListActivity.EXTRA_DEVICE_ADDRESS來取得該裝置的Mac地址,因此當我們掃描完成之後在反饋掃描結果時就需要繫結裝置地址作為EXTRA_DEVICE_ADDRESS的附加值,這和我們上一篇介紹的並不矛盾。另外其他幾個變數則分別是本地藍芽介面卡、已經配對的藍芽列表和掃描出來還沒有配對的藍芽裝置列表,稍後我們可以看到對他們的使用。
進入DeviceListActivity之後我們首先分析onCreate,首先通過如下程式碼對視窗進行了設定:
// 設定視窗
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.device_list);
setResult(Activity.RESULT_CANCELED);
這裡我們設定了視窗需要帶一個進度條,當我們在掃描時就看有很容易的高速使用者掃描進度。具體佈局則設定為device_list.xml也是我們文字第一段程式碼的內容,接下來首先初始化掃描按鈕,程式碼如下:
// 初始化掃描按鈕
Button scanButton = (Button) findViewById(R.id.button_scan);
scanButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
doDiscovery();
v.setVisibility(View.GONE);
}
});
首先取得按鈕物件,然後為其設定一個事件監聽,當事件觸發時就通過doDiscovery函式來執行掃描操作即可,具體掃描過程稍後分析。
然後需要初始化用來顯示裝置的列表和資料來源,使用如下程式碼即可:
//初始化ArrayAdapter,一個是已經配對的裝置,一個是新發現的裝置
mPairedDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name);
mNewDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name);
// 檢測並設定已配對的裝置ListView
ListView pairedListView = (ListView) findViewById(R.id.paired_devices);
pairedListView.setAdapter(mPairedDevicesArrayAdapter);
pairedListView.setOnItemClickListener(mDeviceClickListener);
// 檢查並設定行發現的藍芽裝置ListView
ListView newDevicesListView = (ListView) findViewById(R.id.new_devices);
newDevicesListView.setAdapter(mNewDevicesArrayAdapter);
newDevicesListView.setOnItemClickListener(mDeviceClickListener);
並分別對這些列表中的選項設定了監聽mDeviceClickListener,用來處理,當選擇該選項時,就進行連結和配對操作。既然是掃描,我們就需要對掃描的結果進行監控,這裡我們構建了一個廣播BroadcastReceiver來對掃描的結果進行處理,程式碼如下:
// 當一個裝置被發現時,需要註冊一個廣播
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
this.registerReceiver(mReceiver, filter);
// 當顯示檢查完畢的時候,需要註冊一個廣播
filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
this.registerReceiver(mReceiver, filter);
這裡我們註冊到廣播mReceiver的IntentFilter主要包括了發現藍芽裝置(BluetoothDevice.ACTION_FOUND)和掃描結束(BluetoothAdapter.ACTION_DISCOVERY_FINISHED),稍後我們分析如何在mReceiver中來處理這些事件。
最後我們需要取得本地藍芽介面卡和一些初始的藍芽裝置資料顯示列表進行處理,程式碼如下:
// 得到本地的藍芽介面卡
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
// 得到一個已經匹配到本地介面卡的BluetoothDevice類的物件集合
Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices();
// 如果有配對成功的裝置則新增到ArrayAdapter
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);
}
首先通過藍芽介面卡的getBondedDevices函式取得已經配對的藍芽裝置,並將其新增到mPairedDevicesArrayAdapter資料來源中,會顯示到pairedListView列表檢視中,如果沒有已經配對的藍芽裝置,則顯示一個R.string.none_paired字串表示目前沒有配對成功的裝置。
onDestroy函式中會制定銷燬操作,主要包括藍芽介面卡和廣播的登出操作,程式碼如下:
@Override
protected void onDestroy() {
super.onDestroy();
// 確保我們沒有發現,檢測裝置
if (mBtAdapter != null) {
mBtAdapter.cancelDiscovery();
}
// 解除安裝所註冊的廣播
this.unregisterReceiver(mReceiver);
}
對於藍芽介面卡的取消方式則呼叫cancelDiscovery()函式即可,解除安裝mReceiver則需要呼叫unregisterReceiver即可。
做好初始化工作之後,下面我們開始分析掃描函式doDiscovery(),其掃描過程的實現很就簡單,程式碼如下:
/**
* 請求能被發現的裝置
*/
private void doDiscovery() {
if (D) Log.d(TAG, "doDiscovery()");
// 設定顯示進度條
setProgressBarIndeterminateVisibility(true);
// 設定title為掃描狀態
setTitle(R.string.scanning);
// 顯示新裝置的子標題
findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE);
// 如果已經在請求現實了,那麼就先停止
if (mBtAdapter.isDiscovering()) {
mBtAdapter.cancelDiscovery();
}
// 請求從藍芽介面卡得到能夠被發現的裝置
mBtAdapter.startDiscovery();
}
首先通過setProgressBarIndeterminateVisibility將進度條設定為顯示狀態,設定標題title為R.string.scanning字串,表示正在掃描中,程式碼中所說的新裝置的子標題,其實就是上面我們所說的掃描到的沒有經過配對的裝置的title,對應於R.id.title_new_devices。掃描之前我們首先通過isDiscovering函式檢測當前是否正在掃描,如果正在掃描則呼叫cancelDiscovery函式來取消當前的掃描,最後呼叫startDiscovery函式開始執行掃描操作。
現在已經開始掃描了,下面我們就需要對掃描過程進行監控和對掃描的結果進行處理。即我們所定義的廣播mReceiver,其實現如下所示。
//監聽掃描藍芽裝置
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 device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// 如果已經配對,則跳過,因為他已經在裝置列表中了
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
//否則新增到裝置列表
mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
// 當掃描完成之後改變Activity的title
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
//設定進度條不顯示
setProgressBarIndeterminateVisibility(false);
//設定title
setTitle(R.string.select_device);
//如果計數為0,則表示沒有發現藍芽
if (mNewDevicesArrayAdapter.getCount() == 0) {
String noDevices = getResources().getText(R.string.none_found).toString();
mNewDevicesArrayAdapter.add(noDevices);
}
}
}
};
其中我們通過intent.getAction()可以取得一個動作,然後判斷如果動作為BluetoothDevice.ACTION_FOUND,則表示發現一個藍芽裝置,然後通過BluetoothDevice.EXTRA_DEVICE常量可以取得Intent中的藍芽裝置物件(BluetoothDevice),然後通過條件"device.getBondState() != BluetoothDevice.BOND_BONDED"來判斷裝置是否配對,如果沒有配對則新增到行裝置列表資料來源mNewDevicesArrayAdapter中,另外,當我們取得的動作為BluetoothAdapter.ACTION_DISCOVERY_FINISHED,則表示掃描過程完畢,這時首先需要設定進度條不現實,並且設定視窗的標題為選擇一個裝置(R.string.select_device)。當然如果掃描完成之後沒有發現新的裝置,則新增一個沒有發現新的裝置字串(R.string.none_found)到mNewDevicesArrayAdapter中。
最後,掃描介面上還有一個按鈕,其監聽mDeviceClickListener的實現如下:
// ListViews中所有裝置的點選事件監聽
private OnItemClickListener mDeviceClickListener = new OnItemClickListener() {
public void onItemClick(AdapterView<?> av, View v, int arg2, long arg3) {
// 取消檢測掃描發現裝置的過程,因為內非常耗費資源
mBtAdapter.cancelDiscovery();
// 得到mac地址
String info = ((TextView) v).getText().toString();
String address = info.substring(info.length() - 17);
// 建立一個包括Mac地址的Intent請求
Intent intent = new Intent();
intent.putExtra(EXTRA_DEVICE_ADDRESS, address);
// 設定result並結束Activity
setResult(Activity.RESULT_OK, intent);
finish();
}
};
當用戶點選該按鈕時,首先取消掃描程序,因為掃描過程是一個非常耗費資源的過程,然後去的裝置的mac地址,構建一個Intent 物件,通過附加資料EXTRA_DEVICE_ADDRESS將mac地址傳遞到BluetoothChat中,然後呼叫finish來結束該介面。這時就會回到上一篇文章我們介紹的BluetoothChat中的onActivityResult函式中去執行請求程式碼為REQUEST_CONNECT_DEVICE的片段,用來連線一個裝置。
BluetoothChatService
對於裝置的監聽,連線管理都將由REQUEST_CONNECT_DEVICE來實現,其中又包括三個主要部分,三個程序分貝是:請求連線的監聽執行緒(AcceptThread)、連線一個裝置的程序(ConnectThread)、連線之後的管理程序(ConnectedThread)。同樣我們先熟悉一下該類的成員變數的作用,定義如下:
// Debugging
private static final String TAG = "BluetoothChatService";
private static final boolean D = true;
//當建立socket服務時的SDP名稱
private static final String NAME = "BluetoothChat";
// 應用程式的唯一UUID
private static final UUID MY_UUID = UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");
// 本地藍芽介面卡
private final BluetoothAdapter mAdapter;
//Handler
private final Handler mHandler;
//請求連結的監聽執行緒
private AcceptThread mAcceptThread;
//連結一個裝置的執行緒
private ConnectThread mConnectThread;
//已經連結之後的管理執行緒
private ConnectedThread mConnectedThread;
//當前的狀態
private int mState;
// 各種狀態
public static final int STATE_NONE = 0;
public static final int STATE_LISTEN = 1;
public static final int STATE_CONNECTING = 2;
public static final int STATE_CONNECTED = 3;
Debugging為除錯相關,NAME 是當我們在建立一個socket監聽服務時的一個SDP名稱,另外還包括一個狀態變數mState,其值則分別是下面的"各種狀態"部分,另外還有一個本地藍芽介面卡和三個不同的程序物件,由此可見,本地藍芽介面卡的確是任何藍芽操作的基礎物件,下面我們會分別介紹這些程序的實現。
首先是初始化操作,即建構函式,程式碼如下:
public BluetoothChatService(Context context, Handler handler) {
//得到本地藍芽介面卡
mAdapter = BluetoothAdapter.getDefaultAdapter();
//設定狀態
mState = STATE_NONE;
//設定Handler
mHandler = handler;
}
取得本地藍芽介面卡、設定狀態為STATE_NONE,設定傳遞進來的mHandler。接下來需要控制當狀態改變之後,我們需要通知UI介面也同時更改狀態,下面是得到狀態和設定狀態的實現部分,如下:
private synchronized void setState(int state) {
if (D) Log.d(TAG, "setState() " + mState + " -> " + state);
mState = state;
// 狀態更新之後UI Activity也需要更新
mHandler.obtainMessage(BluetoothChat.MESSAGE_STATE_CHANGE, state, -1).sendToTarget();
}
public synchronized int getState() {
return mState;
}
得到狀態沒有什麼特別的,關鍵在於設定狀態之後需要通過obtainMessage來發送一個訊息到Handler,通知UI介面也同時更新其狀態,對應的Handler的實現則位於BluetoothChat中的private final Handler mHandler = new Handler()部分,從上面的程式碼中,我們可以看到關於狀態更改的之後會發送一個BluetoothChat.MESSAGE_STATE_CHANGE訊息到UI執行緒中,下面我們看一下UI執行緒中如何處理這些訊息的,程式碼如下:
case MESSAGE_STATE_CHANGE:
if(D) Log.i(TAG, "MESSAGE_STATE_CHANGE: " + msg.arg1);
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;
可以看出,當不同的狀態在更改之後會進行不同的設定,但是大多數都是根據不同的狀態設定顯示了不同的title,當已經連結(STATE_CONNECTED)之後,設定了標題為連結的裝置名,並同時還mConversationArrayAdapter進行了清除操作,即清除聊天記錄。
現在,初始化操作已經完成了,下面我們可以呼叫start函式來開啟一個服務程序了,也即是在BluetoothChat中的onResume函式中所呼叫的start操作,其具體實現如下:
public synchronized void start() {
if (D) Log.d(TAG, "start");
// 取消任何執行緒檢視建立一個連線
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
// 取消任何正在執行的連結
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
// 啟動AcceptThread執行緒來監聽BluetoothServerSocket
if (mAcceptThread == null) {
mAcceptThread = new AcceptThread();
mAcceptThread.start();
}
//設定狀態為監聽,,等待連結
setState(STATE_LISTEN);
}
操作過程很簡單,首先取消另外兩個程序,新建一個AcceptThread程序,並啟動AcceptThread程序,最後設定狀態變為監聽(STATE_LISTEN),這時UI介面的title也將更新為監聽狀態,即等待裝置的連線。關於AcceptThread的具體實現如下所示。private class AcceptThread extends Thread {
// 本地socket服務
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
BluetoothServerSocket tmp = null;
// 建立一個新的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;
// 如果當前沒有連結則一直監聽socket服務
while (mState != STATE_CONNECTED) {
try {
//如果有請求連結,則接受
//這是一個阻塞呼叫,將之返回連結成功和一個異常
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:
// 如果狀態為監聽或者正在連結中,,則呼叫connected來連結
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");
}
//關閉BluetoothServerSocket
public void cancel() {
if (D) Log.d(TAG, "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of server failed", e);
}
}
}
首先通過listenUsingRfcommWithServiceRecord建立一個socket服務,用來監聽裝置的連線,當程序啟動之後直到有裝置連線時,這段時間都將通過accept來監聽和接收一個連線請求,如果連線無效則呼叫close來關閉即可,如果連線有效則呼叫connected進入連線程序,進入連線程序之後會取消當前的監聽程序,取消過程則直接呼叫cancel通過mmServerSocket.close()來關閉即可。下面我們分析連線函式connect的實現,如下:
public synchronized void connect(BluetoothDevice device) {
if (D) Log.d(TAG, "connect to: " + device);
// 取消任何連結執行緒,檢視建立一個連結
if (mState == STATE_CONNECTING) {
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
}
// 取消任何正在執行的執行緒
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
// 啟動一個連結執行緒連結指定的裝置
mConnectThread = new ConnectThread(device);
mConnectThread.start();
setState(STATE_CONNECTING);
}
同樣,首先關閉其他兩個程序,然後新建一個ConnectThread程序,並啟動,通知UI介面狀態更改為正在連線的狀態(STATE_CONNECTING)。具體的連線程序由ConnectThread來實現,如下:
private class ConnectThread extends Thread {
//藍芽Socket
private final BluetoothSocket mmSocket;
//藍芽裝置
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
mmDevice = device;
BluetoothSocket tmp = null;
//得到一個給定的藍芽裝置的BluetoothSocket
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();
// 建立一個BluetoothSocket連結
try {
//同樣是一個阻塞呼叫,返回成功和異常
mmSocket.connect();
} catch (IOException e) {
//連結失敗
connectionFailed();
// 如果異常則關閉socket
try {
mmSocket.close();
} catch (IOException e2) {
Log.e(TAG, "unable to close() socket during connection failure", e2);
}
// 重新啟動監聽服務狀態
BluetoothChatService.this.start();
return;
}
// 完成則重置ConnectThread
synchronized (BluetoothChatService.this) {
mConnectThread = null;
}
// 開啟ConnectedThread(正在執行中...)執行緒
connected(mmSocket, mmDevice);
}
//取消連結執行緒ConnectThread
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
在建立該程序時,就已經知道當前需要被連線的藍芽裝置,然後通過createRfcommSocketToServiceRecord可以構建一個藍芽裝置的BluetoothSocket物件,當進入連線狀態時,就可以呼叫cancelDiscovery來取消藍芽的可見狀態,然後通過呼叫connect函式進行連結操作,如果出現異常則表示連結失敗,則呼叫connectionFailed函式通知UI程序更新介面的顯示為連結失敗狀態,然後關閉BluetoothSocket,呼叫start函式重新開啟一個監聽服務AcceptThread,對於連結失敗的處理實現如下:
private void connectionFailed() {
setState(STATE_LISTEN);
// 傳送連結失敗的訊息到UI介面
Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.TOAST, "Unable to connect device");
msg.setData(bundle);
mHandler.sendMessage(msg);
}
首先更改狀態為STATE_LISTEN,然後傳送一個Message帶UI介面,通知UI更新,顯示一個Toast告知使用者,當BluetoothChat中的mHandler接收到BluetoothChat.TOAST訊息時,就會直接更新UI介面的顯示,如果連線成功則將呼叫connected函式進入連線管理程序,其實現如下:
public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
if (D) Log.d(TAG, "connected");
// 取消ConnectThread連結執行緒
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
// 取消所有正在連結的執行緒
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
// 取消所有的監聽執行緒,因為我們已經連結了一個裝置
if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;}
// 啟動ConnectedThread執行緒來管理連結和執行翻譯
mConnectedThread = new ConnectedThread(socket);
mConnectedThread.start();
// 傳送連結的裝置名稱到UI Activity介面
Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.DEVICE_NAME, device.getName());
msg.setData(bundle);
mHandler.sendMessage(msg);
//狀態變為已經連結,即正在執行中
setState(STATE_CONNECTED);
}
首先,關閉所有的程序,構建一個ConnectedThread程序,並準備一個Message訊息,就裝置名稱(BluetoothChat.DEVICE_NAME)也傳送到UI程序,因為UI程序需要顯示當前連線的裝置名稱,當UI程序收到BluetoothChat.MESSAGE_DEVICE_NAME訊息時就會更新相應的UI介面,就是設定視窗的title,這裡我們就不貼出程式碼了,下面我們分析一下ConnectedThread的實現,程式碼如下:
private class ConnectedThread extends Thread {
//BluetoothSocket
private final BluetoothSocket mmSocket;
//輸入輸出流
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket) {
Log.d(TAG, "create ConnectedThread");
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// 得到BluetoothSocket的輸入輸出流
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 {
// 從輸入流中讀取資料
bytes = mmInStream.read(buffer);
// 傳送一個訊息到UI執行緒進行更新
mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
//出現異常,則連結丟失
Log.e(TAG, "disconnected", e);
connectionLost();
break;
}
}
}
/**
* 寫入藥傳送的訊息
* @param buffer The bytes to write
*/
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
// 將寫的訊息同時傳遞給UI介面
mHandler.obtainMessage(BluetoothChat.MESSAGE_WRITE, -1, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "Exception during write", e);
}
}
//取消ConnectedThread連結管理執行緒
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
連線之後的主要操作就是傳送和接收聊天訊息了,因為需要通過其輸入(出)流來操作具體資訊,程序會一直從輸入流中讀取資訊,並通過obtainMessage函式將讀取的資訊以BluetoothChat.MESSAGE_READ命令傳送到UI程序,到UI程序收到是,就需要將其顯示到訊息列表之中,同時對於傳送訊息,需要實行寫操作write,其操作就是將要傳送的訊息寫入到輸出流mmOutStream中,並且以BluetoothChat.MESSAGE_WRITE命令的方式傳送到UI程序中,進行同步更新,如果在讀取訊息時失敗或者產生了異常,則表示連線丟失,這是就呼叫connectionLost函式來處理連線丟失,程式碼如下:
private void connectionLost() {
setState(STATE_LISTEN);
// 傳送失敗訊息到UI介面
Message msg = mHandler.obtainMessage(BluetoothChat.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(BluetoothChat.TOAST, "Device connection was lost");
msg.setData(bundle);
mHandler.sendMessage(msg);
}
操作同樣簡單,首先改變狀態為STATE_LISTEN,然後BluetoothChat.MESSAGE_TOAST命令傳送一個訊息Message到UI程序,通知UI程序更新顯示畫面即可。對於寫操作,是呼叫了BluetoothChatService.write來實現,其實現程式碼如下:
//寫入自己要傳送出來的訊息
public void write(byte[] out) {
// Create temporary object
ConnectedThread r;
// Synchronize a copy of the ConnectedThread
synchronized (this) {
//判斷是否處於已經連結狀態
if (mState != STATE_CONNECTED) return;
r = mConnectedThread;
}
// 執行寫
r.write(out);
}
其實就是檢測,當前的狀態是否處於已經連結狀態STATE_CONNECTED,然後呼叫ConnectedThread 程序中的write操作,來完成訊息的傳送。因此這時我們可以回過頭來看BluetoothChat中的sendMessage的實現了,如下所示:
private void sendMessage(String message) {
// 檢查是否處於連線狀態
if (mChatService.getState() != BluetoothChatService.STATE_CONNECTED) {
Toast.makeText(this, R.string.not_connected, Toast.LENGTH_SHORT).show();
return;
}
// 如果輸入的訊息不為空才傳送,否則不傳送
if (message.length() > 0) {
// Get the message bytes and tell the BluetoothChatService to write
byte[] send = message.getBytes();
mChatService.write(send);
// Reset out string buffer to zero and clear the edit text field
mOutStringBuffer.setLength(0);
mOutEditText.setText(mOutStringBuffer);
}
}
同樣首先檢測了當前的狀態是否為已經連線狀態,然後對要傳送的訊息是否為null進行了判斷,如果為空則不需要傳送,否則呼叫mChatService.write(即上面所說的ConnectedThread 中的wirte操作)來發送訊息。然後一個小的細節就是設定編輯框的內容為null即可。最後我們可以看一下在BluetoothChat中如何處理這些接收到的訊息,主要位於mHandler中的handleMessage函式中,對於狀態改變的訊息我們已經分析過了,下面是其他幾個訊息的處理:
case MESSAGE_WRITE:
byte[] writeBuf = (byte[]) msg.obj;
// 將自己寫入的訊息也顯示到會話列表中
String writeMessage = new String(writeBuf);
mConversationArrayAdapter.add("Me: " + 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:
// 儲存連結的裝置名稱,並顯示一個toast提示
mConnectedDeviceName = msg.getData().getString(DEVICE_NAME);
Toast.makeText(getApplicationContext(), "Connected to "
+ mConnectedDeviceName, Toast.LENGTH_SHORT).show();
break;
case MESSAGE_TOAST:
//處理連結(傳送)失敗的訊息
Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST),
Toast.LENGTH_SHORT).show();
break;
分別是讀取訊息和寫訊息(傳送訊息),對於一些資訊提示訊息MESSAGE_TOAST,則通過Toast顯示出來即可。如果訊息是裝置名稱MESSAGE_DEVICE_NAME,則提示使用者當前連線的裝置的名稱。對於寫訊息(MESSAGE_WRITE)和讀訊息(MESSAGE_READ)我們就不重複了,大家看看程式碼都已經加入了詳細的註釋了。
最後當我們在需要停止這些程序時就看有直接呼叫stop即可,具體實現如下:
//停止所有的執行緒
public synchronized void stop() {
if (D) Log.d(TAG, "stop");
if (mConnectThread != null) {mConnectThread.cancel(); mConnectThread = null;}
if (mConnectedThread != null) {mConnectedThread.cancel(); mConnectedThread = null;}
if (mAcceptThread != null) {mAcceptThread.cancel(); mAcceptThread = null;}
//狀態設定為準備狀態
setState(STATE_NONE);
}
分別檢測三個程序是否為null,然後呼叫各自的cancel函式來取消程序,最後不要忘記將狀態恢復到STATE_NONE即可。
總結
終於完成了對藍芽聊天程式的實現和分析,該示例程式比較全面,基本上包括了藍芽程式設計的各個方面,希望通過這幾篇文章的問題,能夠幫助大家理解在Android平臺上進行藍芽程式設計,同時將藍芽技術運用到其他應用程式中實現應用程式的網路化,聯機性。或許你有更多的用處。