소스 검색

refactor(开关和设备异常)
- 蓝牙处理

周文健 5 달 전
부모
커밋
45d4d85443

+ 12 - 418
app/src/main/java/com/grkj/iscs/BusinessManager.kt

@@ -1,6 +1,5 @@
 package com.grkj.iscs
 //todo 所有蓝牙包替换com.clj. -> com.clj.
-import android.bluetooth.BluetoothGatt
 import android.content.Context
 import android.content.Intent
 import androidx.appcompat.app.AppCompatActivity
@@ -11,13 +10,11 @@ import com.clj.fastble.exception.BleException
 import com.google.gson.Gson
 import com.grkj.iscs.ble.BleBean
 import com.grkj.iscs.ble.BleCmdManager
+import com.grkj.iscs.ble.BleConnectionManager
 import com.grkj.iscs.ble.BleConst
 import com.grkj.iscs.ble.BleConst.STATUS_READY
 import com.grkj.iscs.ble.BleConst.STATUS_WORK
 import com.grkj.iscs.ble.BleUtil
-import com.grkj.iscs.ble.CustomBleGattCallback
-import com.grkj.iscs.ble.CustomBleIndicateCallback
-import com.grkj.iscs.ble.CustomBleScanCallback
 import com.grkj.iscs.ble.CustomBleWriteCallback
 import com.grkj.iscs.extentions.removeLeadingZeros
 import com.grkj.iscs.extentions.serialNo
@@ -27,7 +24,6 @@ import com.grkj.iscs.modbus.DockBean
 import com.grkj.iscs.modbus.ModBusController
 import com.grkj.iscs.modbus.ModBusController.dockList
 import com.grkj.iscs.modbus.ModBusController.getOneKey
-import com.grkj.iscs.model.Constants.PERMISSION_REQUEST_CODE
 import com.grkj.iscs.model.Constants.USER_TYPE_LOCKER
 import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_CARD
 import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_FINGERPRINT
@@ -75,14 +71,12 @@ import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.activity.LoginActivity
 import com.grkj.iscs.view.base.BaseActivity
 import com.grkj.iscs.view.dialog.TipDialog
