Android藍牙開發(二)經典藍牙消息傳輸實現

上篇文章中,我們主要介紹了藍牙模塊,傳統/經典藍牙模塊BT和低功耗藍牙BLE及其相關的API,不熟悉的可以查看Android藍牙開發(一)藍牙模塊及核心API 進行了解。

本篇主要記錄用到的經典藍牙開發流程及連接通訊。

1. 開啓藍牙

藍牙連接前,給與相關係統權限:

<!-- 使用藍牙的權限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<!-- 掃描藍牙設備或者操作藍牙設置 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!--模糊定位權限,僅作用於6.0+-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--精準定位權限,僅作用於6.0+-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

安卓6.0以上系統要動態請求及獲取開啓GPS內容:

/**
 * 檢查GPS是否打開
 */
private boolean checkGPSIsOpen() {
    LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
    if (locationManager == null)
        return false;
    return locationManager.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER);
}

藍牙核心對象獲取,若獲取對象爲null則說明設備不支持藍牙:

mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

判斷藍牙是否開啓,沒有則開啓:

//判斷藍牙是否開啓
if (!mBluetoothAdapter.isEnabled()) {
    //開啓藍牙,耗時,監聽廣播進行後續操作
    mBluetoothAdapter.enable();
}

2.掃描藍牙

藍牙掃描:

mBluetoothAdapter.startDiscovery();

取消掃描:

mBluetoothAdapter.cancelDiscovery();

3.藍牙狀態監聽

藍牙監聽廣播,監聽藍牙開關,發現設備,掃描結束等狀態,定義狀態回調接口,進行對應操作,例如:監聽到藍牙開啓後,進行設備掃描;發現設備後進行連接等。

/**
 * 監聽藍牙廣播-各種狀態
 */
public class BluetoothReceiver extends BroadcastReceiver {
    private static final String TAG = BluetoothReceiver.class.getSimpleName();
    private final OnBluetoothReceiverListener mOnBluetoothReceiverListener;

    public BluetoothReceiver(Context context,OnBluetoothReceiverListener onBluetoothReceiverListener) {
        mOnBluetoothReceiverListener = onBluetoothReceiverListener;
        context.registerReceiver(this,getBluetoothIntentFilter());
    }

    private IntentFilter getBluetoothIntentFilter() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);//藍牙開關狀態
        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);//藍牙開始搜索
        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//藍牙搜索結束
        filter.addAction(BluetoothDevice.ACTION_FOUND);//藍牙發現新設備(未配對的設備)
        return filter;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (action == null) {
            return;
        }
        Log.i(TAG, "===" + action);
        BluetoothDevice dev = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        if (dev != null) {
            Log.i(TAG, "BluetoothDevice: " + dev.getName() + ", " + dev.getAddress());
        }
        switch (action) {
            case BluetoothAdapter.ACTION_STATE_CHANGED:
                int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0);
                Log.i(TAG, "STATE: " + state);
                if (mOnBluetoothReceiverListener != null) {
                    mOnBluetoothReceiverListener.onStateChanged(state);
                }
                break;
            case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
                Log.i(TAG, "ACTION_DISCOVERY_STARTED ");
                break;
            case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
                if (mOnBluetoothReceiverListener != null) {
                    mOnBluetoothReceiverListener.onScanFinish();
                }
                break;
            case BluetoothDevice.ACTION_FOUND:
                short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MAX_VALUE);
                Log.i(TAG, "EXTRA_RSSI:" + rssi);
                if (mOnBluetoothReceiverListener != null) {
                    mOnBluetoothReceiverListener.onDeviceFound(dev);
                }
                break;
            default:
                break;
        }
    }

    public interface OnBluetoothReceiverListener {
        /**
         * 發現設備
         *
         * @param bluetoothDevice 藍牙設備
         */
        void onDeviceFound(BluetoothDevice bluetoothDevice);

        /**
         * 掃描結束
         */
        void onScanFinish();

        /**
         * 藍牙開啓關閉狀態
         *
         * @param state 狀態
         */
        void onStateChanged(int state);
    }
}

