1. 程式人生 > >11. 藍芽通訊

11. 藍芽通訊

##11.1 問題
在應用程式中,要通過藍芽通訊實現不同裝置之間的資料傳輸。

##11.2 解決方案
(API Level 5)
可以使用API Level5中引入的藍芽API,在射頻通訊(RFCCOMM)協議介面上建立一個點對點的連線。藍芽是一種非常流行的無線電技術,幾乎現在所有的移動裝置都支援該技術。很多使用者認為藍芽只能用來連線移動裝置與無線耳機或者與車載音響系統整合。但實際上,對於開發人員來說,在應用程式中藍芽還是一種用來建立點對點連線的簡單的高效的方式。

##11.3 實現機制
要點
Android模擬器現在還不支援藍芽,因此要想執行本例中的程式碼,必須把它們執行在一臺Android裝置上。此外,要很好地測試這個功能,需要在兩臺裝置上同時執行這個應用程式。

藍芽點對點
以下三個程式碼演示了使用藍芽查詢附近的其他使用者並快速交換聯絡資訊(本例中,只是交換電子郵件地址)。藍芽上的連線是通過發現可用的“服務”,並通過全域性唯一的128位UUID值連線到相應的服務。也就是說,在連線某個服務之前,必須首先發現或知道它的UUID。
在本例中,連線兩端的裝置執行的是相同的應用程式,兩個應用都會有對應的UUID,因此我們可以在程式碼中自由地將UUID定義為常數。
注意:
為了確保選擇的UUID是唯一的,請使用網路上免費的UUID生成器,或者使用相應的工具,例如Mac/Linux上的uuidgen。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.examples.bluetooth">

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

    <application
    android:icon="@drawable/icon"
    android:label="@string/app_name">
        <activity   android:name=".ExchangeActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

要點:
記住,要想使用這些API,需要在清單中宣告android.permission.BLUETOOTH許可權。另外,要想改變藍芽的可發現性以及啟用/禁用藍芽介面卡,還要在清單中宣告android.permission.BLUETOOTH_ADMIN許可權。
res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
	<TextView
		android:id="@+id/label"
		android:layout_width="wrap_content"
		android:layout_height="wrap_content"
		android:textAppearance="?android:attr/textAppearanceLarge"
		android:text="Enter Your Email:"/>
	<EditText
		android:id="@+id/emailField"
		android:layout_width="match_parent"
		android:layout_height="wrap_content"
		android:layout_below="@id/label"
		android:singleLine="true"
		android:inputType="textEmailAddress"/>
	<Button
	android:id="@+id/scanButton"
  	android:layout_width="match_parent" 
   	android:layout_height="wrap_content"
   	android:layout_alignParentBottom="true"
   	android:text="Connect and Share"  />
  <Button
		android:id="@+id/listenButton"
  	android:layout_width="match_parent" 
   	android:layout_height="wrap_content"
   	android:layout_above="@id/scanButton"
   	android:text="Listen for Sharers"  />
</RelativeLayout>

這個示例的UI由一個讓使用者輸入電子郵件的EditText和兩個用於初始化通訊的按鈕組成。名為“Listen for Shares”的按鈕用來將裝置設為監聽模式。在這種模式下,裝置會接受其他裝置的連線,並嘗試與之進行通訊。名為“Connect and Share”的按鈕用來將裝置設為搜尋模式。在這種模式下,裝置會搜尋當前處於監聽模式的監聽,並與之進行連線(參見以下程式碼)。
藍芽交換Activity

public class ExchangeActivity extends Activity {

        // 本應用程式唯一的UUID (從網上生成)
	private static final UUID MY_UUID = UUID.fromString("321cb8fa-9066-4f58-935e-ef55d1ae06ec");
	//發現時用於匹配的一個更加友好的名稱
	private static final String SEARCH_NAME = "bluetooth.recipe";
	
