基於Android Classic Bluetooth的藍牙聊天軟件


代碼地址如下:
http://www.demodashi.com/demo/12133.html

BluetoothChat

基於Android Classic Bluetooth的藍牙聊天軟件,目前僅支持一對一實時通信、文件傳輸、好友添加、好友分組、好友在線狀態更新等功能,其中消息發送支持文本、表情等方式。

前景

藍牙技術作為一種小范圍無線連接技術,能夠在設備間實現方便快捷、靈活安全、低成本、低功耗的數據和語音通信,是目前實現無線個人局域網的主流技術之一。同時,藍牙系統以自組式組網的方式工作,每個藍牙設備都可以在網絡中實現路由選擇的功能,可以形成移動自組網絡。藍牙的特性在許多方面正好符合Ad Hoc和WPAN的概念,顯示了其真正的潛力所在。而且,將藍牙與其他網絡相連接可帶來更廣泛的應用,例如接入互聯網、PSTN或公眾移動通信網,可以使用戶應用更方便或給用戶帶來更大的實惠。

藍牙聊天作為一款針對局域網范圍內的聊天軟件,在辦公密集,想實現快速穩定實時通訊還是比較有實用價值的。目前藍牙技術發展迅速,5.0傳輸速率已經達到2Mbps,傳輸級別達到無損級別,有效工作距離可達300米,在藍牙組網方面技術也在進一步更新,相信要不了多久會有很成熟的方案出來,這樣一來就可以實現多人在線實時聊天功能,打破只能一對多實時聊天的界限。

技術簡介

1、藍牙通信的主從關系
藍牙技術規定每一對設備之間進行藍牙通訊時,必須一個為主角色,另一為從角色,才能進行通信,通信時,必須由主端進行查找,發起配對,建鏈成功后,雙方即可收發數據。理論上,一個藍牙主端設備,可同時與7個藍牙從端設備進行通訊。一個具備藍牙通訊功能的設備, 可以在兩個角色間切換,平時工作在從模式,等待其它主設備來連接,需要時,轉換為主模式,向其它設備發起呼叫。一個藍牙設備以主模式發起呼叫時,需要知道對方的藍牙地址,配對密碼等信息,配對完成后,可直接發起呼叫。

2、藍牙的呼叫過程
藍牙主端設備發起呼叫,首先是查找,找出周圍處於可被查找的藍牙設備。主端設備找到從端藍牙設備后,與從端藍牙設備進行配對,此時需要輸入從端設備的PIN碼,也有設備不需要輸入PIN碼。配對完成后,從端藍牙設備會記錄主端設備的信任信息,此時主端即可向從端設備發起呼叫,已配對的設備在下次呼叫時,不再需要重新配對。已配對的設備,做為從端的藍牙設備也可以發起建鏈請求,但做數據通訊的藍牙模塊一般不發起呼叫。鏈路建立成功后,主從兩端之間即可進行雙向的數據或語音通訊。在通信狀態下,主端和從端設備都可以發起斷鏈,斷開藍牙鏈路。

3、藍牙一對一的串口數據傳輸應用
藍牙數據傳輸應用中,一對一串口數據通訊是最常見的應用之一,藍牙設備在出廠前即提前設好兩個藍牙設備之間的配對信息,主端預存有從端設備的PIN碼、地址等,兩端設備加電即自動建鏈,透明串口傳輸,無需外圍電路干預。一對一應用中從端設備可以設為兩種類型,一是靜默狀態,即只能與指定的主端通信,不被別的藍牙設備查找;二是開發狀態,既可被指定主端查找,也可以被別的藍牙設備查找建鏈。

功能概述

藍牙聊天功能主要分為以下幾個模塊:消息模塊、好友模塊以及個人模塊。

項目結構

項目結構

消息模塊

支持一對一、一對多、多對多實時聊天,能傳輸文字、表情、圖片、文件等。對方不在線時可支持離線消息發送,在對方在線時能及時推送過去。消息支持歷史消息存儲與查看。

好友模塊

支持附近好友添加,好友刪除,好友分組顯示,好友上下線提醒,好友昵稱及分組名稱修改。

