2013年3月19日 星期二

[Android] Android Developer Note Connectivity 1

Bluetooth   (見Android Developer Bluetooth)

序言
Android平台支援了藍芽連線stack,允許裝置無線地和其他藍芽裝置交換資料。
app的框架透過Android Bluetooth APIs提供藍芽存取的機能,
這些這些API讓app能使用藍芽裝置來開啟點對點或多點間的無線功能。

使用藍芽的API,app可以做到下面幾件事:
a. 藍芽裝置掃描
b. 跟本地藍芽對接器要求尋找藍芽配對
c. 建立RFCOMM的頻道
d. 透過服務探索(service discovery)來連接其他裝置
e. 上/下傳輸資料至其他裝置
f.  管理多重的連線

註: RFCOMM通訊協定是基於L2CAP通訊協定上提供序列埠(RS-232)的模擬功能。
L2CAP -> Logical Link Control and Adaptation Protocol

The Basics
這篇會描述Android Bluetooth APIs提供四個主要的必要工作,用來溝通藍芽連線:
a. 設定藍芽
b. 尋找當地的可用或已配對的裝置
c. 連結裝置
d. 裝置之前的資料傳送/接收

所有的藍芽API都在android.bluetooth的package裡提供。
底下是對於做藍芽連線所需的一些物件(classes)和介面(interfaces)。

BluetoothAdapter
    代表本地的藍芽配對器,可以當作給藍芽交流的進入點。
透過它可以探索其他的藍芽裝置並尋求配對、
和已知MAC位址的裝置連線(實例化BluetoothDevice),
或開BluetoothServerSocket來監聽和其他裝置的溝通。

BluetoothDevice
    代表遠端的藍芽裝置,用它來取得和遠端裝置的連線(透過BluetoothSocket),
或者要求取得裝置的資訊(名稱、位址、class、和連結狀態)

BluetoothSocket
    代表類似TCP Socket的一個藍芽socket介面。這是連線的接點,
允許app和別的藍芽裝置交換資料(透過 InputStream 和 OutputStream)

BluetoothServerSocket
    代表類似TCP的ServerSocket,在Android devices的連接中,
其中一個必須要當server(並在自己的class中開server socket)。
遠端的藍芽裝置發出連線要求時,如果連線被接受(accepted),
BluetoothServerSocket會回傳一個連線成功的BluetoothSocket。

BluetoothClass
    描述了一個藍芽裝置整體的特性和機能。
這是一組定義裝置的主要和非主要的classes和services屬性(properties),
這並不是完全可靠地描述全部的藍芽裝置所能支援的服務,
但可以當作裝置種類的有用提示。

BluetoothProfile
    代表藍芽資料(profile)的介面,可以定義裝置間藍芽連線的規範設定。
BluetoothHeadset
    對手機的耳機支援。
BluetoothA2dp
    高音質的藍芽連線,A2dp表"Advanced Audio Distribution Profile"
(這四個就別管它了......)
BluetoothHealth
    代表一個操縱藍芽服務的Health Device(例如血壓計嗎? 我不知道= =)Profile的proxy。
BluetoothHealthCallback
    抽象的class,用來實作前面的BluetoothHealth的callback。
BluetoothHealthAppConfiguration
    對前面相應的app設定,第三方支援等。
BluetoothProfile.ServiceListener
    和IPC clients相應的連/斷線相關

Bluetooth Permissions
要使用藍芽功能,必須要最少開放這兩個權限:BLUETOOTHBLUETOOTH_ADMIN
BLUETOOTH權限用來執行藍芽溝通(要求/接受連線,以及傳輸資料)。
BLUETOOTH_ADMIN權限用來執行裝置探索或操作藍芽設定。因為一般來說,
藍芽裝置都需要去搜尋到當地(local)藍芽的裝置(阿不然怎麼用= =)
除了power manager當使用者請求時,會修改到藍芽需求。

定義藍芽的許可需求要在app中的manifest增加<uses-permission>。
例:

<manifest ... >
  <uses-permission android:name="android.permission.BLUETOOTH" />
  ...
</manifest>


