Android 通過samples\android-x\BluetoothChat學習藍牙操作

最近幫別人寫了一個東西需要用到藍牙共享數據,發現Android SDK裏的例子裏的BluetoothChat——藍牙聊天軟件代碼寫得不錯,就學習分析了一下。

項目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);

第一個參數是字節數組,第二個爲偏移量,(內容是從第一個位置寫入的),第三個參數是長度。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章