1. 程式人生 > >手把手教你做藍芽聊天應用(四)-藍芽連線模組

手把手教你做藍芽聊天應用(四)-藍芽連線模組

第4節 藍芽連線模組

藍芽連線的管理模組需要為ChatActivity提供於連線相關的所有功能,要設計的方便使用,並儘量隱藏連線的細節。

4.1 對外介面

我們首先來看看ConnectionManager需要向Chat Activity提供哪些介面。

  1. 監聽。當應用執行起來後,聊天應用需要啟動對其它藍芽裝置的監聽,迎接隨時可能到來的連線請求。所以ConnectionManager需要提供啟動監聽-startListen()停止監聽-stopListen()的兩個介面;

  2. 主動連線。應用搜索到可連線到裝置後,可以主動的連線對方裝置。假如正在連線,使用者可能會取消連線;假如已經連線上,使用者可能會想斷開這個連線。所以ConnectionManager

    需要提供連線-connect()斷開連線-disconnect()(包含取消連線的功能)兩個介面;

  3. 主動查詢當前連線狀態。應用在採取某些操作的時候,可能需要知道當前的連線狀態。所以需要提供主動查詢當前連線狀態的介面。

    連線的狀態,我們可以把它分成兩類,一個是監聽的狀態,一個是連線的狀態。監聽的狀態表示當前是否在監聽;連線的狀態應該包括沒有連線,正在連線和已經連線。

    所以介面應該有getCurrentListenState()getCurrentConnectState()

  4. 獲取連線狀態變化通知。應用介面需要根據連線的狀態做出相應的變化,需要ConnectionManager能在連線狀態發生變化的時候,主動把資訊通知給ChatActivity

    。所以還要提供一個通知狀態變化的介面。

  5. 接收資料。ConnectionManager收到資料以後,需要一個機制將資料通知給ChatActivity,這樣ChatActivity才能顯示接收到的內容。

  6. 傳送資料。使用者要傳送資料給被連線的裝置,所以ConnectionManager要提供一個傳送資料的介面-sendData()

45都需要ConnectionManager主動向ChatActivity傳遞資訊(收到的資料、狀態改變),可以通過設計監聽器來實現。因此需要設計安裝監聽器的方法:定義一個回撥的介面-ConnectionListener;把監聽器設定到ConnectioManager

中。

綜上所述,ConnectionManager大概應該是這個樣子,

public class ConnectionManager {

    //定義監聽器
    public interface ConnectionListener {

        //當ConnectionManager的連線狀態發生變化,
        //通過onConnectStateChange()將變化通知到ChatActivity
        public void onConnectStateChange(int oldState, int State);
        //當ConnectionManager的監聽狀態發生變化,
        //通過onListenStateChange()將變化通知到ChatActivity
        public void onListenStateChange(int oldState, int State);
        //當ConnectionManager的資料傳送完成後,
        //通過onSendData()將傳送的內容通知到ChatActivity
        public void onSendData(boolean suc, byte[] data);
        //當ConnectionManager的接收到對方連線裝置傳來的資料,
        //通過onReadData()將傳來的資料通知到ChatActivity
        public void onReadData(byte [] data);
    }

    private ConnectionListener mConnectionListener;

    //建構函式中設定監聽器
    public ConnectionManager(ConnectionListener cl) {
        mConnectionListener = cl;
    }

    public void startListen() {
    }

    public void stopListen() {
    }

    public void connect() {
    }

    public void disconnect() {
    }

    public int getCurrentListenState() {
    }

    public int getCurrentConnectState() {
    }

    public void sendData(byte[] data) {
    }

}

4.2 ConnectionManager的結構

我們已經明確了ConnectionManager對外的介面。接下來就需要來實現功能了。

由Android SDK提供的藍芽連線介面函式,很多都是阻塞的(wifi連線也是如此),例如accept()connect()等等,所以就需要將這些操作放到單獨的工作執行緒中進行。