-import com.sik.sikcore.activity.ActivityTracker
 import com.sik.sikcore.date.TimeUtils
 import com.sik.sikcore.thread.ThreadUtils
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.async
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
-import pub.devrel.easypermissions.AfterPermissionGranted
 import kotlin.coroutines.resume
 
 /**
@@ -97,9 +91,6 @@ object BusinessManager {
     // 已连接的蓝牙钥匙集合
     var deviceList: MutableList<BleBean> = mutableListOf()
 
-    // 是否正在进行准备流程
-    private var isPreparing: Boolean = false
-
     // Modbus数据页面监听
     class DeviceListener(
         val key: Any,
@@ -108,15 +99,6 @@ object BusinessManager {
 
     private val listeners = ArrayList<DeviceListener>()
 
-    // 蓝牙连接准备监听
-    data class ConnectListener(
-        val mac: String,
-        val callBack: ((Boolean, BleBean?) -> Unit)? = null
-    )
-
-    // 蓝牙连接准备监听集合,全是待准备连接的,准备完毕的会移除
-    private val connectListeners = mutableListOf<ConnectListener>()
-
     // 归还设备是否需要登录
     var NEED_AUTH = true
 
@@ -779,406 +761,14 @@ object BusinessManager {
      * 注册连接监听
      */
     fun registerConnectListener(mac: String, callBack: ((Boolean, BleBean?) -> Unit)? = null) {
-        LogUtil.i("registerConnectListener : $mac")
-        if (connectListeners.any { it.mac == mac }) {
-            LogUtil.w("Ignore mac : $mac 已存在")
-            return
-        }
-        if (deviceList.none { it.bleDevice.mac == mac }) {
-            connectListeners.add(ConnectListener(mac, callBack))
-            if (connectListeners.size > 0) {
-                Executor.runOnMain {
-                    //todo 连接钥匙
-                    connectKey()
-                }
-            }
-        } else {
-            val bean = deviceList.find { it.bleDevice.mac == mac }
-            if (bean?.token == null) {
-                connectListeners.add(ConnectListener(mac, callBack))
-                if (connectListeners.size > 0) {
-                    Executor.runOnMain {
-                        //todo 连接钥匙
-                        connectKey()
-                    }
-                }
-                return
-            }
-            callBack?.invoke(true, bean)
-        }
+        BleConnectionManager.registerConnectListener(mac, callBack)
     }
 
     /**
      * 连接监听反注册
      */
     fun unregisterConnectListener(mac: String, bleBean: BleBean? = null) {
-        LogUtil.i("unregisterConnectListener : $mac")
-        connectListeners.find { it.mac == mac }?.let {
-            // TODO 移除的时候是否要回调待定
-//            it.callBack?.invoke(true, bleBean)
-            connectListeners.remove(it)
-        }
-    }
-
-    /**
-     * 检查是否能进行蓝牙连接准备的下一步,防止未准备完但是已经取消订阅
-     */
-    private fun checkProcess(mac: String?): Boolean {
-        val canProcess = connectListeners.any { it.mac == mac }
-        if (!canProcess) {
-            sendLoadingEventMsg(null, false)
-        }
-        return canProcess
-    }
-
-    /**
-     * 连接钥匙,单个mac走完prepare再进行下一个
-     */
-    private fun connectKey() {
-        if (connectListeners.isEmpty()) {
-            return
-        }
-        if (isPreparing || BleManager.getInstance().allConnectedDevice.size > BleConst.MAX_KEY_STAND_BY) {
-            Executor.delayOnMain(1000) {
-                connectKey()
-            }
-            return
-        }
-        isPreparing = true
-        val listener = connectListeners[0]
-        if (ActivityTracker.getCurrentActivity() == null) {
-            LogUtil.w("Ignore connectKey : ${listener.mac} no current activity")
-            isPreparing = false
-            return
-        }
-        prepareBle(
-            listener.mac, ActivityUtils.currentActivity() as BaseActivity<*>, false
-        ) { isDone, bleBean ->
-            Executor.runOnMain {
-                isPreparing = false
-                if (!isDone) {
-                    // 判断是否仍然待连,防止拿走;移到末尾,防止循环影响
-                    if (checkProcess(listener.mac)) {
-                        unregisterConnectListener(listener.mac)
-                        Executor.delayOnMain(2000) {
-                            registerConnectListener(
-                                listener.mac,
-                                listener.callBack
-                            )
-                        }
-                    }
-                    return@runOnMain
-                }
-                // 判断是否仍然待连,防止拿走
-                // TODO 暂时只处理准备成功
-                if (connectListeners.contains(listener)) {
-                    listener.callBack?.invoke(true, bleBean)
-                    unregisterConnectListener(listener.mac)
-                }
-                if (connectListeners.isNotEmpty()) {
-                    connectKey()
-                }
-            }
-        }
-    }
-
-    /**
-     * @param loadingCallBack 是否显示loading、loading文字、流程是否结束
-     * @param prepareDoneCallBack 蓝牙连接是否成功、蓝牙连接对象
-     */
-    private fun prepareBle(
-        mac: String,
-        activity: AppCompatActivity,
-        isNeedLoading: Boolean = false,
-        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
-    ) {
-        if (!checkProcess(mac)) {
-            LogUtil.e("Prepare is canceled : $mac")
-            return
-        }
-        Executor.runOnMain {
-            CommonUtils.checkBlePermission(activity) {
-                doScanBle(mac, isNeedLoading, prepareDoneCallBack)
-            }
-        }
-    }
-
-    @AfterPermissionGranted(PERMISSION_REQUEST_CODE)
-    private fun doScanBle(
-        mac: String,
-        isNeedLoading: Boolean = false,
-        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
-    ) {
-        LogUtil.i("doScanBle:$mac")
-        if (!checkProcess(mac)) {
-            LogUtil.e("Prepare is canceled : $mac")
-            return
-        }
-        if (isNeedLoading) sendEventMsg(
-            MsgEvent(
-                MSG_EVENT_LOADING, LoadingMsg(true, "正在扫描设备...", null)
-            )
-        )
-        BleUtil.instance?.scan(object : CustomBleScanCallback() {
-            override fun onPrompt(promptStr: String?) {
-                // 蓝牙未启动重试
-                BleManager.getInstance().enableBluetooth()
-                doScanBle(mac, isNeedLoading, prepareDoneCallBack)
-            }
-
-            override fun onScanStarted(success: Boolean) {
-                LogUtil.i("onScanStarted:${success}")
-                if (!success) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING, LoadingMsg(false, null, null)
-                        )
-                    )
-                    prepareDoneCallBack?.invoke(false, null)
-                }
-            }
-
-            override fun onScanning(bleDevice: BleDevice?) {
-                LogUtil.i("onScanning:${bleDevice?.mac}")
-                bleDevice?.let {
-                    deviceList
-                    doConnect(it, isNeedLoading, prepareDoneCallBack)
-                }
-            }
-
-            override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
-                LogUtil.i("onScanFinished: $mac - ${scanResultList?.none { it.mac == mac }}")
-                if (isNeedLoading) sendEventMsg(
-                    MsgEvent(
-                        MSG_EVENT_LOADING, LoadingMsg(false, null, null)
-                    )
-                )
-                // 没有扫描到
-                if (scanResultList?.none { it.mac == mac } == true) {
-                    LogUtil.w("$mac is not scanned")
-                    prepareDoneCallBack?.invoke(false, null)
-                }
-            }
-        })
-    }
-
-    /**
-     * 连接蓝牙设备
-     */
-    private fun doConnect(
-        bleDevice: BleDevice,
-        isNeedLoading: Boolean = false,
-        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
-    ) {
-        LogUtil.i("doConnect : ${bleDevice.mac}")
-        if (!checkProcess(bleDevice.mac)) {
-            LogUtil.e("Prepare is canceled : ${bleDevice.mac}")
-            return
-        }
-        if (isNeedLoading) sendEventMsg(
-            MsgEvent(
-                MSG_EVENT_LOADING,
-                LoadingMsg(true, CommonUtils.getStr(R.string.ble_connecting), null)
-            )
-        )
-        BleManager.getInstance().disconnect(bleDevice)
-        BleUtil.instance?.connectBySelect(
-            bleDevice, object : CustomBleGattCallback() {
-                override fun onPrompt(promptStr: String?) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING, LoadingMsg(false, promptStr, null)
-                        )
-                    )
-                }
-
-                override fun onStartConnect() {}
-
-                override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING,
-                            LoadingMsg(false, CommonUtils.getStr(R.string.ble_connect_fail), false)
-                        )
-                    )
-                    LogUtil.e("onConnectFail : ${bleDevice?.mac} - ${exception?.description}")
-                    prepareDoneCallBack?.invoke(false, null)
-                }
-
-                override fun onConnectSuccess(
-                    bleDevice: BleDevice?, gatt: BluetoothGatt?, status: Int
-                ) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING, LoadingMsg(false, null, null)
-                        )
-                    )
-                    LogUtil.i("onConnectSuccess : ${bleDevice?.mac}")
-                    bleDevice?.let {
-                        deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
-                        val bleBean = BleBean(it)
-                        deviceList.add(bleBean)
-                        removeExceptionKey(it.mac)
-                        // 设置MTU
-                        Executor.delayOnMain(200) {
-                            if (!checkProcess(bleDevice.mac)) {
-                                LogUtil.e("Prepare is canceled : ${bleDevice.mac}")
-                                return@delayOnMain
-                            }
-                            BleUtil.instance?.setMtu(it)
-                        }
-                        // 监听
-                        Executor.delayOnMain(500) {
-                            indicate(bleBean, isNeedLoading, prepareDoneCallBack)
-                        }
-                    }
-                }
-
-                override fun onDisConnected(
-                    isActiveDisConnected: Boolean,
-                    device: BleDevice?,
-                    gatt: BluetoothGatt?,
-                    status: Int
-                ) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING, LoadingMsg(false, null, false)
-                        )
-                    )
-                    LogUtil.i("onDisConnected : ${device?.mac} - $isActiveDisConnected")
-                    getBleDeviceByMac(device?.mac)?.let {
-                        deviceList.remove(it)
-                    }
-                    bleDevice.mac?.let { itMac ->
-                        unregisterConnectListener(itMac)
-                    }
-                    if (!isActiveDisConnected) {
-                        // 测试模式下不重连
-                        if (isTestMode) {
-                            return
-                        }
-                        // 断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况
-                        Executor.delayOnMain(300) {
-                            registerConnectListener(bleDevice.mac) { isDone, bleBean ->
-                                if (isDone && bleBean != null) {
-                                    Executor.delayOnMain(300) {
-                                        getCurrentStatus(6, bleBean.bleDevice)
-                                    }
-                                }
-                            }
-                        }
-                    } else {
-                        ModBusController.updateKeyReadyStatus(bleDevice.mac, false, 3)
-                    }
-                }
-            })
-    }
-
-    /**
-     * 监听蓝牙设备
-     */
-    private fun indicate(
-        bleBean: BleBean?,
-        isNeedLoading: Boolean = false,
-        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
-    ) {
-        if (!checkProcess(bleBean?.bleDevice?.mac)) {
-            LogUtil.e("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
-            return
-        }
-        if (isNeedLoading) sendEventMsg(
-            MsgEvent(
-                MSG_EVENT_LOADING, LoadingMsg(true, "开始监听...", null)
-            )
-        )
-        bleBean?.let {
-            var isIndicateSuccess = false
-            BleUtil.instance?.indicate(
-                it.bleDevice, indicateCallback = object : CustomBleIndicateCallback() {
-                    override fun onPrompt(promptStr: String?) {
-                        LogUtil.i("indicate onPrompt : $promptStr")
-                    }
-
-                    override fun onConnectPrompt(promptStr: String?) {
-                        LogUtil.i("indicate onConnectPrompt : $promptStr")
-                    }
-
-                    override fun onDisConnectPrompt(promptStr: String?) {
-                        LogUtil.i("indicate onDisConnectPrompt : $promptStr")
-                    }
-
-                    override fun onIndicateSuccess() {
-                        LogUtil.i("onIndicateSuccess")
-//                    val testStr = "{\"cardNo\":\"80A8C0F4EA\",\"password\":\"12345678\",\"effectiveTime\":24,\"data\":[{\"taskCode\":\"202401020001\",\"taskId\":\"71b49baa49b343bc84d7e6b829ac1bdc\",\"codeId\":1,\"dataList\":[{\"dataId\":1,\"equipRfidNo\":\"049648B2E31690\",\"infoRfidNo\":\"04E3BCCA201290\",\"target\":1},{\"dataId\":2,\"equipRfidNo\":\"0405982414C563\",\"target\":0,\"prevId\":1}]},{\"taskCode\":\"202401020002\",\"taskId\":\"145b5a4cc38c41e19943f4c8b48d12b0\",\"codeId\":2,\"dataList\":[{\"dataId\":1,\"equipRfidNo\":\"045460F7F4F438\",\"infoRfidNo\":\"04BC6584C65009\",\"target\":1},{\"dataId\":2,\"equipRfidNo\":\"042B99E449E795\",\"target\":0,\"prevId\":1},{\"dataId\":3,\"equipRfidNo\":\"04A312EE848B62\",\"infoRfidNo\":\"04220E86831289\",\"target\":1,\"prevId\":2}]}],\"lockList\":[{\"lockId\":\"1\",\"rfid\":\"040E21443010E9\"},{\"lockId\":\"2\",\"rfid\":\"0457505E5861C2\"}]}"
-//                    sendTicket(testStr, it.bleDevice, loadingCallBack)
-                        isIndicateSuccess = true
-                        getToken(bleBean, isNeedLoading, prepareDoneCallBack)
-                    }
-
-                    override fun onIndicateFailure(exception: BleException?) {
-                        if (isNeedLoading) sendEventMsg(
-                            MsgEvent(
-                                MSG_EVENT_LOADING, LoadingMsg(false, null, false)
-                            )
-                        )
-                        LogUtil.e("onIndicateFailure : ${bleBean.bleDevice.mac} - ${exception?.description}")
-                        Executor.delayOnIO(500) {
-                            if (isIndicateSuccess) {
-                                return@delayOnIO
-                            }
-                            prepareDoneCallBack?.invoke(false, null)
-                        }
-                    }
-
-                    override fun onCharacteristicChanged(data: ByteArray?) {
-                        LogUtil.i("onCharacteristicChanged : ${data?.toHexStrings()}")
-                        data?.let { itData ->
-                            handleRsp(it, itData, isNeedLoading, prepareDoneCallBack)
-                        }
-                    }
-                })
-        }
-    }
-
-    /**
-     * 获取蓝牙钥匙token
-     */
-    private fun getToken(
-        bleBean: BleBean?,
-        isNeedLoading: Boolean = false,
-        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
-    ) {
-        if (!checkProcess(bleBean?.bleDevice?.mac)) {
-            LogUtil.e("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
-            return
-        }
-        if (isNeedLoading) sendEventMsg(
-            MsgEvent(
-                MSG_EVENT_LOADING, LoadingMsg(true, "开始获取token...", null)
-            )
-        )
-        bleBean?.let {
-            BleCmdManager.getToken(it.bleDevice.mac, object : CustomBleWriteCallback() {
-                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING, LoadingMsg(false, "token获取成功", null)
-                        )
-                    )
-                    LogUtil.i("getToken success : ${bleBean.bleDevice.mac}")
-                }
-
-                override fun onWriteFailure(exception: BleException?) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING, LoadingMsg(false, "token获取失败", false)
-                        )
-                    )
-                    LogUtil.e("getToken fail : ${bleBean.bleDevice.mac}")
-                    prepareDoneCallBack?.invoke(false, null)
-                }
-            })
-        }
+        BleConnectionManager.unregisterConnectListener(mac, bleBean)
     }
 
     /******************************************蓝牙通用准备结束******************************************/
