1. 程式人生 > >Android開發——藍芽多裝置連線(四)

Android開發——藍芽多裝置連線(四)

前言

經過一個多月的時間藍芽多裝置連線的重構終於告一段落了,這次的重構不止是程式碼方面的完善,還結合了一些使用者的使用場景,另外增加一些離線操作,使手機端對藍芽的操作更加的便捷,對藍芽裝置的管理更加統一。

場景分析

支援的場景

  • 多裝置連線(一個血壓計和一個廚房秤)—— 在飯點,使用同一個手機連線血壓計和廚房秤,病人在測量血壓,家人在做飯稱量菜品。
  • 把連線到的裝置資訊儲存到本地——方便下次自動連線。
  • 把藍芽返回結果儲存本地——方便在其他頁面獲取資料顯示UI。
  • 同種裝置只能連線一個——保持連線裝置的唯一性,防止找不到連線的裝置。
  • 可以在藍芽列表掃描到所支援的裝置,並連線、斷開和刪除藍芽——對藍芽裝置進行管理,檢視自己掃描到的裝置,並檢視藍芽的連線狀態。
  • 啟動 App 可以自動連線上一次已經連線過的裝置——可以快速的使用裝置。

暫不支援的場景

  • 同時掃描多個同種裝置——可能導致連線到了裝置,而不知道連線的是哪個裝置。
  • 新舊裝置替換——不能直接進行替換,需要在裝置列表頁面刪除舊裝置,之後關掉舊裝置,重新掃描新裝置。

場景實現

效果展示

藍芽多裝置連線效果圖.gif

多裝置連線

現在一臺手機可以連線多個裝置,例如連線藍芽耳機,智慧手環等。既然手機可以連線多個裝置,那麼移動應用也是可以連線多個裝置的(血壓計、心率計等),下面就是移動應用 App 實現多裝置連線的思路方法。

實現思路

關於藍芽連線,主要是 BluetoothGatt 這個型別,每個藍芽的連線都需要用獨立且唯一的 BluetoothGatt 。開始的想法是每個藍芽都重新建立一個 Service, 在新的 Service 內使用 BluetoothGatt 進行連線,然而這個方法是可以實現多裝置連線,但是建立多個 Service 對手機消耗比較大。之後,想到把 BluetoothGatt 儲存起來不就可以了麼,那用什麼儲存呢,既可以臨時儲存多個,又可以按照需要獲取相對應的 BluetoothGatt 。在 java 裡面有個型別 Map(String, Object) ,它是以 key-value 的形式儲存到 Map 中。可以根據當時的 Key 來取相應的 Value 值,而且在關掉程序時相應的變數也就釋放了。

程式碼實現

    private Map<String, BluetoothGatt> mBluetoothGattMap = new HashMap<>(); //臨時儲存 BluetoothGatt
    private Map<String, BluetoothGattCharacteristic> mGattCharacteristicMap = new HashMap<>();// 臨時儲存藍芽的特徵值 Characteristic
    private Map<String, BluetoothInfo> mBluetoothInfoMap = new
HashMap<>();// 臨時儲存自己設定的藍芽資訊(deviceName、deviceType、startCMD、stopCMD 等) private Map<BluetoothGatt, String> mDeviceTypeMap = new HashMap<>();// 臨時儲存 deviceType private Map<String, GGBLEDeviceEntity> mConnectModelMap = new HashMap<>();// 臨時儲存 已連線的裝置 //... /** * 連線裝置 * * @param deviceType 裝置型別 * @return true 連線成功,false連線失敗 */ boolean connectBluetooth(Context context, String deviceType, String deviceAddress) { //... BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceAddress);//根據 mac 地址獲取藍芽裝置 //... BluetoothGatt bluetoothGatt = device.connectGatt(this, false, mGattCallback);// 對藍芽進行連線操作 //... mBluetoothGattMap.put(deviceType, bluetoothGatt);//把 BluetoothGatt 已 key-value 的形式臨時儲存起來 return true; }

裝置自動連線

要有良好的使用者操作體驗,我們應該避免對一些無關的操作重複進行。例如我們第一次開啟應用連線了藍芽裝置,以後再開啟 App 不需要重複操作連線過程,使用者就可以少開啟一個頁面,少點選兩次按鈕。減少使用者重複操作,讓使用者直接進入正題,提高主功能的使用率。

實現思路

在 Android 中連線藍芽的方法是

public BluetoothGatt connectGatt(Context context, boolean autoConnect,
                                     BluetoothGattCallback callback) {
        return (connectGatt(context, autoConnect,callback, TRANSPORT_AUTO));
    }