	BluetoothAdapter mBtAdapter;
	BluetoothSocket mBtSocket;
	Button listenButton, scanButton;
	EditText emailField;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setTitle("Activity");
	requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
		setContentView(R.layout.main);
		//檢查系統狀態
		mBtAdapter = BluetoothAdapter.getDefaultAdapter();
		if(mBtAdapter == null) {
		    Toast.makeText(this, "Bluetooth is not supported.", Toast.LENGTH_SHORT).show();
		    finish();
		    return;
		}
		if (!mBtAdapter.isEnabled()) {
            Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableIntent, REQUEST_ENABLE);
		}
		
		emailField = (EditText)findViewById(R.id.emailField);
		listenButton = (Button)findViewById(R.id.listenButton);
		listenButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //首先要確保裝置是可以發現的
                if (mBtAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
                    Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
                    discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
                    startActivityForResult(discoverableIntent, REQUEST_DISCOVERABLE);
                    return;
                }
                startListening();
            }
        });
		scanButton = (Button)findViewById(R.id.scanButton);
		scanButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mBtAdapter.startDiscovery();
                setProgressBarIndeterminateVisibility(true);
            }
        });
	}
	
	@Override
	public void onResume() {
	    super.onResume();
	    //為Activity註冊廣播Intent
        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
        registerReceiver(mReceiver, filter);
        filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
        registerReceiver(mReceiver, filter);
	}
	
	@Override
	public void onPause() {
	    super.onPause();
	    unregisterReceiver(mReceiver);
	}
	
	@Override
	public void onDestroy() {
	    super.onDestroy();
	    try {
	        if(mBtSocket != null) {
	            mBtSocket.close();
	        }
	    } catch (IOException e) {
	        e.printStackTrace();
	    }
	}
	
	private static final int REQUEST_ENABLE = 1;
	private static final int REQUEST_DISCOVERABLE = 2;
	
	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
	    switch(requestCode) {
	    case REQUEST_ENABLE:
	        if(resultCode != Activity.RESULT_OK) {
	            Toast.makeText(this, "Bluetooth Not Enabled.", Toast.LENGTH_SHORT).show();
	            finish();
	        }
	        break;
	    case REQUEST_DISCOVERABLE:
	        if(resultCode == Activity.RESULT_CANCELED) {
	            Toast.makeText(this, "Cannot listen unless we are discoverable.", Toast.LENGTH_SHORT).show();
	        } else {
	            startListening();
	        }
	        break;
	    default:
	        break;
	    }
	}
	//啟動伺服器套接字並監聽
	private void startListening() {
	    AcceptTask task = new AcceptTask();
        task.execute(MY_UUID);
        setProgressBarIndeterminateVisibility(true);
	}
	
	//AsyncTask接受傳入的連線
	private class AcceptTask extends AsyncTask<UUID,Void,BluetoothSocket> {

        @Override
        protected BluetoothSocket doInBackground(UUID... params) {
            String name = mBtAdapter.getName();
            try {
                //監聽時,將發現名設定為指定的值
                mBtAdapter.setName(SEARCH_NAME);
                BluetoothServerSocket socket = mBtAdapter.listenUsingRfcommWithServiceRecord("BluetoothRecipe", params[0]);
                BluetoothSocket connected = socket.accept();
                //復位藍芽適配名稱
                mBtAdapter.setName(name);
                return connected;
            } catch (IOException e) {
                e.printStackTrace();
                mBtAdapter.setName(name);
                return null;
            }
        }
	    
        @Override
        protected void onPostExecute(BluetoothSocket socket) {
            if(socket == null) {
                return;
            }
            mBtSocket = socket;
            ConnectedTask task = new ConnectedTask();
            task.execute(mBtSocket);
        }
        
	}
	
	//AsyncTask接收一行資料併發送
	private class ConnectedTask extends AsyncTask<BluetoothSocket,Void,String> {

		@Override
		protected String doInBackground(BluetoothSocket... params) {
			InputStream in = null;
			OutputStream out = null;
			try {
			    //傳送資料
			    out = params[0].getOutputStream();
			    out.write(emailField.getText().toString().getBytes());
			    //接收其他資料
				in = params[0].getInputStream();
				byte[] buffer = new byte[1024];
				in.read(buffer);
				//從結果建立一個空字串
				String result = new String(buffer);
				//關閉連線
				mBtSocket.close();
				return result.trim();
			} catch (Exception exc) {
				return null;
			}
		}
		
		@Override
		protected void onPostExecute(String result) {
		    
			Toast.makeText(ExchangeActivity.this, result, Toast.LENGTH_SHORT).show();
			setProgressBarIndeterminateVisibility(false);
		}
		
	}

    // 用來監聽發現裝置的BroadcastReceiver 
    private BroadcastReceivercovered devices 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(TextUtils.equals(device.getName(), SEARCH_NAME)) {
                    //匹配找到的裝置,連線
                    mBtAdapter.cancelDiscovery();
                    try {
                        mBtSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
                        mBtSocket.connect();
                        ConnectedTask task = new ConnectedTask();
                        task.execute(mBtSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                        Toast.makeText(ExchangeActivity.this, "Error connecting to remote", Toast.LENGTH_SHORT).show();
                    }
                }
            //發現完成
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                setProgressBarIndeterminateVisibility(false);
            }       	
        }
    };
}