因此,我們將會在ConnectionManager中設計兩個工作執行緒,

  1. AcceptThread:負責監聽別的藍芽裝置發起的連線;

  2. ConnectThread:用來維持與其它裝置的連線。

主執行緒首先建立一個監聽執行緒,用來接收其它裝置可能發出的連線請求,當監聽執行緒監聽到了連線請求,就會得到一個Socket;然後我們再建立一個連線執行緒,將Socket交給連線執行緒,兩個裝置就可以通過這個Socket,在連線執行緒中進行訊息的傳送和接收了。

4.2.1 ConnectionManager

  • AcceptThread執行緒的工作狀態,反應的就是監聽狀態。

    它包括LISTEN_STATE_IDLELISTEN_STATE_LISTENING。這個狀態儲存在mListenState變數中;

    當監聽狀態發生改變的時候,通過setListenState()設定狀態的改變,並利用回撥的方式,將改變的狀態通知給關注者。

    public class ConnectionManager {
        ......
        //監聽的兩種狀態
        public static final int LISTEN_STATE_IDLE = 3;
        public static final int LISTEN_STATE_LISTENING = 4;
    
        //記錄當前監聽的狀態
        private int mListenState = LISTEN_STATE_IDLE;
    
        //修改當前監聽的狀態
        private void setListenState(int state) {
    
            //狀態沒有發生變化,不用通知
            if(mListenState == state) {
                return;
            }
    
            int oldState = mListenState;
            mListenState = state;
    
            //狀態發生變化,發起通知
            if(mConnectionListener != null) {
                mConnectionListener.onListenStateChange(oldState, mListenState);
            }
        }
        ......
    }
  • ConnectedThread執行緒的工作狀態,反應的就是連線狀態。

    它包括CONNECT_STATE_IDLE CONNECT_STATE_CONNECTINGCONNECT_STATE_CONNECTED;
    這個狀態儲存在mConnectState變數中;

    當連線狀態發生改變的時候,通過setConnectState()設定狀態的改變,並利用回撥的方式,將改變的狀態通知給關注者。

    public class ConnectionManager {
        ......
        //連線的三種狀態
        public static final int CONNECT_STATE_IDLE = 0;
        public static final int CONNECT_STATE_CONNECTING = 1;
        public static final int CONNECT_STATE_CONNECTED = 2;
    
        //記錄當前連線的狀態
        private int mConnectState = CONNECT_STATE_IDLE;
    
        //修改當前連線的狀態
        private void setConnectState(int state) {
    
            ///狀態沒有發生變化,不用通知
            if(mConnectState == state) {
                return;
            }
    
            int oldState = mConnectState;
            mConnectState = state;
    
            //狀態發生變化,發起通知
            if(mConnectionListener != null) {
                mConnectionListener.onConnectStateChange(oldState, mConnectState);
            }
        }
        ......
    }
  • ConnectionManager對外提供的介面,實際上就是對這兩個工作執行緒的控制;

public class ConnectionManager {
    ......
    private ConnectionListener mConnectionListener;

    private AcceptThread mAcceptThread;
    private ConnectedThread mConnectedThread;
    private final BluetoothAdapter mBluetoothAdapter;

    //建構函式中設定監聽器
    public ConnectionManager(ConnectionListener cl) {
        mConnectionListener = cl;
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    }

    public void startListen() {
        //建立監聽執行緒
        if(mAcceptThread != null) {
            mAcceptThread.cancel();
        }

        mAcceptThread = new AcceptThread();
        mAcceptThread.start();
    }

    public void stopListen() {
        //停止監聽執行緒
        if(mAcceptThread != null) {
            mAcceptThread.cancel();
        }
    }

    public void connect() {
        //發起連線
        if(mConnectedThread != null) {
            mConnectedThread.cancel();
        }

        BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceAddr);