其中 BluetoothGatt 是每個連線成功的藍芽返回唯一的屬性。當藍芽裝置連線成功後會返回唯一的 BluetoothGatt ,並用它進行對藍芽的命令操作;autoConnect 是設定是否為自動連線的一個屬性,然而根據我自己的測試,當 autoConnect 的屬性設定為 true 時,是有可能自動連線的,但是有時也會失效,所以不採用;BluetoothGattCallback 是藍芽連線、命令操作,資料返回等成功時的回撥。
因為 autoConnect 的設定具有不確定性,所以我們採取另一種方式:當我們第一次連線藍芽成功的時候,把藍芽的 Mac 地址儲存起來;在第二次啟動 App 的時候,先把藍芽的 Mac 地址從 SharedPreferences 中取出來,用 Mac 地址進行連線,如果連線失敗(可能結果是裝置不對或者裝置沒有開啟),我們就開啟藍芽掃描功能,進行重新掃描裝置,開啟裝置進行連線。

程式碼實現

/**
  *儲存 mac 地址到 SharedPreferences
  */
public void saveMac(Context context, String macAddress){
if (null != context) {
            SharedPreferences sharedPreferences = context.getSharedPreferences(BLUETOOTH_MAC_TABLE, Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putString(deviceType, bluetoothAddress);
            editor.apply();
            GGLog.i(TAG, "save success");
        }
}

void autoConnect(Context context, final String deviceType) {
        /*防止藍芽 adapter 為空,程式崩潰*/
        if (null == mBluetoothAdapter) {
            GGLog.e(TAG, "method discoveryBluetooth \n mBluetoothAdapter is null");
            return;
        }
        //根據 deviceType 獲取 藍芽 mac 地址
        SharedPreferences sharedPreferences = context.getSharedPreferences(BLUETOOTH_MAC_TABLE, Context.MODE_PRIVATE);
        String deviceAddress = sharedPreferences.getString(deviceType, "");
        if (!"".equals(deviceAddress)) {
            if (!deviceAddressList.contains(deviceAddress)) {
                deviceAddressList.add(deviceAddress);
            }
            connectBluetooth(context, deviceType, deviceAddress);// 連線裝置
        }
    }

void connectBluetooth(Context context, String deviceType, String deviceAddress){
//...
// 連線失敗 開啟掃描功能
if (!bluetoothGatt.connect() || bluetoothGatt.getServices().size() == 0) {
            startScan();
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    stopScan();//十秒後停止掃描
                }
            }, 10000);
            return false;
        }
}

不同頁面共享結果資料

在移動端我們很多時候,同一份資料要在多個頁面上顯示,又不想多次呼叫介面,我們只好把資料儲存到本地,來達到資料共享的目的。

實現思路