4.通訊連接

客戶端,與服務端建立長連接,進行通訊:

/**
 * 客戶端,與服務端建立長連接
 */
public class BluetoothClient extends BaseBluetooth {

    public BluetoothClient(BTListener BTListener) {
        super(BTListener);
    }

    /**
     * 與遠端設備建立長連接
     *
     * @param bluetoothDevice 遠端設備
     */
    public void connect(BluetoothDevice bluetoothDevice) {
        close();
        try {
//             final BluetoothSocket socket = bluetoothDevice.createRfcommSocketToServiceRecord(SPP_UUID); //加密傳輸,Android強制執行配對,彈窗顯示配對碼
            final BluetoothSocket socket = bluetoothDevice.createInsecureRfcommSocketToServiceRecord(SPP_UUID); //明文傳輸(不安全),無需配對
            // 開啓子線程(必須在新線程中進行連接操作)
            EXECUTOR.execute(new Runnable() {
                @Override
                public void run() {
                    //連接,並進行循環讀取
                    loopRead(socket);
                }
            });
        } catch (Throwable e) {
            close();
        }
    }
}

服務端監聽客戶端發起的連接,進行接收及通訊:

/**
 * 服務端監聽和連接線程,只連接一個設備
 */
public class BluetoothServer extends BaseBluetooth {
    private BluetoothServerSocket mSSocket;

    public BluetoothServer(BTListener BTListener) {
        super(BTListener);
        listen();
    }