應用程式初次啟動後,會首先對裝置上藍芽的狀態做一些基本的檢查。如果BluetoothAdapter.getDefaultAdapter()返回null,表明裝置不支援藍芽,應用程式無法繼續使用。即使裝置上有藍芽,它也必須是啟用的,這樣應用程式才能使用它。如果藍芽是禁用的,推薦啟用介面卡的方法是向系統傳送一個action值為BluetoothAdapter.ACTION_REQUEST_ENABLED的Intent。這樣就會通知使用者啟用藍芽。可以用enable()方法手動啟動BluetoothAdapter,但我們不推薦這種方法,除非需要通過某種特別的方式來獲得使用者的許可權。
驗證過藍芽可用後,應用程式會等待使用者輸入。正如之前提到的,這個示例可以將每臺裝置設為兩種模式之一:監聽模式或搜尋模式。接下來看看這兩種模式各種的工作方式。

監聽模式
單擊“Listen for Sharers”按鈕開始讓應用程式對接入的連線進行監聽。對於一臺裝置來說,如果想要接收未知裝置的接入連線,那麼該裝置必須設定為可被發現的。應用程式會檢查介面卡的掃描模式是否等於SCAN_MODE_CONNECTABLE_DISCOVERABLE,從而確定裝置是否是可被發現的。如果介面卡不滿足這個要求,就會給系統傳送一個Intent,告訴使用者應該讓裝置處於可發現狀態,這和要求使用者啟用藍芽的方法很相似。如果使用者拒絕了該請求,Activity會返回Activity.RESULT_CANCELED。這個示例會在onActivityResult()中處理使用者拒絕請求的行為,即在這些條件下終止應用。
如果使用者啟用了可發現狀態或者裝置已經處於可發現狀態,就會建立並執行一個AcceptTask。這個任務會為我們所定義服務的UUID建立監聽器埠,通過這個埠阻塞主調執行緒並等待接入連線請求。在收到有效的請求時,就會接受。然後應用程式會切換到Connected Mode(已連線模式)。
在裝置處於監聽狀態的過程中,其藍芽的名稱會被設定為已知的唯一值(SEARCH_NAME)來加速發現的過程(具體原因見後面的“搜尋模式”小節)。連線建立後,介面卡就會恢復到預設名稱。