在我這藍芽的開發中,是使用 SQL 對資料進行儲存。藍芽測量中的資料因為是實時更新,我們不需要進行快取,只要把測量的資料結果和藍芽的資訊儲存下來就可以了。
使用 SQL 有一點不好的是使用 ContentValues 以 key-value 的形式進行儲存,那麼帶來一個問題就是 key 我們手寫很容易寫錯,所以可能一不小心就會萬劫不復。在 Git 上有個輕量級的資料庫第三方 Litepal (https://github.com/LitePalFramework/LitePal) ,它操作簡單,不需要手寫 key ,對一些常用的增刪改查都進行了封裝,而且還支援手寫 SQL 語句,在 CSDN 上部落格專家郭霖有詳細的介紹 Litepal 的說明和使用方法。在這裡我使用的是原生的 SQL ,防止與庫外的第三方衝突。
藍芽裝置資訊的儲存,我儲存了以下資訊:

儲存欄位 型別 功能說明
deviceType String 裝置型別,更具裝置型別進行藍芽操作
deviceAddress String 藍芽的 mac 地址,用來連線藍芽
deviceName String 藍芽名稱,掃描藍芽時,進行校驗
connectStatus int 藍芽連線狀態

對測量結果的儲存內容是:

儲存欄位 型別 功能說明
deviceType String 裝置型別,更具裝置型別進行藍芽操作
measuredData String 測量資料,把測量完成的資料進行儲存,又 int[] 轉成 String 進行儲存
measuredTime long 測量時間
isUpdate boolean 上傳狀態,判斷是否上傳服務區,如果沒有,則在有網路的情況下自動上傳

程式碼實現

/**
     * 插入一條測量結果。因為資料庫是封裝在藍芽庫裡面,我們獲取不到 Application的 Context 所以傳遞一個 context
     */
    void insertComplete(Context context, CompleteModel entity) {
        SQLiteTemplate sqLiteTemplate = SQLiteTemplate.getInstance(context, instance.mBLEDBManager, false);
        ContentValues values = new ContentValues();
        values.put("deviceType", entity.getDeviceType()); //儲存藍芽裝置型別
        values.put("measuredData", Arrays.toString(entity.getMeasuredData()));//儲存測量結果
        values.put("measuredTime", entity.getMeasuredTime());// 儲存測量時間
        values.put("isUpdate", entity.isUpdate());//儲存是否已經上傳完畢
        sqLiteTemplate.insert("ble_complete_table", values);// 資料插入表中
    }

這些都是移動端對資料庫的簡單操作,剩下的 更新、刪除、查詢的方法就不都一一列舉了。對於藍芽裝置資訊的儲存與儲存測量結果相似,也不列舉了。

藍芽搜尋、連線、刪除

既然我們有單獨藍芽列表頁面,那麼就要有對藍芽的一些基本的操作。在列表頁面我們可以對藍芽進行搜尋,發現周圍開啟的藍芽裝置;點選連結,連線我們需要使用的血壓計、廚房秤等;長安斷開連結並刪除相應的藍芽裝置,當我們裝置不再使用或者更換新裝置的時候,我們可以刪除多餘的裝置,使頁面看起來更加簡潔。

實現思路

首先,我們在BluetoothService裡面對掃描到的藍芽進行區分,檢查是否是我們設定的的藍芽裝置(extends BaseBluetoothAdapter的類),根據設定的藍芽名稱(deviceName)篩選掃描到的裝置,之後把裝置通過 Listener 監聽傳遞到 Activity 中,並新增到列表裡顯示出來。

程式碼實現

以下是 BluetoothService 中藍芽掃描結果的處理

private void scanResult(BluetoothDevice device, int type) {
        if (null != device) {
            String name = device.getName();//獲取掃描到的裝置名稱
            if (null != name) {
                //獲取我們自己設定的藍芽詳情(deviceType、deviceName等)
                for (BluetoothInfo bluetoothInterface : mInterfaceList) {
                    String deviceName = bluetoothInterface.getDeviceName();
                    //判斷裝置名稱是否與我們自己設定的名稱相同
                    if (!deviceName.equals(name)) {
                        continue;
                    }
                    //判斷裝置是否已經新增到列表中
                    if (deviceNameList.contains(deviceName)) {
                        continue;
                    }
                    // 把裝置名稱新增到列表中
                    if (!deviceNameList.contains(deviceName)) {
                        deviceNameList.add(deviceName);
                    }
                    //如果掃描到的裝置是上次連線過的裝置,則自動連線。
                    if (null != deviceAddressList && deviceAddressList.size() > 0) {
                        for (String address : deviceAddressList) {
                            if (address.equals(device.getAddress())) {
                                connectBluetooth(null, bluetoothInterface.getDeviceType(), address);
                            }
                        }
                    }
                    GGBLEDeviceEntity entity = BLEDeviceToGGBLEEntity(bluetoothInterface.getDeviceType(), device);//把bluetoothDevice 轉換成我們自定義的BLEEntity。
                    if (null != mResultListener) {
                        mResultListener.onScanResult(entity);//掃描結果監聽賦值
                    } else {
                        setError(bluetoothInterface.getDeviceType(), HHCBluetoothProfile.ERROR_NULL, "mResultListener is null");
                    }
                }
            }
        }
    }

其他

  • 在多裝置連線中,需要時刻獲取到連線的裝置是什麼,才能獲取到相應的資料。通過 onBluetoothConnectStatus 介面方法只能在連線狀態發生改變的時候才會回撥,不能在已進入頁面的時候就獲取到裝置的連線狀態,因此我們在 BluetoothService 中提供兩個方法:
//根據裝置型別獲取連線的藍芽裝置
GGBLEDeviceEntity getConnectedDevice(String deviceType) {
        return mConnectModelMap.get(deviceType);
    }
//獲取所有的連線裝置
List<GGBLEDeviceEntity> getAllConnectedDevice() {
        List<GGBLEDeviceEntity> connectModelList = new ArrayList<>();
        for (String type : mConnectModelMap.keySet()) {
            connectModelList.add(mConnectModelMap.get(type));
        }
        return connectModelList;
    }
  • 在 service 中不能持久化 Context ,如果定義了 Context 會發生記憶體溢位,所以在我們所提供的方法中都會傳入 context 用於承接上下文。
  • 我把 service 內對外開放的方法都集中在一個類裡,方便呼叫和管理。
  • 藍芽掃描的方法有三種:
    1、在5.0之前使用的是:mBluetoothAdapter.startLeScan(mLeScanCallback);;
    對應的停止掃描為:mBluetoothAdapter.stopLeScan(mLeScanCallback);
    2、在 Android 5.0之後 使用的是:mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);;
    對應的停止掃描為:mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
    3、一種通用的掃描方法:mBluetoothAdapter.startDiscovery();
    對應的停止掃描為:mBluetoothAdapter.cancelDiscovery();

總結

  • 在這次的開發中,主要面臨的問題是場景方面的考慮,開始只是單一的想要進行多個裝置的連線,沒有考慮現實中的場景,做出的第一個版本,只能在同一個 Activity 中進行多裝置連線,而不能各自在不同的頁面進行操作,資料也不能共享;之後和 leader 多次討論,想到很多場景,結合這些場景,羅列出開發功能,並調整程式碼結構,讓使用者使用起來更加方便。總之,一切的功能開發都是需要場景來支撐。
  • 在資料儲存這方面還有待改進,手寫的 key-value 形式很容易出現書寫錯誤,即使寫錯也不容易找到錯誤出處。改進方向是資料儲存直接儲存物件而不是一個一個的 key-value 進行儲存。