個人模塊

展示個人信息,包含昵稱、圖像、加入時間等信息。

個人中心

該模塊還未實現,目前實現功能主要有一對一實時聊天、能傳輸文字、表情、文件,支持好友添加、刪除、分組。下文主要介紹已經實現的藍牙通信流程。

操作流程

查找已配對設備(即好友列表)

代碼實現:

private void findDevice(){
    // 獲得已經保存的配對設備
    Set<BluetoothDevice> pairedDevices = BluetoothAdapter.getDefaultAdapter().getBondedDevices();
    if (pairedDevices.size() > 0) {
        mGroupFriendListData.clear();
        GroupInfo groupInfo = new GroupInfo();
        groupInfo.setGroupName(BluetoothAdapter.getDefaultAdapter().getName());
        List<FriendInfo> friendInfoList = new ArrayList<>();
        for (BluetoothDevice device : pairedDevices) {
            FriendInfo friendInfo = new FriendInfo();
            friendInfo.setIdentificationName(device.getName());
            friendInfo.setDeviceAddress(device.getAddress());
            friendInfo.setFriendNickName(device.getName());
            friendInfo.setOnline(false);
            friendInfo.setJoinTime(DateTime.getStringByFormat(new Date(), DateTime.DEFYMDHMS));
            friendInfo.setBluetoothDevice(device);
            friendInfoList.add(friendInfo);
        }
        groupInfo.setFriendList(friendInfoList);
        groupInfo.setOnlineNumber(0);
        mGroupFriendListData.add(groupInfo);
        mGroupFriendAdapter.setGroupInfoList(mGroupFriendListData);
    }
}

好友列表示例圖:

好友列表

啟用設備的可發現性

如果要讓本地設備可以被其他設備發現,那么就要調用ACTION_REQUEST_DISCOVERABLE操作意圖的startActivityForResult(Intent, int)方法。這個方法會向系統設置發出一個啟用可發現模式的請求。默認情況下,設備的可發現模式會持續120秒。通過給Intent對象添加EXTRA_DISCOVERABLE_DURATION附加字段,可以定義不同持續時間。應用程序能夠設置的最大持續時間是3600秒,0意味着設備始終是可發現的。任何小於0或大於3600秒的值都會自動的被設為120秒。例如,以下代碼把持續時間設置為300秒:

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

申請用戶啟用設備的可發現模式時,會顯示一個對話框。如果響應“Yes”,那么設備的可發現模式會持續指定的時間,而且你的Activity會接收帶有結果代碼等於可發現設備持續時間的onActivityResult()回調方法的調用。如果用戶響應“No”或有錯誤發生,則結果代碼等於RESULT_CANCELED.

在可發現模式下,設備會靜靜的把這種模式保持到指定的時長。如果你想要在可發現模式被改變時獲得通知,那么你可以注冊一個ACTION_SCAN_MODE_CHANGED類型的Intent廣播。這個Intent對象中包含了EXTRA_SCAN_MODE和EXTRA_PREVIOUS_SCAN_MODE附加字段,它們會分別告訴你新舊掃描模式。它們每個可能的值是:SCAN_MODE_CONNECTABLE_DISCOVERABLE,SCAN_MODE_CONNECTABLE或SCAN_MODE_NONE,它們分別指明設備是在可發現模式下,還是在可發現模式下但依然可接收連接,或者是在可發現模式下並不能接收連接。

如果你要初始化跟遠程設備的連接,你不需要啟用設備的可現性。只有在你想要把你的應用程序作為服務端來接收輸入連接時,才需要啟用可發現性,因為遠程設備在跟你的設備連接之前必須能夠發現它。

搜索設備並進行配對(即添加好友)

簡單的調用startDiscovery()方法就可以開始發現設備。該過程是異步的,並且該方法會立即返回一個布爾值來指明發現處理是否被成功的啟動。通常發現過程會查詢掃描大約12秒,接下來獲取掃描發現的每個設備的藍牙名稱。

public class ScanBroadcastReceiver extends BroadcastReceiver {