    /**
     * 監聽客戶端發起的連接
     */
    public void listen() {
        try {
            BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
//            mSSocket = adapter.listenUsingRfcommWithServiceRecord("BT", SPP_UUID); //加密傳輸,Android強制執行配對,彈窗顯示配對碼
            mSSocket = adapter.listenUsingInsecureRfcommWithServiceRecord("BT", SPP_UUID); //明文傳輸(不安全),無需配對
            // 開啓子線程
            EXECUTOR.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        BluetoothSocket socket = mSSocket.accept(); // 監聽連接
                        mSSocket.close(); // 關閉監聽,只連接一個設備
                        loopRead(socket); // 循環讀取
                    } catch (Throwable e) {
                        close();
                    }
                }
            });
        } catch (Throwable e) {
            close();
        }
    }

    @Override
    public void close() {
        super.close();
        try {
            mSSocket.close();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

客戶端連接及服務端監聽基類,用於客戶端和服務端之前Socket消息通訊,進行消息或文件的發送、接收,進行通訊關閉操作等:

public class BaseBluetooth {
    public static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
    protected static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); //自定義
    private static final String FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/bluetooth/";
    private static final int FLAG_MSG = 0;  //消息標記
    private static final int FLAG_FILE = 1; //文件標記

    private BluetoothSocket mSocket;
    private DataOutputStream mOut;
    private BTListener mBTListener;
    private boolean isRead;
    private boolean isSending;

    public BaseBluetooth(BTListener BTListener) {
        mBTListener = BTListener;
    }

    /**
     * 循環讀取對方數據(若沒有數據,則阻塞等待)
     */
    public void loopRead(BluetoothSocket socket) {
        mSocket = socket;
        try {
            if (!mSocket.isConnected())
                mSocket.connect();
            notifyUI(BTListener.CONNECTED, mSocket.getRemoteDevice());
            mOut = new DataOutputStream(mSocket.getOutputStream());
            DataInputStream in = new DataInputStream(mSocket.getInputStream());
            isRead = true;
            while (isRead) { //循環讀取
                switch (in.readInt()) {
                    case FLAG_MSG: //讀取短消息
                        String msg = in.readUTF();
                        notifyUI(BTListener.MSG_RECEIVED, msg);
                        break;
                    case FLAG_FILE: //讀取文件
                        File file = new File(FILE_PATH);
                        if (!file.exists()) {
                            file.mkdirs();
                        }
                        String fileName = in.readUTF(); //文件名
                        long fileLen = in.readLong(); //文件長度
                        notifyUI(BTListener.MSG_RECEIVED, "正在接收文件(" + fileName + ")····················");
                        // 讀取文件內容
                        long len = 0;
                        int r;
                        byte[] b = new byte[4 * 1024];
                        FileOutputStream out = new FileOutputStream(FILE_PATH + fileName);
                        while ((r = in.read(b)) != -1) {
                            out.write(b, 0, r);
                            len += r;
                            if (len >= fileLen)
                                break;
                        }
                        notifyUI(BTListener.MSG_RECEIVED, "文件接收完成(存放在:" + FILE_PATH + ")");
                        break;
                }
            }
        } catch (Throwable e) {
            close();
        }
    }

    /**
     * 發送短消息
     */
    public void sendMsg(String msg) {
        if (isSending || TextUtils.isEmpty(msg))
            return;
        isSending = true;
        try {
            mOut.writeInt(FLAG_MSG); //消息標記
            mOut.writeUTF(msg);
        } catch (Throwable e) {
            close();
        }
        notifyUI(BTListener.MSG_SEND, "發送短消息:" + msg);
        isSending = false;
    }

    /**
     * 發送文件
     */
    public void sendFile(final String filePath) {
        if (isSending || TextUtils.isEmpty(filePath))
            return;
        isSending = true;
        EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    notifyUI(BTListener.MSG_SEND, "正在發送文件(" + filePath + ")····················");
                    FileInputStream in = new FileInputStream(filePath);
                    File file = new File(filePath);
                    mOut.writeInt(FLAG_FILE); //文件標記
                    mOut.writeUTF(file.getName()); //文件名
                    mOut.writeLong(file.length()); //文件長度
                    int r;
                    byte[] b = new byte[4 * 1024];
                    while ((r = in.read(b)) != -1) {
                        mOut.write(b, 0, r);
                    }
                    notifyUI(BTListener.MSG_SEND, "文件發送完成.");
                } catch (Throwable e) {
                    close();
                }
                isSending = false;
            }
        });
    }

    /**
     * 關閉Socket連接
     */
    public void close() {
        try {
            isRead = false;
            if (mSocket != null) {
                mSocket.close();
                notifyUI(BTListener.DISCONNECTED, null);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * 當前設備與指定設備是否連接
     */
    public boolean isConnected(BluetoothDevice dev) {
        boolean connected = (mSocket != null && mSocket.isConnected());
        if (dev == null)
            return connected;
        return connected && mSocket.getRemoteDevice().equals(dev);
    }

    private void notifyUI(final int state, final Object obj) {
        mBTListener.socketNotify(state, obj);
    }

    public interface BTListener {
        int DISCONNECTED = 0;
        int CONNECTED = 1;
        int MSG_SEND = 2;
        int MSG_RECEIVED = 3;

        void socketNotify(int state, Object obj);
    }

我這裏只是簡單記錄了項目中用到的藍牙通訊,兩個設備之間不通過配對進行連接、通訊。相關詳細內容及 使用請查看我的GitHub項目DevelopmentRecord:https://github.com/MickJson/DevelopmentRecord

藍牙配對操作及其它內容,可以詳細查看我下面的參考資料,寫的十分詳細,比如設備通過MAC地址,可以通過BluetoothAdapter獲取設備,再通過客戶端connect方法去進行連接等。

BluetoothDevice btDev = mBluetoothAdapter.getRemoteDevice(address);

最後

連接中遇到問題:read failed, socket might closed or timeout, read ret: -1。

通過改UUID,反射等方法都還是會出現錯誤。連接時,要確保服務端及客戶端都處於完全斷開狀態,否則連接就會出現以上問題,但偶爾還是會有問題,期待有什麼好的方法可留言告訴我。

參考資料:

Android-經典藍牙(BT)-建立長連接傳輸短消息和文件

Android藍牙開發—經典藍牙詳細開發流程

歡迎點贊/評論,你們的贊同和鼓勵是我寫作的最大動力!

關注公衆號:幾圈年輪,查看更多有趣的技術資源。

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