@@ -1840,10 +1430,10 @@ object BusinessManager {
                                     )
                                 )
                                 //钥匙取出之后重新再连一把钥匙待机
-                                connectListeners.removeIf { connectListener ->
-                                    connectListener.mac == ModBusController.getKeyByRfid(
-                                        info.nfc
-                                    )?.mac
+                                ModBusController.getKeyByRfid(
+                                    info.nfc
+                                )?.mac?.let {
+                                    unregisterConnectListener(it)
                                 }
                                 //待机数不够就再连一把,但不能是原来那把
                                 if (BleManager.getInstance().allConnectedDevice.size < BleConst.MAX_KEY_STAND_BY) {
@@ -1903,7 +1493,11 @@ object BusinessManager {
                                 // 检查有无当前工作票的钥匙
                                 mDeviceTakeList.find { it.deviceType == DEVICE_TYPE_KEY && it.ticketId == info.ticketId }
                                     ?.let { itKey ->
-                                        sendLoadingEventMsg(MyApplication.instance?.applicationContext!!.getString(R.string.ble_connecting))
+                                        sendLoadingEventMsg(
+                                            MyApplication.instance?.applicationContext!!.getString(
+                                                R.string.ble_connecting
+                                            )
+                                        )
                                         handleGiveKey(itKey)
                                     }
                             }

+ 465 - 0
app/src/main/java/com/grkj/iscs/ble/BleConnectionManager.kt

@@ -0,0 +1,465 @@
+package com.grkj.iscs.ble
+
+import android.bluetooth.BluetoothGatt
+import androidx.appcompat.app.AppCompatActivity
+import com.clj.fastble.BleManager
+import com.clj.fastble.data.BleDevice
+import com.clj.fastble.exception.BleException
+import com.grkj.iscs.BusinessManager
+import com.grkj.iscs.BusinessManager.deviceList
+import com.grkj.iscs.BusinessManager.getBleDeviceByMac
+import com.grkj.iscs.BusinessManager.getCurrentStatus
+import com.grkj.iscs.BusinessManager.isTestMode
+import com.grkj.iscs.BusinessManager.removeExceptionKey
+import com.grkj.iscs.BusinessManager.sendEventMsg
+import com.grkj.iscs.BusinessManager.sendLoadingEventMsg
+import com.grkj.iscs.R
+import com.grkj.iscs.extentions.toHexStrings
+import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.model.Constants.PERMISSION_REQUEST_CODE
+import com.grkj.iscs.model.eventmsg.LoadingMsg
+import com.grkj.iscs.model.eventmsg.MsgEvent
+import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_LOADING
+import com.grkj.iscs.util.ActivityUtils
+import com.grkj.iscs.util.CommonUtils
+import com.grkj.iscs.util.Executor
+import com.grkj.iscs.util.log.LogUtil
+import com.grkj.iscs.view.base.BaseActivity
+import com.sik.sikcore.activity.ActivityTracker
+import pub.devrel.easypermissions.AfterPermissionGranted
+import java.util.LinkedList
+
+/**
+ * BLE 连接管理工具:保持原有扫描、连接、监听、取 Token 流程,
+ * 并增加“最大待机数”功能,超出后断开最旧连接,保证同时在线设备数不超过阈值。
+ */
+object BleConnectionManager {
+    /**
+     * 最大待机连接数,超过则断开最旧设备。
+     * 默认为业务常量 MAX_KEY_STAND_BY,可根据需求调整。
+     */
+    @Volatile
+    var maxStandbyCount: Int = BleConst.MAX_KEY_STAND_BY
+
+
+    // 按连接完成顺序维护待机队列
+    private val standbyQueue = LinkedList<String>()
+
+    // 原有回调管理
+    private val connectListeners = mutableListOf<ConnectListener>()
+    private var isPreparing: Boolean = false
+
+    /**
+     * 注册连接监听
+     */
+    fun registerConnectListener(mac: String, callBack: ((Boolean, BleBean?) -> Unit)? = null) {
+        LogUtil.i("registerConnectListener : $mac")
+        if (connectListeners.any { it.mac == mac }) {
+            LogUtil.w("Ignore mac : $mac 已存在")
+            return
+        }
+        if (deviceList.none { it.bleDevice.mac == mac }) {
+            connectListeners.add(ConnectListener(mac, callBack))
+            if (connectListeners.isNotEmpty()) connectKey()
+        } else {
+            val bean = deviceList.find { it.bleDevice.mac == mac }!!
+            if (bean.token == null) {
+                connectListeners.add(ConnectListener(mac, callBack))
+                connectKey()
+            } else {
+                callBack?.invoke(true, bean)
+            }
+        }
+    }
+
+    /**
+     * 连接监听反注册
+     */
+    fun unregisterConnectListener(mac: String, bleBean: BleBean? = null) {
+        LogUtil.i("unregisterConnectListener : $mac")
+        connectListeners.removeAll { it.mac == mac }
+    }
+
+    /**
+     * 检查是否能进行蓝牙连接准备的下一步,防止未准备完但是已经取消订阅
+     */
+    private fun checkProcess(mac: String?): Boolean {
+        val canProcess = connectListeners.any { it.mac == mac }
+        if (!canProcess) sendLoadingEventMsg(null, false)
+        return canProcess
+    }
+
+    /**
+     * 连接钥匙,单个mac走完prepare再进行下一个
+     */
+    private fun connectKey() {
+        if (connectListeners.isEmpty()) return
+        if (isPreparing || BleManager.getInstance().allConnectedDevice.size >= maxStandbyCount) {
+            Executor.delayOnMain(1000) { connectKey() }
+            return
+        }
+        isPreparing = true
+        val listener = connectListeners.first()
+        if (ActivityTracker.getCurrentActivity() == null) {
+            LogUtil.w("Ignore connectKey : ${listener.mac} no current activity")
+            isPreparing = false
+            return
+        }
+        prepareBle(
+            listener.mac, ActivityUtils.currentActivity() as BaseActivity<*>, false
+        ) { isDone, bleBean ->
+            Executor.runOnMain {
+                isPreparing = false
+                if (!isDone) {
+                    // 判断是否仍然待连,防止拿走;移到末尾,防止循环影响
+                    if (checkProcess(listener.mac)) {
+                        unregisterConnectListener(listener.mac)
+                        Executor.delayOnMain(2000) {
+                            registerConnectListener(
+                                listener.mac,
+                                listener.callBack
+                            )
+                        }
+                    }
+                    return@runOnMain
+                }
+                // 判断是否仍然待连,防止拿走
+                // TODO 暂时只处理准备成功
+                if (connectListeners.contains(listener)) {
+                    listener.callBack?.invoke(true, bleBean)
+                    unregisterConnectListener(listener.mac)
+                }
+                bleBean?.bleDevice?.mac?.let { mac ->
+                    addToStandby(mac)
+                }
+                if (connectListeners.isNotEmpty()) connectKey()
+            }
+        }
+    }
+
+    /**
+     * 添加到待机队列并断开最旧超出连接
+     */
+    private fun addToStandby(mac: String) {
+        synchronized(standbyQueue) {
+            standbyQueue.addLast(mac)
+            while (standbyQueue.size > maxStandbyCount) {
+                val oldMac = standbyQueue.removeFirst()
+                deviceList.find { it.bleDevice.mac == oldMac }?.let { oldBean ->
+                    LogUtil.i("断开最旧待机设备: $oldMac")
+                    BleManager.getInstance().disconnect(oldBean.bleDevice)
+                }
+            }
+        }
+    }
+
+    /**
+     * @param loadingCallBack 是否显示loading、loading文字、流程是否结束
+     * @param prepareDoneCallBack 蓝牙连接是否成功、蓝牙连接对象
+     */
+    private fun prepareBle(
+        mac: String,
+        activity: AppCompatActivity,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        if (!checkProcess(mac)) {
+            LogUtil.e("Prepare is canceled : $mac")
+            return
+        }
+        Executor.runOnMain {
+            CommonUtils.checkBlePermission(activity) {
+                doScanBle(mac, isNeedLoading, prepareDoneCallBack)
+            }
+        }
+    }
+
+    @AfterPermissionGranted(PERMISSION_REQUEST_CODE)
+    private fun doScanBle(
+        mac: String,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        LogUtil.i("doScanBle:$mac")
+        if (!checkProcess(mac)) {
+            LogUtil.e("Prepare is canceled : $mac")
+            return
+        }
+        if (isNeedLoading) sendEventMsg(
+            MsgEvent(
+                MSG_EVENT_LOADING, LoadingMsg(true, "正在扫描设备...", null)
+            )
+        )
+        BleUtil.instance?.scan(object : CustomBleScanCallback() {
+            override fun onPrompt(promptStr: String?) {
+                // 蓝牙未启动重试
+                BleManager.getInstance().enableBluetooth()
+                doScanBle(mac, isNeedLoading, prepareDoneCallBack)
+            }
+
+            override fun onScanStarted(success: Boolean) {
+                LogUtil.i("onScanStarted:${success}")
+                if (!success) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, null, null)
+                        )
+                    )
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+            }
+
+            override fun onScanning(bleDevice: BleDevice?) {
+                LogUtil.i("onScanning:${bleDevice?.mac}")
+                bleDevice?.let {
+                    doConnect(it, isNeedLoading, prepareDoneCallBack)
+                }
+            }
+
+            override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
+                LogUtil.i("onScanFinished: $mac - ${scanResultList?.none { it.mac == mac }}")
+                if (isNeedLoading) sendEventMsg(
+                    MsgEvent(
+                        MSG_EVENT_LOADING, LoadingMsg(false, null, null)
+                    )
+                )
+                // 没有扫描到
+                if (scanResultList?.none { it.mac == mac } == true) {
+                    LogUtil.w("$mac is not scanned")
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+            }
+        })
+    }
+
+    /**
+     * 连接蓝牙设备
+     */
+    private fun doConnect(
+        bleDevice: BleDevice,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        LogUtil.i("doConnect : ${bleDevice.mac}")
+        if (!checkProcess(bleDevice.mac)) {
+            LogUtil.e("Prepare is canceled : ${bleDevice.mac}")
+            return
+        }
+        if (isNeedLoading) sendEventMsg(
+            MsgEvent(
+                MSG_EVENT_LOADING,
+                LoadingMsg(true, CommonUtils.getStr(R.string.ble_connecting), null)
+            )
+        )
+        BleManager.getInstance().disconnect(bleDevice)
+        BleUtil.instance?.connectBySelect(
+            bleDevice, object : CustomBleGattCallback() {
+                override fun onPrompt(promptStr: String?) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, promptStr, null)
+                        )
+                    )
+                }
+
+                override fun onStartConnect() {}
+
+                override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING,
+                            LoadingMsg(false, CommonUtils.getStr(R.string.ble_connect_fail), false)
+                        )
+                    )
+                    LogUtil.e("onConnectFail : ${bleDevice?.mac} - ${exception?.description}")
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+
+                override fun onConnectSuccess(
+                    bleDevice: BleDevice?, gatt: BluetoothGatt?, status: Int
+                ) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, null, null)
+                        )
+                    )
+                    LogUtil.i("onConnectSuccess : ${bleDevice?.mac}")
+                    bleDevice?.let {
+                        deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
+                        val bleBean = BleBean(it)
+                        deviceList.add(bleBean)
+                        removeExceptionKey(it.mac)
+                        // 设置MTU
+                        Executor.delayOnMain(200) {
+                            if (!checkProcess(bleDevice.mac)) {
+                                LogUtil.e("Prepare is canceled : ${bleDevice.mac}")
+                                return@delayOnMain
+                            }
+                            BleUtil.instance?.setMtu(it)
+                        }
+                        // 监听
+                        Executor.delayOnMain(500) {
+                            indicate(bleBean, isNeedLoading, prepareDoneCallBack)
+                        }
+                    }
+                }
+
+                override fun onDisConnected(
+                    isActiveDisConnected: Boolean,
+                    device: BleDevice?,
+                    gatt: BluetoothGatt?,
+                    status: Int
+                ) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, null, false)
+                        )
+                    )
+                    LogUtil.i("onDisConnected : ${device?.mac} - $isActiveDisConnected")
+                    getBleDeviceByMac(device?.mac)?.let {
+                        deviceList.remove(it)
+                    }
+                    bleDevice.mac?.let { itMac ->
+                        unregisterConnectListener(itMac)
+                    }
+                    if (!isActiveDisConnected) {
+                        // 测试模式下不重连
+                        if (isTestMode) {
+                            return
+                        }
+                        // 断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况
+                        Executor.delayOnMain(300) {
+                            registerConnectListener(bleDevice.mac) { isDone, bleBean ->
+                                if (isDone && bleBean != null) {
+                                    Executor.delayOnMain(300) {
+                                        getCurrentStatus(6, bleBean.bleDevice)
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        ModBusController.updateKeyReadyStatus(bleDevice.mac, false, 3)
+                    }
+                }
+            })
+    }
+
+    /**
+     * 监听蓝牙设备
+     */
+    private fun indicate(
+        bleBean: BleBean?,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        if (!checkProcess(bleBean?.bleDevice?.mac)) {
+            LogUtil.e("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
+            return
+        }
+        if (isNeedLoading) sendEventMsg(
+            MsgEvent(
+                MSG_EVENT_LOADING, LoadingMsg(true, "开始监听...", null)
+            )
+        )
+        bleBean?.let {
+            var isIndicateSuccess = false
+            BleUtil.instance?.indicate(
+                it.bleDevice, indicateCallback = object : CustomBleIndicateCallback() {
+                    override fun onPrompt(promptStr: String?) {
+                        LogUtil.i("indicate onPrompt : $promptStr")
+                    }
+
+                    override fun onConnectPrompt(promptStr: String?) {
+                        LogUtil.i("indicate onConnectPrompt : $promptStr")
+                    }
+
+                    override fun onDisConnectPrompt(promptStr: String?) {
+                        LogUtil.i("indicate onDisConnectPrompt : $promptStr")
+                    }
+
+                    override fun onIndicateSuccess() {
+                        LogUtil.i("onIndicateSuccess")
+//                    val testStr = "{\"cardNo\":\"80A8C0F4EA\",\"password\":\"12345678\",\"effectiveTime\":24,\"data\":[{\"taskCode\":\"202401020001\",\"taskId\":\"71b49baa49b343bc84d7e6b829ac1bdc\",\"codeId\":1,\"dataList\":[{\"dataId\":1,\"equipRfidNo\":\"049648B2E31690\",\"infoRfidNo\":\"04E3BCCA201290\",\"target\":1},{\"dataId\":2,\"equipRfidNo\":\"0405982414C563\",\"target\":0,\"prevId\":1}]},{\"taskCode\":\"202401020002\",\"taskId\":\"145b5a4cc38c41e19943f4c8b48d12b0\",\"codeId\":2,\"dataList\":[{\"dataId\":1,\"equipRfidNo\":\"045460F7F4F438\",\"infoRfidNo\":\"04BC6584C65009\",\"target\":1},{\"dataId\":2,\"equipRfidNo\":\"042B99E449E795\",\"target\":0,\"prevId\":1},{\"dataId\":3,\"equipRfidNo\":\"04A312EE848B62\",\"infoRfidNo\":\"04220E86831289\",\"target\":1,\"prevId\":2}]}],\"lockList\":[{\"lockId\":\"1\",\"rfid\":\"040E21443010E9\"},{\"lockId\":\"2\",\"rfid\":\"0457505E5861C2\"}]}"
+//                    sendTicket(testStr, it.bleDevice, loadingCallBack)
+                        isIndicateSuccess = true
+                        getToken(bleBean, isNeedLoading, prepareDoneCallBack)
+                    }
+
+                    override fun onIndicateFailure(exception: BleException?) {
+                        if (isNeedLoading) sendEventMsg(
+                            MsgEvent(
+                                MSG_EVENT_LOADING, LoadingMsg(false, null, false)
+                            )
+                        )
+                        LogUtil.e("onIndicateFailure : ${bleBean.bleDevice.mac} - ${exception?.description}")
+                        Executor.delayOnIO(500) {
+                            if (isIndicateSuccess) {
+                                return@delayOnIO
+                            }
+                            prepareDoneCallBack?.invoke(false, null)
+                        }
+                    }
+
+                    override fun onCharacteristicChanged(data: ByteArray?) {
+                        LogUtil.i("onCharacteristicChanged : ${data?.toHexStrings()}")
+                        data?.let { itData ->
+                            BusinessManager.handleRsp(
+                                it,
+                                itData,
+                                isNeedLoading,
+                                prepareDoneCallBack
+                            )
+                        }
+                    }
+                })
+        }
+    }
+
+    /**
+     * 获取蓝牙钥匙token
+     */
+    private fun getToken(
+        bleBean: BleBean?,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        if (!checkProcess(bleBean?.bleDevice?.mac)) {
+            LogUtil.e("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
+            return
+        }
+        if (isNeedLoading) sendEventMsg(
+            MsgEvent(
+                MSG_EVENT_LOADING, LoadingMsg(true, "开始获取token...", null)
+            )
+        )
+        bleBean?.let {
+            BleCmdManager.getToken(it.bleDevice.mac, object : CustomBleWriteCallback() {
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, "token获取成功", null)
+                        )
+                    )
+                    LogUtil.i("getToken success : ${bleBean.bleDevice.mac}")
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, "token获取失败", false)
+                        )
+                    )
+                    LogUtil.e("getToken fail : ${bleBean.bleDevice.mac}")
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+            })
+        }
+    }
+
+
+    // 蓝牙连接准备监听
+    data class ConnectListener(
+        val mac: String,
+        val callBack: ((Boolean, BleBean?) -> Unit)? = null
+    )
+}