        try {
            //建立發起主動連線使用的Socket
            BluetoothSocket socket = device.createRfcommSocketToServiceRecord(BT_UUID);
            //啟動連線執行緒
            mConnectedThread = new ConnectedThread(socket, true);
            mConnectedThread.start();
        } catch (IOException e) {

        }
    }

    public void disconnect() {
        //停止連線執行緒
        if(mConnectedThread != null) {
            mConnectedThread.cancel();
        }
    }

    public int getCurrentListenState() {
        //查詢當前監聽執行緒的狀態
        return mListenState;
    }

    public int getCurrentConnectState() {
        //查詢當前連線執行緒的狀態
        return mConnectState;
    }

    public boolean sendData(byte[] data) {
        //傳送資料
        if(mConnectedThread != null 
            && mConnectState == CONNECT_STATE_CONNECTED) {
            mConnectedThread.sendData(data);

            return true;
        }
        return false;
    }
    ......
}

通過這裡,我們可以看出,為了能夠取消正在執行的工作執行緒,在設計AcceptThreadConnectedThread的時候,我們需要給它們新增上取消的方法-cancel();為了在連線後能夠傳送資料,需要給ConnectedThread添加發送資料的方法-sendData()

4.2.2 監聽執行緒

監聽執行緒進行的工作有,

  1. 等待其它藍芽裝置發起的連線;

  2. 如果接收到連線的請求,就創建出一個Socket

  3. 之後繼續等待其它裝置可能發起的連線;

  4. 假如已經處於正在連線或者已經連線的狀態,就斷開最新收到的連線,因為我們假定了每次只能連線一個裝置;

  5. 監聽執行緒可以被取消,退出執行;

public class ConnectionManager {
    ......
    private class AcceptThread extends Thread {

        private BluetoothServerSocket mServerSocket;
        private boolean mUserCancel;

        public AcceptThread() {
            BluetoothServerSocket tmp = null;
            mUserCancel = false;

            //建立監聽用的ServerSocket
            try {
                tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(
                        BT_NAME, BT_UUID);
            } catch (IOException e) {

            }
            mServerSocket = tmp;
        }

        //監聽執行緒開始執行
        @Override
        public void run() {

            setName("AcceptThread");

            //將ConnectionManger監聽的狀態設定成“正在監聽”
            setListenState(LISTEN_STATE_LISTENING);

            BluetoothSocket socket = null;

            while(!mUserCancel) {
                try {
                    //阻塞在這裡,等待別的裝置連線
                    socket = mServerSocket.accept();

                } catch (IOException e) {
                    //阻塞過程中,如果其它地方呼叫了mServerSocket.close(),
                    //將會進入到這個異常當中
                    mServerSocket = null;
                    break;
                }

                if(mConnectState == CONNECT_STATE_CONNECTED 
                || mConnectState == CONNECT_STATE_CONNECTING) {
                    //如果當前正在連線別的裝置,
                    //或者已經和別的裝置連線上了,就放棄這個連線,
                    //因為每次只能和一個裝置連線
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                else if(mConnectState == CONNECT_STATE_IDLE) {
                    //如果當前沒有和別的裝置連線上,
                    //啟動連線執行緒
                    mConnectedThread = new ConnectedThread(socket, false);
                    mConnectedThread.start();
                }
            }

            if(mServerSocket != null) {
                try {
                    mServerSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                mServerSocket = null;
            }
            setListenState(LISTEN_STATE_IDLE);
            mAcceptThread = null;
        }

        //讓監聽執行緒退出執行
        public void cancel() {       
            try {
                mUserCancel = true;
                //ServerSocket此時阻塞在accept()方法中,
                //關閉之後,會讓accept()方法丟擲異常,實現監聽執行緒的退出
                if(mServerSocket != null) {
                    mServerSocket.close();
                }
            } catch (IOException e) {
            }
        }
    }
    ......

}

4.2.3 連線執行緒

連線執行緒的工作是,

  1. 假如是主動連線,連線執行緒要主動呼叫connect()方法;該方法是一個阻塞呼叫,在呼叫前,應該把ConnectionManager的連線狀態,設定成CONNECT_STATE_CONNECTING;如果連線成功,就把狀態設定成CONNECT_STATE_CONNECTED