搜尋模式
單擊“Connect and Share”按鈕,通知應用程式開始搜尋另一臺想要連線的裝置。這當中會首先啟動藍芽發現過程並且在BroadcastReceiver中處理搜尋結果。通過BluetoothAdapter.startDiscovery()開始一次發現後,以下兩種情況下,Android會通過廣播進行非同步回撥:找到了另一臺裝置或發現過程完成。
私有的接收器mReceiver在Activity對使用者可見時會隨時進行註冊,對於每一臺新發現的裝置,它都會收到一個廣播。回憶一下,在討論監聽模式時,監聽裝置的名稱被設定成唯一的值。在每次發現完成後,接收器會檢測與當前名稱匹配的裝置,並且在搜尋到結果後嘗試進行連線。這對於發現過程的速度非常重要,因為驗證一臺裝置是否可用的唯一途徑就是將這臺裝置與一個特殊的服務UUID進行嘗試性連線,以檢視操作是否成功。藍芽連線過程屬於重量級操作,並且很慢,應該在確保其他一切執行良好時才進行這個操作。
這種匹配裝置的方式同樣減輕了使用者手動連線其想要連線的裝置的過程。應該程式會智慧地尋找到同樣執行這個應用程式並且處於監聽模式的另一臺裝置來完成傳輸。移除使用者也意味著這個值是唯一且非常少見的,就是為了避免查詢其他裝置時,這些裝置可能意外具有相同的名稱。
找到匹配的裝置後,就會停止發現過程(因為它同樣是重量級操作,並且會減緩連線過程),然後連線到服務的UUID。在連線成功後,應用程式就進入了已連線模式。
提示:
可以在許多地方生成自己的唯一ID(UUID)值。各種網站,如http://www.uuidgenerator.net/,將自動建立一個UUID。Mac OS X和Linux使用者還可以從命令列執行到uuidgen命令。

已連線模式
一旦連線成功,兩臺裝置上的應用程式將建立一個ConnectedTask來發送和接收使用者聯絡資訊。連線的BluetoothSocket會用一個InputStream和一個OutputStream進行資料傳輸。首先,電子郵件的文字欄位的當前值在封裝後被寫入OutputStream。然後,從InputStream讀取以接收遠端裝置的資訊。最後,每臺裝置都需要將它接收的原始資料封裝成一個單純的字串顯示給使用者。
ConnectedTask.onPostExecute()方法的任務是向用戶顯示交流的結果;目前,是將接收的內容顯示在一個Toast中。交流完成後,連線被關閉,兩臺裝置都會進入相同的模式,準備進行下一次交流。
有關此主題的更多資訊,可以檢視Android SDK提供的BlueoothChat示例應用程式。這個應用程式很好地演示瞭如何使用一個長連線在裝置之間發生聊天資訊。
藍芽通訊

Android之外的藍芽:
正如本節開始是描述的那樣,除了手機和平板電腦,許多無線裝置上也有藍芽。在諸如藍芽調變解調器和序列介面卡這樣的裝置上同樣有RFCOMM介面。在Android裝置上建立點對點連線是使用的API同樣可以用來連線其他嵌入式藍芽裝置,從而實現對裝置的監控和控制。
要想與這些嵌入式裝置建立連線,關鍵是要獲得它們所支援的RFCOMM服務的UUID。作為配置檔案標準的一部分的藍芽服務及其識別符號由藍芽特別興趣小組(Special Interest Group,SIG)定義;因此,我們能夠從www.bluetooth.org提供的文件中獲得給定裝置所需的UUID。然而,如果裝置製造商為自定義服務型別定義了裝置特有的UUID,並且沒有歸入文件,則必須通過某種方式來發現該UUID。與前面的示例一樣,我們可以使用適當的UUID建立一個BluetoothSocket和傳輸資料。
SDK就擁有這種能力,雖然在Android 4.0.3(API Level 15)之前它並不是SDK的開放部分。對於藍芽裝置來說,有兩個方法能夠提供此資訊:fetchUuidsWithSdp()和getUuids()。後者只會簡單地返回在發現期間找到的裝置的快取例項,而前者則會非同步連線裝置並進行一個新的查詢。正因為如此,在使用fetchUuidsWithSdp()時,必須註冊一個BroadcastReceiver,它將接收action值為BluetoothDevice.ACTION_UUID的Intent並發現UUID值。