+ 7 - 1
app/src/main/java/com/grkj/iscs/model/UrlConsts.kt

@@ -4,7 +4,8 @@ object UrlConsts {
     //    const val BASE_URL = "http://192.168.28.82:9190"  // 本地
 //    const val BASE_URL = "http://192.168.28.97:9190"    // 车
 //    const val BASE_URL = "http://36.133.174.236:9190"    // 外
-    const val BASE_URL = "http://192.168.0.10:9190"    // 外
+//    const val BASE_URL = "http://192.168.0.10:9190"    // 外
+    const val BASE_URL = "http://192.168.1.121:9190"    // 外
 
     //    const val BASE_URL = "http://120.27.232.27:9190"    // 外
     const val WEB_SOCKET = "ws://192.168.1.127:9090/websocket/iot/127"
@@ -326,4 +327,9 @@ object UrlConsts {
      * 获取RFID标识列表
      */
     const val GET_IS_RFID_TOKEN_PAGE = "/iscs/token/getIsRfidTokenPage"
+
+    /**
+     * 获取锁定站列表
+     */
+    const val GET_IS_LOTO_STATION_PAGE = "/iscs/station/getIsLotoStationPage"
 }

+ 13 - 0
app/src/main/java/com/grkj/iscs/model/vo/machinery/IsLotoStationPageRespVO.kt

@@ -0,0 +1,13 @@
+package com.grkj.iscs.model.vo.machinery
+
+data class IsLotoStationPageRespVO(
+    val total: Int,
+    val size: Int,
+    val current: Int,
+    val records: List<IsLotoStationPageItem>
+)
+
+data class IsLotoStationPageItem(
+    val lotoSerialNumber: String?,
+    val mapId: String?
+)

+ 23 - 1
app/src/main/java/com/grkj/iscs/util/NetApi.kt

@@ -25,6 +25,7 @@ import com.grkj.iscs.model.vo.key.KeyPageRespVO
 import com.grkj.iscs.model.vo.lock.LockInfoRespVO
 import com.grkj.iscs.model.vo.lock.LockPageRespVO
 import com.grkj.iscs.model.vo.lock.LockTakeUpdateReqVO
+import com.grkj.iscs.model.vo.machinery.IsLotoStationPageRespVO
 import com.grkj.iscs.model.vo.machinery.MachineryDetailRespVO
 import com.grkj.iscs.model.vo.machinery.MachineryPageRespVO
 import com.grkj.iscs.model.vo.map.MapInfoRespVO
@@ -1001,7 +1002,7 @@ object NetApi {
         exceptionType: String,
         raiser: Long,
         sourceName: String,
-        hardwareId:String,
+        hardwareId: String,
         callBack: (Boolean) -> Unit
     ) {
         val map = mutableMapOf(
@@ -1421,4 +1422,25 @@ object NetApi {
             }, isGet = true, isAuth = true
         )
     }
+
+    /**
+     * 获取Rfid标识列表
+     */
+    fun getIsLotoStationPage(callBack: (IsLotoStationPageRespVO?) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_RFID_TOKEN_PAGE,
+            false,
+            mapOf(
+                "current" to 1,
+                "size" to 50
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(getRefBean(it))
+                } ?: run {
+                    callBack.invoke(null)
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
 }

+ 17 - 1
app/src/main/java/com/grkj/iscs/view/fragment/DeviceStatusFragment.kt

@@ -28,6 +28,9 @@ import com.zhy.adapter.recyclerview.CommonAdapter
 import com.zhy.adapter.recyclerview.MultiItemTypeAdapter
 import com.zhy.adapter.recyclerview.base.ItemViewDelegate
 import com.zhy.adapter.recyclerview.base.ViewHolder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
 
 /**
  * 硬件状态页
@@ -117,7 +120,20 @@ class DeviceStatusFragment :
 
     override fun onResume() {
         super.onResume()
-        mBinding?.rvDock?.adapter?.notifyDataSetChanged()
+
+        fun refreshAdapter() {
+            ThreadUtils.runOnIO {
+                if (isResumed) {
+                    withContext(Dispatchers.Main) {
+                        mBinding?.rvDock?.adapter?.notifyDataSetChanged()
+                    }
+                    delay(1000)
+                    refreshAdapter()
+                }
+            }
+
+        }
+        refreshAdapter()
         getSlotsStatus()
     }
 

+ 23 - 0
app/src/main/java/com/grkj/iscs/view/fragment/DockTestFragment.kt

@@ -5,10 +5,13 @@ import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentDockTestBinding
+import com.grkj.iscs.extentions.removeLeadingZeros
+import com.grkj.iscs.extentions.toHexStrings
 import com.grkj.iscs.modbus.ModBusController
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_LOCK
 import com.grkj.iscs.util.SPUtils
+import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.view.base.BaseFragment
 import com.zhy.adapter.recyclerview.CommonAdapter
 import com.zhy.adapter.recyclerview.base.ViewHolder
@@ -71,6 +74,16 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
                         holder.setOnClickListener(R.id.tv_turn_off) {
                             ModBusController.controlKeyBuckle(false, deviceIndex == 1, dock.address)
                         }
+                        holder.setOnClickListener(R.id.tv_read) {
+                            ModBusController.readKeyRfid(
+                                dock.address,
+                                if (deviceIndex == 1) 0 else 1
+                            ) { isLeft, res ->
+                                val rfid = res.copyOfRange(3, 11).toHexStrings(false)
+                                    .removeLeadingZeros()
+                                ToastUtils.tip("RFID: ${rfid}")
+                            }
+                        }
                     }
                 }
             }
@@ -97,6 +110,16 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
                         holder.setOnClickListener(R.id.tv_turn_off) {
                             ModBusController.controlLockBuckle(false, dock.address, deviceIndex - 1)
                         }
+                        holder.setOnClickListener(R.id.tv_read) {
+                            ModBusController.readLockRfid(
+                                dock.address,
+                                deviceIndex - 1
+                            ) { res ->
+                                val rfid = res.copyOfRange(3, 11).toHexStrings(false)
+                                    .removeLeadingZeros()
+                                ToastUtils.tip("RFID: ${rfid}")
+                            }
+                        }
                     }
                 }
             }

+ 5 - 4
app/src/main/java/com/grkj/iscs/view/fragment/SwitchStatusFragment.kt

@@ -2,7 +2,6 @@ package com.grkj.iscs.view.fragment
 
 import android.graphics.PointF
 import com.grkj.iscs.BusinessManager
-import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentSwitchStatusBinding
 import com.grkj.iscs.util.BitmapUtil
 import com.grkj.iscs.util.ToastUtils
@@ -32,7 +31,9 @@ class SwitchStatusFragment :
 
     override fun onResume() {
         super.onResume()
-        getMap()
+        presenter?.getMapData {
+            getMap(it)
+        }
         fun refreshSwitchStatus() {
             if (!isResumed) {
                 return
@@ -46,8 +47,8 @@ class SwitchStatusFragment :
         refreshSwitchStatus()
     }
 
-    private fun getMap() {
-        presenter?.getMapInfo(8) { itMapInfo ->
+    private fun getMap(mapId: String) {
+        presenter?.getMapInfo(mapId.toLong()) { itMapInfo ->
             // 如果没有图 URL,直接返回
             val imageUrl = itMapInfo?.imageUrl ?: return@getMapInfo
 

+ 11 - 0
app/src/main/java/com/grkj/iscs/view/presenter/SwitchStatusPresenter.kt

@@ -1,5 +1,7 @@
 package com.grkj.iscs.view.presenter
 
+import com.grkj.iscs.MyApplication
+import com.grkj.iscs.extentions.serialNo
 import com.grkj.iscs.model.vo.machinery.MachineryDetailRespVO
 import com.grkj.iscs.model.vo.map.MapInfoRespVO
 import com.grkj.iscs.model.vo.ticket.LotoMapRespVO
@@ -19,4 +21,13 @@ class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
             }
         }
     }
+
+    fun getMapData(done: (String) -> Unit) {
+        NetApi.getIsLotoStationPage {
+            done(
+                it?.records?.find { it.lotoSerialNumber == MyApplication.instance?.applicationContext!!.serialNo() }?.mapId
+                    ?: "4"
+            )
+        }
+    }
 }

+ 7 - 0
app/src/main/res/layout/item_rv_dock_test_child.xml

@@ -29,8 +29,15 @@
         <TextView
             android:id="@+id/tv_turn_off"
             style="@style/CommonTextView"
+            android:layout_marginRight="@dimen/common_spacing_small"
             android:background="@color/main_color_dark"
             android:padding="2dp"
             android:text="@string/turn_off" />
+        <TextView
+            android:id="@+id/tv_read"
+            style="@style/CommonTextView"
+            android:background="@color/main_color_dark"
+            android:padding="2dp"
+            android:text="@string/turn_read" />
     </LinearLayout>
 </LinearLayout>

+ 1 - 0
app/src/main/res/values-en/strings.xml

@@ -353,4 +353,5 @@
     <string name="no_permission">no permission</string>
     <string name="exception_select_hardware_tip">Please select hardware</string>
     <string name="check_key_and_lock">check key and lock</string>
+    <string name="turn_read">read</string>
 </resources>

+ 1 - 0
app/src/main/res/values-zh/strings.xml

@@ -353,4 +353,5 @@
     <string name="no_permission">权限不足</string>
     <string name="exception_select_hardware_tip">请选择硬件</string>
     <string name="check_key_and_lock">正在检查钥匙和挂锁</string>
+    <string name="turn_read">读</string>
 </resources>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -353,4 +353,5 @@
     <string name="no_permission">权限不足</string>
     <string name="exception_select_hardware_tip">请选择硬件</string>
     <string name="check_key_and_lock">正在检查钥匙和挂锁</string>
+    <string name="turn_read">读</string>
 </resources>