    private IScanCallback<BluetoothDevice> scanCallback;
    private final Map<String, BluetoothDevice> mDeviceMap = new HashMap<>();

    public ScanBroadcastReceiver(IScanCallback<BluetoothDevice> scanCallback) {
        this.scanCallback = scanCallback;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        if (scanCallback == null) {
            return;
        }
        if(intent.getAction().equals(BluetoothDevice.ACTION_FOUND)){
            //掃描到藍牙設備
            BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            if (bluetoothDevice == null) {
                return;
            }
            if (!mDeviceMap.containsKey(bluetoothDevice.getAddress())) {
                mDeviceMap.put(bluetoothDevice.getAddress(), bluetoothDevice);
            }
            scanCallback.discoverDevice(bluetoothDevice);
        }else if(intent.getAction().equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
            //掃描設備結束
            final List<BluetoothDevice> deviceList = new ArrayList<>(mDeviceMap.values());
            if(deviceList != null && deviceList.size() > 0){
                scanCallback.scanFinish(deviceList);
            } else{
                scanCallback.scanTimeout();
            }
        }
    }
}

搜索好友示例圖:

添加好友

public class PairBroadcastReceiver extends BroadcastReceiver {

    private IPairCallback pairCallback;

    public PairBroadcastReceiver(IPairCallback pairCallback) {
        this.pairCallback = pairCallback;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        if(intent.getAction().equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)){
            //取得狀態改變的設備,更新設備列表信息(配對狀態)
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            if(device != null){
                resolveBondingState(device.getBondState());
            }
        }
    }

    private void resolveBondingState(final int bondState) {
        if (pairCallback == null) {
            return;
        }
        switch (bondState) {
            case BluetoothDevice.BOND_BONDED://已配對
                pairCallback.bonded();
                break;
            case BluetoothDevice.BOND_BONDING://配對中
                pairCallback.bonding();
                break;
            case BluetoothDevice.BOND_NONE://未配對
                pairCallback.unBonded();
                break;
            default:
                pairCallback.bondFail();
                break;
        }
    }
}

配對信息示例圖:

添加好友

配對過程示例圖:

添加好友

連接設備(即好友建立通信通道)

當你想要連接兩個設備時,一個必須通過持有一個打開的BluetoothServerSocket對象來作為服務端。服務套接字的用途是監聽輸入的連接請求,並且在一個連接請求被接收時,提供一個BluetoothSocket連接對象。在從BluetoothServerSocket對象中獲取BluetoothSocket時,BluetoothServerSocket能夠(並且也應該)被廢棄,除非你想要接收更多的連接。

以下是建立服務套接字和接收一個連接的基本過程:

1、調用listenUsingRfcommWithServiceRecord(String, UUID)方法來獲得一個BluetoothServerSocket對象。該方法中的String參數是一個可識別的你的服務端的名稱,系統會自動的把它寫入設備上的Service Discovery Protocol(SDP)數據庫實體(該名稱是任意的,並且可以簡單的使用你的應用程序的名稱)。UUID參數也會被包含在SDP實體中,並且是跟客戶端設備連接的基本協議。也就是說,當客戶端嘗試跟服務端連接時,它會攜帶一個它想要連接的服務端能夠唯一識別的UUID。只有在這些UUID完全匹配的情況下,連接才可能被接收。

2、通過調用accept()方法,啟動連接請求。這是一個阻塞調用。只有在連接被接收或發生異常的情況下,該方法才返回。只有在發送連接請求的遠程設備所攜帶的UUID跟監聽服務套接字所注冊的一個UUID匹配的時候,該連接才被接收。連接成功,accept()方法會返回一個被連接的BluetoothSocket對象。

3、除非你想要接收其他連接,否則要調用close()方法。該方法會釋放服務套接字以及它所占用的所有資源,但不會關閉被連接的已經有accept()方法所返回的BluetoothSocket對象。跟TCP/IP不一樣,每個RFCOMM通道一次只允許連接一個客戶端,因此在大多數情況下,在接收到一個連接套接字之后,立即調用BluetoothServerSocket對象的close()方法是有道理的。

以下是以上過程實現的監聽線程:

