藍牙(Bluetooth)是丹麥國王Harald的綽號。Sun開發者網站的文章(http://developers.sun.com/mobility/midp/articles/bluetooth1/)中有關於藍牙的各種信息,其中一些可能是為了紀念Harald後人杜撰的,比如:
Harald Christianized the Danes
Harald controlled Denmark and Norway
Harald thinks notebooks and cellular phones should communicate seamlessly
為了說明如何在應用中使用Android的Bluetooth類,將創建一些工具,連接到藍牙設備,並傳輸數據。這個示例代碼是基於Android SDK的BluetoothChat示例修改的。修改後通用性更強,可以涵蓋更多的Bluetooth應用,並更易於在應用中使用。
在對Android的Bluetooth API進行探索的過程中,我們將具體查看如何使用這些API,以及在具體的應用中如何使用這些代碼來實現自己的功能,包括Bluetooth開發的診斷工具。
首先,將進一步瞭解Bluetooth如何工作,以及其在Android中如何實現。
藍牙協議棧
本節主要討論關於Bluetooth協議棧的標準和協議(如圖18-1所示)。這些協議和標準是Bluetooth的特徵:Bluetooth要移動的數據、要同時連接的設備及延遲等。
藍牙已經成為獨立的網絡形式,它是個人局域網(personal area network),或稱PAN,也稱為微微網(piconet)。在設計上,藍牙最多連接8個設備,每秒鐘最多傳輸3MB數據。連接的設備必須是近距離的,大約在10m內。Bluetooth在非常低的功率下工作,功率是毫瓦級的。這意味著小電池可以持續很長時間:很小的、輕量級的Bluetooth耳機可以持續通話幾個小時,這幾乎和你的手機耳機的續航時間一樣長,而手機耳機的電池要大得多,因為無線通信信號必須能夠達到相對而言更遠的距離。
圖18-1:Android藍牙協議棧
可以使用藍牙的各種設備,包括中低數據傳輸速率的設備,如鍵盤、鼠標、平板電腦、打印機、揚聲器、耳機以及可能和其他外圍設備交流的手機和個人電腦設備等。藍牙支持PC和手機之間進行連接。
藍牙特定的協議和adopted協議
考慮藍牙協議棧的一種有用方法是把藍牙特定的協議和運行在藍牙上的adopted協議分離開。總體來說,藍牙協議和adopted協議可能是極為複雜的,但是如果分別考慮,如運行在藍牙上的OBEX和TCP/IP大的、複雜的協議,就會更易於理解。因此,我們將從藍牙較低層的協議開始,集中探討這些分層在藍牙的使用中是如何工作的。
另一種有用的設想中的藍牙模型用於取代串行端口。這意味著藍牙的較低層可以模擬用於支持你管理外圍設備的虛擬串行電纜。這就是我們所使用的藍牙協議類型。反過來,它又支持我們使用簡單的java.io類的InputStream和OutputStream讀寫數據。
BlueZ:Linux的藍牙實現
和其他需要連接到電腦和手機的外圍設備不同,我們希望手機能連接到所有類型的藍牙設備。這意味著手機需要相當完整的藍牙和adopted協議實現,支持建立和管理連接的必要交互,以及通過藍牙進行通信的應用。
Android使用BlueZ藍牙協議棧,BlueZ是Linux上最常用的藍牙協議棧。它取代了名為Open BT的項目。關於BlueZ的信息可以在BlueZ項目網站上獲取:http://www.bluez.org。
BlueZ是Qualcomm公司開發的,並被Linux內核採用。BlueZ項目是在2001年開始的,一直是一個活躍的、得到廣泛支持的項目。因此,BlueZ是穩定的、可兼容的實現——這也是Linux成為手機操作系統的原因之一。
在Android應用中使用藍牙
在Android中使用藍牙意味著使用在其中封裝了藍牙功能的類:Bluez協議棧提供了模擬設備、監聽連接及使用連接的方式;java.io包提供了讀寫數據的類;Handler類和Message類提供了橋接分別負責藍牙的輸入、輸出和用戶界面的各個管理線程的方式。我們一起來看代碼及這些類是如何使用的。
編譯和運行這段代碼會使你大概瞭解Android的Bluetooth類可以為需要和附近的設備連接的應用提供了什麼功能。
嘗試藍牙應用的第一步是把手機和PC連接起來。然後,需要程序監測PC通過藍牙接收到了什麼,看你在應用中發送的哪些內容被傳到了PC上。在這個例子中,我們使用的是Linux工具hcidump。
如果想設置幾個斷點並執行單步調試,尤其是在應用打開和接受連接那部分,可以在調試模式下啟動程序。可以通過PC在Linux或應用中使用Blueman applet創建連接。創建連接之後,在終端執行hcidump命令,查看在PC中接收到的內容。使用下面幾個參數控制只顯示藍牙連接的內容:
sudo hcidump -a -R
現在,你在設備中發送內容之後,其會顯示在PC上的hcidump輸出中。
藍牙及相關的I/O類
該程序依賴BluetoothAdapter類控制設備的Bluetooth適配器、控制連接設備狀態的BluetoothDevice類及負責監聽socket並創建連接的BluetoothSocket類。
package com.finchframework.bluetooth; import android.os.Handler; import android.os.Message; public class BtHelperHandler extends Handler { public enum MessageType { STATE, READ, WRITE, DEVICE, NOTIFY; } public Message obtainMessage(MessageType message, int count, Object obj) { return obtainMessage(message.ordinal, count, -1, obj); } public MessageType getMessageType(int ordinal) { return MessageType.values[ordinal]; } }
BtHelperHandler類定義一些常量,並提供一些封裝功能,使得消息相關的方法代碼更簡潔。
BtSPPHelper.java封裝了藍牙序列號協議(Serial Port Protocol,SPP)的使用:
package com.finchframework.bluetooth; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.UUID; import com.finchframework.finch.R; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.os.Bundle; import android.os.Message; import android.util.Log; /** * Helper class that runs AsyncTask objects for communicating with a Bluetooth * device. This code is derived from the Bluetoothchat example, but modified in * several ways to increase modularity and generality: The Handler is in a * separate class to make it easier to drop into other components. * * Currently this only does Bluetooth SPP. This can be generalized to other * services. */ public class BtSPPHelper { // Debugging private final String TAG = getClass.getSimpleName; private static final boolean D = true; public enum State { NONE, LISTEN, CONNECTING, CONNECTED; } // Name for the SDP record when creating server socket private static final String NAME = "BluetoothTest"; // Unique UUID for this application private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); // Member fields private final BluetoothAdapter mAdapter; private final BtHelperHandler mHandler; private AcceptThread mAcceptThread; private ConnectThread mConnectThread; private ConnectedThread mConnectedThread; private State mState; private Context mContext; /** * Constructor. Prepares a new Bluetooth SPP session. * @param context The UI Activity Context * @param handler A Handler to send messages back to the UI Activity */ public BtSPPHelper(Context context, BtHelperHandler handler) { mContext = context; mAdapter = BluetoothAdapter.getDefaultAdapter; mState = State.NONE; mHandler = handler; } /** * Set the current state of the chat connection * @param state The current connection state */ private synchronized void setState(State state) { if (D) Log.d(TAG, "setState " + mState + " -> " + state); mState = state; // Give the new state to the Handler so the UI Activity can update mHandler.obtainMessage(BtHelperHandler.MessageType.STATE, -1, state).sendToTarget; } /** * Return the current connection state. */ public synchronized State getState { return mState; } /** * Start the session. Start AcceptThread to begin a * session in listening (server) mode. * * Typically, call this in onResume */ public synchronized void start { if (D) Log.d(TAG, "start"); // Cancel any thread attempting to make a connection if (mConnectThread != null) {mConnectThread.cancel; mConnectThread = null;} // Cancel any thread currently running a connection if (mConnectedThread != null) { mConnectedThread.cancel; mConnectedThread = null; } // Start the thread to listen on a BluetoothServerSocket if (mAcceptThread == null) { mAcceptThread = new AcceptThread; mAcceptThread.start; } setState(State.LISTEN); } /** * Start the ConnectThread to initiate a connection to a remote device. * @param device The BluetoothDevice to connect */ public synchronized void connect(BluetoothDevice device) { if (D) Log.d(TAG, "connect to: " + device); // Cancel any thread attempting to make a connection if (mState == State.CONNECTING) { if (mConnectThread != null) { mConnectThread.cancel; mConnectThread = null; } } // Cancel any thread currently running a connection if (mConnectedThread != null) { mConnectedThread.cancel; mConnectedThread = null; } // Start the thread to connect with the given device mConnectThread = new ConnectThread(device); mConnectThread.start; setState(State.CONNECTING); } /** * Start the ConnectedThread to begin managing a Bluetooth connection * * @param socket * The BluetoothSocket on which the connection was made * @param device * The BluetoothDevice that has been connected */ private synchronized void connected(BluetoothSocket socket, BluetoothDevice device) { if (D) Log.d(TAG, "connected"); // Cancel the thread that completed the connection if (mConnectThread != null) { mConnectThread.cancel; mConnectThread = null; } // Cancel any thread currently running a connection if (mConnectedThread != null) { mConnectedThread.cancel; mConnectedThread = null; } // Cancel the accept thread because we only want to connect to one // device if (mAcceptThread != null) { mAcceptThread.cancel; mAcceptThread = null; } // Start the thread to manage the connection and perform transmissions mConnectedThread = new ConnectedThread(socket); mConnectedThread.start; // Send the name of the connected device back to the UI Activity mHandler.obtainMessage(BtHelperHandler.MessageType.DEVICE, -1, device.getName).sendToTarget; setState(State.CONNECTED); } /** * Stop all threads */ 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); } /** * Write to the ConnectedThread in an unsynchronized manner * @param out The bytes to write * @see ConnectedThread#write(byte) */ public void write(byte out) { ConnectedThread r; // Synchronize a copy of the ConnectedThread synchronized (this) { if (mState != State.CONNECTED) return; r = mConnectedThread; } // Perform the write unsynchronized r.write(out); } private void sendErrorMessage(int messageId) { setState(State.LISTEN); mHandler.obtainMessage(BtHelperHandler.MessageType.NOTIFY, -1, mContext.getResources.getString(messageId)).sendToTarget; } /** * This thread listens for incoming connections. */ private class AcceptThread extends Thread { // The local server socket private final BluetoothServerSocket mmServerSocket; public AcceptThread { BluetoothServerSocket tmp = null; // Create a new listening server socket try { tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, SPP_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; // Listen to the server socket if we're not connected while (mState != BtSPPHelper.State.CONNECTED) { try { // This is a blocking call and will only return on a // successful connection or an exception socket = mmServerSocket.accept; } catch (IOException e) { Log.e(TAG, "accept failed", e); break; } // If a connection was accepted if (socket != null) { synchronized (BtSPPHelper.this) { switch (mState) { case LISTEN: case CONNECTING: // Situation normal. Start the connected thread. connected(socket, socket.getRemoteDevice); break; case NONE: case CONNECTED: // Either not ready or already connected. // Terminate new 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 { if (D) Log.d(TAG, "cancel " + this); try { mmServerSocket.close; } catch (IOException e) { Log.e(TAG, "close of server failed", e); } } } /** * This thread runs while attempting to make an outgoing connection * with a device. It runs straight through; the connection either * succeeds or fails. */ private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) { mmDevice = device; BluetoothSocket tmp = null; // Get a BluetoothSocket for a connection with the // given BluetoothDevice try { tmp = device.createRfcommSocketToServiceRecord(SPP_UUID); } catch (IOException e) { Log.e(TAG, "create failed", e); } mmSocket = tmp; } public void run { Log.i(TAG, "BEGIN mConnectThread"); setName("ConnectThread"); // Always cancel discovery because it will slow down a connection mAdapter.cancelDiscovery; // Make a connection to the BluetoothSocket try { // This is a blocking call and will only return on a // successful connection or an exception mmSocket.connect; } catch (IOException e) { sendErrorMessage(R.string.bt_unable); // Close the socket try { mmSocket.close; } catch (IOException e2) { Log.e(TAG, "unable to close socket during connection failure", e2); } // Start the service over to restart listening mode BtSPPHelper.this.start; return; } // Reset the ConnectThread because we're done synchronized (BtSPPHelper.this) { mConnectThread = null; } // Start the connected thread connected(mmSocket, mmDevice); } public void cancel { try { mmSocket.close; } catch (IOException e) { Log.e(TAG, "close of connect socket failed", e); } } } /** * This thread runs during a connection with a remote device. * It handles all incoming and outgoing transmissions. */ 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; InputStream tmpIn = null; OutputStream tmpOut = null; // Get the BluetoothSocket input and output streams 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; // Keep listening to the InputStream while connected while (true) { try { // Read from the InputStream bytes = mmInStream.read(buffer); // Send the obtained bytes to the UI Activity mHandler.obtainMessage(BtHelperHandler.MessageType.READ, bytes, buffer).sendToTarget; } catch (IOException e) { Log.e(TAG, "disconnected", e); sendErrorMessage(R.string.bt_connection_lost); break; } } } /** * Write to the connected OutStream. * @param buffer The bytes to write */ public void write(byte buffer) { try { mmOutStream.write(buffer); // Share the sent message back to the UI Activity mHandler.obtainMessage(BtHelperHandler.MessageType.WRITE, -1, buffer) .sendToTarget; } catch (IOException e) { Log.e(TAG, "Exception during write", e); } } public void cancel { try { mmSocket.close; } catch (IOException e) { Log.e(TAG, "close of connect socket failed", e); } } } }
BtSPPHelper類把這些類的使用結合起來,還另外包含了private Thread子類的定義,用於監聽、建立連接及維護連接。
這也是java.io包滿足Android藍牙之處:Bluetooth Socket對像包含的方法會返回InputStream對像和Output Stream對象的引用,這些引用可以對socket數據進行讀寫:
package com.finchframework.bluetooth; import java.util.Set; import com.finchframework.finch.R; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.Window; import android.view.View.OnClickListener; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import android.widget.AdapterView.OnItemClickListener; /** * Derived from the BluetoothChat example, an activity that enables * picking a paired or discovered Bluetooth device */ 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"; // Member fields private BluetoothAdapter mBtAdapter; private ArrayAdapter<String> mPairedDevicesArrayAdapter; private ArrayAdapter<String> mNewDevicesArrayAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Set up the window setContentView(R.layout.device_list); // Set result CANCELED in case the user backs out setResult(Activity.RESULT_CANCELED); // Initialize the button to perform device discovery Button scanButton = (Button) findViewById(R.id.button_scan); scanButton.setOnClickListener(new OnClickListener { public void onClick(View v) { doDiscovery; v.setVisibility(View.GONE); } }); // Initialize array adapters. One for already paired devices and // one for newly discovered devices mPairedDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name); mNewDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name); // Find and set up the ListView for paired devices ListView pairedListView = (ListView) findViewById(R.id.paired_devices); pairedListView.setAdapter(mPairedDevicesArrayAdapter); pairedListView.setOnItemClickListener(mDeviceClickListener); // Find and set up the ListView for newly discovered devices ListView newDevicesListView = (ListView) findViewById(R.id.new_devices); newDevicesListView.setAdapter(mNewDevicesArrayAdapter); newDevicesListView.setOnItemClickListener(mDeviceClickListener); // Register for broadcasts when a device is discovered IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); this.registerReceiver(mReceiver, filter); // Register for broadcasts when discovery has finished filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); this.registerReceiver(mReceiver, filter); // Get the local Bluetooth adapter mBtAdapter = BluetoothAdapter.getDefaultAdapter; // Get a set of currently paired devices Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices; // If there are paired devices, add each one to the 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); } } @Override protected void onDestroy { super.onDestroy; // Make sure we're not doing discovery anymore if (mBtAdapter != null) { mBtAdapter.cancelDiscovery; } // Unregister broadcast listeners this.unregisterReceiver(mReceiver); } /** * Start device discover with the BluetoothAdapter */ private void doDiscovery { if (D) Log.d(TAG, "doDiscovery"); // Indicate scanning in the title setProgressBarIndeterminateVisibility(true); setTitle(R.string.scanning); // Turn on sub-title for new devices findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE); // If we're already discovering, stop it if (mBtAdapter.isDiscovering) { mBtAdapter.cancelDiscovery; } // Request discover from BluetoothAdapter mBtAdapter.startDiscovery; } // The on-click listener for all devices in the ListViews 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; // Get the device MAC address, which is the last 17 chars in the View String info = ((TextView) v).getText.toString; String address = info.substring(info.length - 17); // Create the result Intent and include the MAC address Intent intent = new Intent; intent.putExtra(EXTRA_DEVICE_ADDRESS, address); // Set result and finish this Activity setResult(Activity.RESULT_OK, intent); finish; } }; // The BroadcastReceiver that listens for discovered devices and // changes the title when discovery is finished private final BroadcastReceiver mReceiver = new BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction; // When discovery finds a device if (BluetoothDevice.ACTION_FOUND.equals(action)) { // Get the BluetoothDevice object from the Intent BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); // If it's already paired, skip it, because it's been listed already if (device.getBondState != BluetoothDevice.BOND_BONDED) { mNewDevicesArrayAdapter.add( device.getName + "\n" + device.getAddress); } // When discovery is finished, change the Activity title } 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); } } } }; }
DeviceListActivity類
活動顯示一個對話框,該對話框會列出已知的設備,並支持用戶掃瞄請求設備。不同於一些應用使用Thread子類實現異步I/O並通過Handler子類傳遞結果給UI線程,BluetoothAdapter的startDiscovery方法啟動了一個專用的線程並通過廣播intent實現數據通信以傳遞結果。這裡,BroadcastReceiver負責處理這些結果。
BtConsoleActivity類
BtConsoleActivity類創建了一個類似聊天工具的activity來和藍牙設備進行交互。該activity菜單支持連接到設備上,該activity的main視圖是發送和接收到的數據的滾動列表。在屏幕的下方,有一個EditText視圖,用於輸入要發送到另一個SPP連接端的文本。
Handler類用於把單線程的UI和負責在socket上監聽、創建連接和執行IO操作的線程結合起來。