Setting Up Bluetooth
開始之前要確認藍芽在裝置上是否支援且打開。
不支援的話當然你可以優雅地(XDDDD)關掉所有藍芽功能囉!但如果支援卻是關閉的話,
app會在要用到藍芽功能時對使用者進行request(不會離開程式)。
設定上是使用BluetoothAdapter

1. 取得匹配器
對每個藍芽的activity來說,BluetoothAdapter都是必要的。
使用static的getDefaultAdapter()來拿到裝置自己的匹配器,
接下來就將它當成一個物件來互動就可以了。
如果回傳是null表示這台裝置不支援藍芽,那你的故事就到此為止Game Over了喵!
舉例來說:

BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
    // Device does not support Bluetooth
}

2. 打開藍芽
接著我們要確保藍芽是打開的,透過isEnabled()來確認現況,若是false的話我們就得打開它。
使用startActivityForResult()並將ACTION_REQUEST_ENABLE的action Intent丟進去。
在app的表現就是會出現一個request要求使用者在app中授權打開藍芽(而不會跳出程式)

舉例來說:

if (!mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

對話框會像上面那張圖一樣出現要求授權。
REQUEST_ENABLE_BT常數是本地定義的大於0的integer,也就是,
系統在你onActivityResult()的實作中回傳的requestCode參數。
成功的話我們會得到一個RESULT_OK的result code,否則會得到RESULT_CANCELED。

接下來這邊是可做可不做的,你的app也可以監聽ACTION_STATE_CHANGED的廣播Intent,
因為系統在藍芽狀態改變時會發出廣播。當中會包含extra fields EXTRA_STATE和
EXTRA_PREVIOUS_STATE,分別包含新舊兩個藍芽的狀態。
可能的值有:
STATE_TURNING_ON、STATE_ON、STATE_TURNING_OFF、STATE_OFF。
當app在執行時監聽這個廣播有助於偵測藍芽狀態的變動。

Querying paired devices
在裝置探索前先去query那些配對的(paired)裝置會是一個挺值得的選擇。
我們可以使用getBondedDevices()來達成。
這函式會回傳一組代表配對的BluetoothDevices,
舉例來說,我們可以用ArrayAdapter來顯示出每個裝置的名稱:


Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// If there are paired devices
if (pairedDevices.size() > 0) {
    // Loop through paired devices
    for (BluetoothDevice device : pairedDevices) {
        // Add the name and address to an array adapter to show in a ListView
        mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
    }
}

所有從BluetoothDevice的Object所需用來開始一個連線的就只有MAC address而已。
上面範例中這項資訊和裝置名稱被一起存到mArrayAdapter。
之後MAC address可以被分離出來用作開始連線。

Discovering devices
開始探索 -> startDiscovery()
它會回傳一個boolean來表探索是否成功開始,探索一般約12秒,
並回傳一頁找到的裝置及取得的藍芽名稱。
你的app必須註冊一個BroadcastReceiver給ACTION_FOUND Intent,
來接收每個被發現的裝置的資訊。
對每個(被發現的)裝置來說系統會廣播ACTION_FOUND的Intent,
Intent中帶了extra fields: EXTRA_DEVICE和EXTRA_CLASS,
分別包含了一個BluetoothDevice和一個BluetoothClass。
舉例來說:


// Create a BroadcastReceiver for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    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);
            // Add the name and address to an array adapter to show in a ListView
            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
        }
    }
};
// Register the BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy

同樣的,所有從BluetoothDevice的Object所需用來開始一個連線的就只有MAC address而已。

上面範例中這項資訊和裝置名稱被一起存到mArrayAdapter。
之後MAC address可以被分離出來用作開始連線。

(這點完全跟上一個小標題重複)

值得注意的是裝置探索對藍芽匹配器是很吃重的工作,且會吃掉很大量的資源。
如果要關掉可以使用cancelDiscovery()。
注意在連線的時候因為探索會大量降低頻寬,所以這時別開著探索。

Eabling discoverability
如果想讓本地裝置能被探索到->startActivityForResult(Intent, int),用ACTION_REQUEST_DISCOVERABLE的action Intent。

這也會像前面的模式一樣,要求開啟權限來進行接下來的能被探索的功能。
預設為120秒,可以用extra加進EXTRA_DISCOVERABLE_DURATION。
最常是3600秒,0的話表設定成一直可被探索不關掉。
下面的範例設定為300秒:

Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);