public class AcceptThread extends Thread {

    private BluetoothChatHelper mHelper;
    private final BluetoothServerSocket mServerSocket;
    private String mSocketType;

    public AcceptThread(BluetoothChatHelper bluetoothChatHelper, boolean secure) {
        mHelper = bluetoothChatHelper;
        BluetoothServerSocket tmp = null;
        mSocketType = secure ? "Secure" : "Insecure";

        try {
            if (secure) {
                tmp = mHelper.getAdapter().listenUsingRfcommWithServiceRecord(ChatConstant.NAME_SECURE, ChatConstant.UUID_SECURE);
            } else {
                tmp = mHelper.getAdapter().listenUsingInsecureRfcommWithServiceRecord(ChatConstant.NAME_INSECURE, ChatConstant.UUID_INSECURE);
            }
        } catch (IOException e) {
            BleLog.e("Socket Type: " + mSocketType + "listen() failed", e);
        }
        mServerSocket = tmp;
    }

    public void run() {
        BleLog.i("Socket Type: " + mSocketType + "BEGIN mAcceptThread" + this);
        setName("AcceptThread" + mSocketType);

        BluetoothSocket socket = null;

        while (mHelper.getState() != com.vise.basebluetooth.common.State.STATE_CONNECTED) {
            try {
                BleLog.i("wait new socket:" + mServerSocket);
                socket = mServerSocket.accept();
            } catch (IOException e) {
                BleLog.e("Socket Type: " + mSocketType + " accept() failed", e);
                break;
            }
            if (socket != null) {
                synchronized (this) {
                    if(mHelper.getState() == com.vise.basebluetooth.common.State.STATE_LISTEN
                            || mHelper.getState() == com.vise.basebluetooth.common.State.STATE_CONNECTING){
                        BleLog.i("mark CONNECTING");
                        mHelper.connected(socket, socket.getRemoteDevice(), mSocketType);
                    } else if(mHelper.getState() == com.vise.basebluetooth.common.State.STATE_NONE
                            || mHelper.getState() == com.vise.basebluetooth.common.State.STATE_CONNECTED){
                        try {
                            socket.close();
                        } catch (IOException e) {
                            BleLog.e("Could not close unwanted socket", e);
                        }
                    }
                }
            }
        }
        BleLog.i("END mAcceptThread, socket Type: " + mSocketType);
    }

    public void cancel() {
        BleLog.i("Socket Type" + mSocketType + "cancel " + this);
        try {
            mServerSocket.close();
        } catch (IOException e) {
            BleLog.e("Socket Type" + mSocketType + "close() of server failed", e);
        }
    }
}

以下是一個基本的連接過程:

1、通過調用BluetoothDevice的createRfcommSocketToServiceRecord(UUID)方法,獲得一個BluetoothSocket對象。這個方法會初始化一個連接到BluetoothDevice對象的BluetoothSocket對象。傳遞給這個方法的UUID參數必須與服務端設備打開BluetoothServerSocket對象時所使用的UUID相匹配。在你的應用程序中簡單的使用硬編碼進行比對,如果匹配,服務端和客戶端代碼就可以應用這個BluetoothSocket對象了。

2、通過調用connect()方法來初始化連接。在這個調用中,為了找到匹配的UUID,系統會在遠程的設備上執行一個SDP查詢。如果查詢成功,並且遠程設備接收了該連接請求,那么它會在連接期間共享使用RFCOMM通道,並且connect()方法會返回。這個方法是一個阻塞調用。如果因為某些原因,連接失敗或連接超時(大約在12秒之后),就會拋出一個異常。

以下是實現以上過程的連接線程:

public class ConnectThread extends Thread {

    private BluetoothChatHelper mHelper;
    private final BluetoothSocket mSocket;
    private final BluetoothDevice mDevice;
    private String mSocketType;