  2. 假如是被動連線,那麼從監聽執行緒獲取的Socket就已經是被連線上了的Socket,不需要進行connect()的操作了;

  3. 從已經連線的Socket當中,獲取輸入、輸出的流介面,以後這個連線上的資料讀取和寫入,就是通過它們對應的流介面進行的;

  4. 連線執行緒進入迴圈,不斷的嘗試通過InputStreamread()方法,讀取資料;

  5. 連線執行緒可以被取消,退出連線;

public class ConnectionManager {

    ......

    private class ConnectedThread extends Thread {

        private final int MAX_BUFFER_SIZE = 1024;

        private BluetoothSocket mSocket;
        private InputStream mInStream;
        private OutputStream mOutStream;
        private boolean mUserCancel;
        private boolean mNeedConnect;

        //needConnect引數告訴連線執行緒是否需要主動發起連線,
        //因為通過監聽執行緒啟動的連線執行緒是不需要發起主動連線的,
        //所以需要一個標誌位來控制這種情況
        public ConnectedThread(BluetoothSocket socket, boolean needConnect) {

            //儲存下工作執行緒中要用到的引數
            setName("ConnectedThread");
            mNeedConnect = needConnect;
            mSocket = socket;
            mUserCancel = false;
        }

        @Override
        public void run() {

            //將ConnectionManager的連線狀態修改成CONNECT_STATE_CONNECTING
            setConnectState(CONNECT_STATE_CONNECTING);

            //如果這是一個主動連線,說明Socket還沒有和對方相連線,就需要發起主動連線
            if(mNeedConnect && !mUserCancel) {
                try {
                    mSocket.connect();
                } catch (IOException e) {
                    //主動連線發生異常,回到未連線的狀態
                    setConnectState(CONNECT_STATE_IDLE);
                    mSocket = null;
                    mConnectedThread = null;

                    return;
                }
            }

            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            //從連線的Socket中獲取讀資料和寫資料的流介面
            try {
                tmpIn = mSocket.getInputStream();
                tmpOut = mSocket.getOutputStream();
            } catch (IOException e) {
                setConnectState(CONNECT_STATE_IDLE);
                mSocket = null;
                mConnectedThread = null;

                return;
            }

            mInStream = tmpIn;
            mOutStream = tmpOut;

            //將ConnectionManager的連線狀態修改成CONNECT_STATE_CONNECTED
            setConnectState(CONNECT_STATE_CONNECTED);

            byte[] buffer = new byte[MAX_BUFFER_SIZE];
            int bytes;

            while (!mUserCancel) {
                try {
                    //阻塞在這裡,用流介面等待讀取資料
                    bytes = mInStream.read(buffer);
                    //將讀取到的資料傳遞給關注它的元件
                    if(mConnectionListener != null && bytes > 0) {

                        byte [] data = new byte[bytes];
                        System.arraycopy(buffer, 0, data, 0, bytes);
                        mConnectionListener.onReadData(data);
                    }
                } catch (IOException e) {
                    //阻塞過程中,如果其它地方呼叫了mSocket.close(),
                    //或者對方的連線關閉
                    //將會進入到這個異常當中
                    break;
                }
            }

            setConnectState(CONNECT_STATE_IDLE);
            mSocket = null;
            mConnectedThread = null;
        }

        //讓連線執行緒退出執行
        public void cancel() {
            try {
                mUserCancel = true;
                //Socket此時阻塞在InputStream的read()方法中,
                //關閉之後,會讓read()方法丟擲異常
                if(mSocket != null) {
                    mSocket.close();
                }

            } catch (IOException e) {
            }
        }

        //向對方傳送資料
        public void sendData(byte[] data) {
            try {
                //用流介面傳送資料
                mOutStream.write(data);
                //向關心的元件通知傳送成功
                if(mConnectionListener != null) {
                    mConnectionListener.onSendData(true, data);
                }
            } catch (IOException e) {
                //向關心的元件通知傳送失敗
                if(mConnectionListener != null) {
                    mConnectionListener.onSendData(false, data);
                }
            }
        }
    }
    ......
}

至此,ConnectionManager就設計好了。

/*******************************************************************/
* 版權宣告
* 本教程只在CSDN安豆網釋出,其他網站出現本教程均屬侵權。

*另外,我們還推出了Arduino智慧硬體相關的教程,您可以在我們的網店跟我學Arduino程式設計中購買相關硬體。同時也感謝大家對我們這些碼農的支援。

*最後再次感謝各位讀者對安豆的支援,謝謝:)
/*******************************************************************/

相關推薦

手把手聊天應用連線模組

第4節 藍芽連線模組 藍芽連線的管理模組需要為ChatActivity提供於連線相關的所有功能,要設計的方便使用,並儘量隱藏連線的細節。 4.1 對外介面 我們首先來看看ConnectionManager需要向Chat Activity提供哪些介面。

手把手一個 C 語言編譯器8:表示式

這是整個編譯器的最後一部分,解析表示式。什麼是表示式?表示式是將各種語言要素的一個組合,用來求值。例如:函式呼叫、變數賦值、運算子運算等等。 表示式的解析難點有二:一是運算子的優先順序問題,二是如何將表示式編譯成目的碼。我們就來逐一說明。 本系列: 運算子的優先順

手把手一個 C 語言編譯器7:語句

整個編譯器還剩下最後兩個部分:語句和表示式的解析。它們的內容比較多,主要涉及如何將語句和表示式編譯成彙編程式碼。這章講解語句的解析,相對於表示式來說它還是較為容易的。 本系列: 語句 C 語言區分“語句”(statement)和“表示式”(expression)兩

手把手一個 C 語言編譯器9:總結

恭喜你完成了自己的 C 語言編譯器,本章中我們發一發牢騷,說一說編寫編譯器值得注意的一些問題;編寫編譯器時遇到的一些難題。 本系列: 虛擬機器與目的碼 整個系列的一開始,我們就著手虛擬機器的實現。不知道你是否有同感,這部分對於整個編譯器的編寫其實是十分重要的。我認

手把手一個 C 語言編譯器2:虛擬機器

本章是“手把手教你構建 C 語言編譯器”系列的第三篇,本章我們要構建一臺虛擬的電腦,設計我們自己的指令集,執行我們的指令集,說得通俗一點就是自己實現一套匯編語言。它們將作為我們的編譯器最終輸出的目的碼。 本系列: 計算機的內部工作原理 我們關心計算機的三個基本部件

手把手一個 C 語言編譯器6:函式定義

由於語法分析本身比較複雜,所以我們將它拆分成 3 個部分進行講解,分別是:變數定義、函式定義、表示式。本章講解函式定義相關的內容。 本系列: EBNF 表示 這是上一章的 EBNF 方法中與函式定義相關的內容。 C

手把手一個 C 語言編譯器4:遞迴下降

本章我們將講解遞迴下降的方法,並用它完成一個基本的四則運算的語法分析器。 本系列: 什麼是遞迴下降 傳統上,編寫語法分析器有兩種方法,一種是自頂向下,一種是自底自上。自頂向下是從起始非終結符開始,不斷地對非終結符進行分解,直到匹配輸入的終結符;自底向上是不斷地將終

手把手一個 C 語言編譯器3:詞法分析器

本章我們要講解如何構建詞法分析器。 本系列: 什麼是詞法分析器 簡而言之,詞法分析器用於對原始碼字串做預處理,以減少語法分析器的複雜程度。 詞法分析器以原始碼字串為輸入,輸出為標記流(token stream),即一連串的標記,每個標記通常包括: (token,

手把手一個 C 語言編譯器0:前言

“手把手教你構建 C 語言編譯器” 這一系列教程將帶你從頭編寫一個 C 語言的編譯器。希望通過這個系列,我們能對編譯器的構建有一定的瞭解,同時,我們也將構建出一個能用的 C 語言編譯器,儘管有許多語法並不支援。 在開始進入正題之前,本篇是一些閒聊,談談這個系列的初衷

手把手一個 C 語言編譯器1:設計

本章是“手把手教你構建 C 語言編譯器”系列的第二篇,我們要從整體上講解如何設計我們的 C 語言編譯器。 本系列: 首先要說明的是,雖然標題是編譯器,但實際上我們構建的是 C 語言的直譯器,這意味著我們可以像執行指令碼一樣去執行 C 語言的原始碼檔案。這麼做的理由

手把手一個 C 語言編譯器5:變數定義

本章中我們用 EBNF 來大致描述我們實現的 C 語言的文法,並實現其中解析變數定義部分。 由於語法分析本身比較複雜,所以我們將它拆分成 3 個部分進行講解,分別是:變數定義、函式定義、表示式。 本系列: EBNF 表示 EBNF 是對前一章提到的 BNF 的擴充

手把手iOS的soap應用 webservice

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

手把手開源專案MyMeiZi 二使用RecyclerView+Glide打造瀑布流

Glide 一個專注於平滑滾動的圖片載入和快取庫,相信大家也不會陌生,話說我以前一直使用的是Fresco,這次也是為了學習使用了Glide,誰上誰下這個不好區分,但是Glide更輕量級,究竟選擇哪個還要看各位自己了 在Github上我們可以看到關於G

手把手玩轉 CSS3 3D 技術轉載

是不是 col 默認 占滿 概念 ebe 說明 adb relative css3的3d起步 要玩轉css3的3d,就必須了解幾個詞匯,便是透視(perspective)、旋轉(rotate)和移動(translate)。透視即是以現實的視角來看屏幕上的2D事物,從而展現3

android聊天開發5簡單聊天

執行截圖:我的裝置設定的使用者名稱是:是。可以改的          mainActivity: package com.lmj.bluetoothchat; import java.util.ArrayList; import an

手把手搭建卷積神經網路CNN

本文是基於吳恩達老師的《深度學習》第四課第一週習題所做,如果本文在某些知識點上描述得不夠透徹的可以參見相關章節的具體講解,同時極力推薦各位有志從事計算機視覺的朋友觀看一下吳恩達老師的《深度學習》課程。 1.卷積神經網路構成 總的來說,卷積神經網路與神經網路的區別是增加了若

【程式設計師面試系列】手把手如何面試:面試過程

作者:Dimple Solgan:當你的才華還無法撐起你的野心時候,那應該靜下心來好好學習 公號:奔跑吧攻城獅 在之前【程式設計師面試系列】中,我們通過簡歷、技術、工作專案三個維度來說明,我們需要如何做好充分的準備工作。 良好的開端是成功的一半,只有做好了前面的準備,我們才有進一步的底氣去投遞簡歷

手把手Spring Boot2.x整合ElasticsearchES

#文末會附上完整的程式碼包供大家下載參考,碼字不易,如果對你有幫助請給個點贊和關注,謝謝! #如果只是想看java對於Elasticsearch的操作可以直接看第四大點 ##一、docker部署Elasticsearch(下面簡稱es)單機版教程 ###1、部署es * 拉取es映象(這裡我使用的版本

手把手一個吸引人的購物網站

網站建設 購物網站 自助建站 購物網站盈利能力相信很多用戶都是有目共睹的,因此不少的中小企業對購物網站的建設也是趨之若鶩,怎麽企業設計購物網站有什麽方法能夠為購物網站提高人氣呢?下面看看凡科網站建設帶來的一些分析。 要對用戶的跟隨心理進行分析。無論是實體銷售還是線上的銷售,用戶都會有一種莫名

萬字幹貨:手把手需求管理

體驗 正常的 知識共享 ktv 實的 但是 列表 等於 啟示 通過這篇文章,總結自己在工作實踐中需求管理的方法論——普拉姆方法。總結這個方法論的特點是,用最輕量化的投入,與他人協作,並管理需求,推動需求上線。這套方法論組合了項目管理、敏捷開發的知識,希望能對大家有所幫助。