同樣如果使用者不同意的話onActivityResult()會回傳RESULT_CANCELLED。
我們也可以監聽ACTION_SCAN_MODE_CHANGED的Intent來偵測目前的狀況。
得到的狀態會有三種,分別表可見可連結、可連結、不可見,
就不再贅述。

Connecting Devices
為了在你的app中連結兩個裝置,server和client都得要實作,
因為必然要是一邊server一邊client。client是拿server的MAC位址來開始連線。
當兩個間有一個BluetoothSocket在同一個RFCOMM頻道上時,
我們將兩者視為是彼此連結的。
當兩邊都擁有server機制的時候,兩邊都可以開始連線並且作為client(另一邊則作為server)。

Connecting as a server
Server必須要拿著一個開放的BluetoothServerSocket,
監聽進來的連線請求。當其中有被接受的時候,提供一個連線的BluetoothSocket。
(用來跟Client溝通的管道)
接著Server就可以丟掉了,除非你要接受更多的連線。
對藍芽來說,是使用listenUsingRfcommWithServiceRecord(String, UUID)來取得BluetoothServerSocket的。(UUID是一個128-bit format的string ID)

大致上:
拿到ServerSocket->開始嘗試accept->接到socket後就透過其開始溝通client。
除了拿到ServerSocket方式不一樣以外,
其他基本上和Java TCP的連線作法如出一轍。

範例:

private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;
 
    public AcceptThread() {
        // Use a temporary object that is later assigned to mmServerSocket,
        // because mmServerSocket is final
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID is the app's UUID string, also used by the client code
            tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) { }
        mmServerSocket = tmp;
    }
 
    public void run() {
        BluetoothSocket socket = null;
        // Keep listening until exception occurs or a socket is returned
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }
            // If a connection was accepted
            if (socket != null) {
                // Do work to manage the connection (in a separate thread)
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }
 
    /** Will cancel the listening socket, and cause the thread to finish */
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) { }
    }
}

值得注意的是manageConnectedSocket()是一個fictional method,
它會開一個thread來做資料傳輸。
然後最後當然不要忘記關掉連線。

Connecting as a client
client取得socket的方式是使用BluetoothDevice的createRfcommSocketToServiceRecord(UUID),
來得到一個回傳的BluetoothSocket。
然後接著要藉由connet()來連線,一般而言如果過12秒的話就會視為time out並丟出例外。
範例如下:

 private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;
 
    public ConnectThread(BluetoothDevice device) {
        // Use a temporary object that is later assigned to mmSocket,
        // because mmSocket is final
        BluetoothSocket tmp = null;
        mmDevice = device;
 
        // Get a BluetoothSocket to connect with the given BluetoothDevice
        try {
            // MY_UUID is the app's UUID string, also used by the server code
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }
 
    public void run() {
        // Cancel discovery because it will slow down the connection
        mBluetoothAdapter.cancelDiscovery();
 
        try {
            // Connect the device through the socket. This will block
            // until it succeeds or throws an exception
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and get out
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }
 
        // Do work to manage the connection (in a separate thread)
        manageConnectedSocket(mmSocket);
    }
 
    /** Will cancel an in-progress connection, and close the socket */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}


注意在cancelDiscovery()是在連線前叫的,就如上面講過的相關問題,
在確保有連線要開始的時候就該先關掉探索。
而且這個函式不論原本有沒有關探索都可以安全的呼叫。
manageConnectedSocket()和close()和在server裡的用意是一樣的。

Managing a Connection
這裡同樣跟Java TCP非常相似,我們透過InputStream和OutputStream來進行資料傳輸,
然後一般是使用read(byte[])和write(byte[])來取得串流。
(如果真的需要的話也可以再外加上BufferedReader和BufferedWriter)
範例:

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;
 
        // Get the input and output streams, using temp objects because
        // member streams are final
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }
 
        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }
 
    public void run() {
        byte[] buffer = new byte[1024];  // buffer store for the stream
        int bytes; // bytes returned from read()
 
        // Keep listening to the InputStream until an exception occurs
        while (true) {
            try {
                // Read from the InputStream
                bytes = mmInStream.read(buffer);
                // Send the obtained bytes to the UI activity
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }
 
    /* Call this from the main activity to send data to the remote device */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }
 
    /* Call this from the main activity to shutdown the connection */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

建構子(constructor)會去要求必要的串流,且一旦執行,
thread就會開始等資料從InputStream進來。(並且read)
往外送的資料則是write()的method(並且write)。
同樣不要忘記要在cancel()的地方將socket做關閉。

Working with Profiles
從Android 3.0版本開始Bluetooth API包含了支援使用Bluetooth profiles的做法。
Bluetooth profile就是一個無線的介面用來定位裝置間基於藍芽的溝通。
比如免持的(Hands-Free)profile。對於手機連線無線耳機來說,
兩個裝置都必須要支援Hands-Free profile。

如同最上面介紹的,Android Bluetooth API提供了實作介面給下面的Bluetooth profiles:
Headset(耳機、免持)、A2DP(無線高音質的藍芽串流)、
Heath Device(API leve 14,即4.0以上支援,可以連接心跳、血液、熱感等等的偵測器)

使用profile的基礎的步驟:
1. 拿預設的匹配器
2. 用getProfileProxy()來建立連線並關聯profile(下面的例子是跟Headset)
3. 設置好一個BluetoothProfile.ServiceListener。當連結或斷線時,
這監聽器會發出通知給BluetoothProfile IPC clients。
4. 在onServiceConnected()裡,處理profile proxy object。
5. 一旦拿到了profile proxy object,你可以用它來監控連線的狀況,
並且執行其他的相關於該profile的操作。
下面範例介紹連線到一個Bluetooth proxy object以控制Headset profile的方法:

BluetoothHeadset mBluetoothHeadset;
 // Get the default adapter
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 // Establish connection to the proxy.
mBluetoothAdapter.getProfileProxy(context, mProfileListener, BluetoothProfile.HEADSET);
 private BluetoothProfile.ServiceListener mProfileListener = new BluetoothProfile.ServiceListener() {
    public void onServiceConnected(int profile, BluetoothProfile proxy) {
        if (profile == BluetoothProfile.HEADSET) {
            mBluetoothHeadset = (BluetoothHeadset) proxy;
        }
    }
    public void onServiceDisconnected(int profile) {
        if (profile == BluetoothProfile.HEADSET) {
            mBluetoothHeadset = null;
        }
    }
};
 // ... call functions on mBluetoothHeadset
 // Close proxy connection after use.
mBluetoothAdapter.closeProfileProxy(mBluetoothHeadset);

Vendor-specific AT commands
從Android 3.0版本開始,app可以註冊接收先由耳機傳送,
廠商定義的AT commands的系統廣播。
接收ACTION_VENDOR_SPECIFIC_HEADSET_EVENT intent的廣播可以處理之。

Health Device Profile
Android 4.0版本引入了對於藍芽健康Profile(HDP)的支援。
這可以讓你做出可以連線溝通Health Device並支援藍芽的app。
對於其API的概念,可以參照如下表格:
ConceptDescription
SourceA role defined in HDP. A source is a health device that transmits medical data (weight scale, glucose meter, thermometer, etc.) to a smart device such as an Android phone or tablet.
SinkA role defined in HDP. In HDP, a sink is the smart device that receives the medical data. In an Android HDP application, the sink is represented by a BluetoothHealthAppConfiguration object.
RegistrationRefers to registering a sink for a particular health device.
ConnectionRefers to opening a channel between a health device and a smart device such as an Android phone or tablet.

如果要做一個HDP的app:
1. 取得BluetoothHealth proxy object的參照(reference)
2. 新開一個BluetoothHealthCallback,
並註冊app configuration(BluetoothHealthAppConfiguration)作為health sink。
3. 和HD做連線,一些裝置會初始連線,對那些裝置而言這步就不需要執行了。
4. 當成功連結了HD,使用file descriptor來讀寫資料。
取得的資料必須要使用以IEEE 11073-xxxxx規範來實作的health manager來轉譯。
5. 當結束時,關掉health channel並反註冊app。當沒額外活動時,channel也會被關掉。




沒有留言:

張貼留言