    public ConnectThread(BluetoothChatHelper bluetoothChatHelper, BluetoothDevice device, boolean secure) {
        mHelper = bluetoothChatHelper;
        mDevice = device;
        BluetoothSocket tmp = null;
        mSocketType = secure ? "Secure" : "Insecure";

        try {
            if (secure) {
                tmp = device.createRfcommSocketToServiceRecord(ChatConstant.UUID_SECURE);
            } else {
                tmp = device.createInsecureRfcommSocketToServiceRecord(ChatConstant.UUID_INSECURE);
            }
        } catch (IOException e) {
            BleLog.e("Socket Type: " + mSocketType + "create() failed", e);
        }
        mSocket = tmp;
    }

    public void run() {
        BleLog.i("BEGIN mConnectThread SocketType:" + mSocketType);
        setName("ConnectThread" + mSocketType);

        mHelper.getAdapter().cancelDiscovery();

        try {
            mSocket.connect();
        } catch (IOException e) {
            try {
                mSocket.close();
            } catch (IOException e2) {
                BleLog.e("unable to close() " + mSocketType + " socket during connection failure", e2);
            }
            mHelper.connectionFailed();
            return;
        }

        synchronized (this) {
            mHelper.setConnectThread(null);
        }

        mHelper.connected(mSocket, mDevice, mSocketType);
    }

    public void cancel() {
        try {
            mSocket.close();
        } catch (IOException e) {
            BleLog.e("close() of connect " + mSocketType
                    + " socket failed", e);
        }
    }
}

在建立連接之前要調用cancelDiscovery()方法。在連接之前應該始終調用這個方法,並且不用實際的檢查藍牙發現處理是否正在運行也是安全的(如果想要檢查,調用isDiscovering()方法)。

管理連接(即好友間通信)

當你成功的連接了兩個(或更多)設備時,每一個設備都有一個被連接的BluetoothSocket對象。這是良好的開始,因為你能夠在設備之間共享數據。使用BluetoothSocket對象來傳輸任意數據的過程是簡單的:

1、分別通過getInputStream()和getOutputStream()方法來獲得通過套接字來處理傳輸任務的InputStream和OutputStream對象;

2、用read(byte[])和write(byte[])方法來讀寫流中的數據。

以下為實現以上過程的通信線程:

public class ConnectedThread extends Thread {

    private final BluetoothChatHelper mHelper;
    private final BluetoothSocket mSocket;
    private final InputStream mInStream;
    private final OutputStream mOutStream;

    public ConnectedThread(BluetoothChatHelper bluetoothChatHelper, BluetoothSocket socket, String socketType) {
        BleLog.i("create ConnectedThread: " + socketType);
        mHelper = bluetoothChatHelper;
        mSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;

        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) {
            BleLog.e("temp sockets not created", e);
        }

        mInStream = tmpIn;
        mOutStream = tmpOut;
    }

    public void run() {
        BleLog.i("BEGIN mConnectedThread");
        int bytes;
        byte[] buffer = new byte[1024];

        // Keep listening to the InputStream while connected
        while (true) {
            try {
                bytes = mInStream.read(buffer);
                byte[] data = new byte[bytes];
                System.arraycopy(buffer, 0, data, 0, data.length);
                mHelper.getHandler().obtainMessage(ChatConstant.MESSAGE_READ, bytes, -1, data).sendToTarget();
            } catch (IOException e) {
                BleLog.e("disconnected", e);
                mHelper.start(false);
                break;
            }
        }
    }

    public void write(byte[] buffer) {
        if(mSocket.isConnected()){
            try {
                mOutStream.write(buffer);
                mHelper.getHandler().obtainMessage(ChatConstant.MESSAGE_WRITE, -1, -1, buffer).sendToTarget();
            } catch (IOException e) {
                BleLog.e("Exception during write", e);
            }
        }
    }

    public void cancel() {
        try {
            mSocket.close();
        } catch (IOException e) {
            BleLog.e("close() of connect socket failed", e);
        }
    }
}

發送消息示例圖:

發送消息

發送表情示例圖:

發送表情

發送文件示例圖:

發送文件
基於Android Classic Bluetooth的藍牙聊天軟件

代碼地址如下:
http://www.demodashi.com/demo/12133.html

注:本文著作權歸作者,由demo大師代發,拒絕轉載,轉載需要作者授權


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM