Full Code of buhuiming/BleCore for AI

main 9ff5f7654a1a cached
131 files
494.4 KB
121.4k tokens
1 requests
Download .txt
Showing preview only (571K chars total). Download the full file or copy to clipboard to get everything.
Repository: buhuiming/BleCore
Branch: main
Commit: 9ff5f7654a1a
Files: 131
Total size: 494.4 KB

Directory structure:
gitextract_p5rasqxh/

├── .gitignore
├── README.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── bhm/
│       │               └── demo/
│       │                   └── ExampleInstrumentedTest.kt
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── bhm/
│       │   │           └── demo/
│       │   │               ├── BaseActivity.kt
│       │   │               ├── adapter/
│       │   │               │   ├── DetailsExpandAdapter.kt
│       │   │               │   ├── DeviceListAdapter.kt
│       │   │               │   └── LoggerListAdapter.kt
│       │   │               ├── constants/
│       │   │               │   └── Constants.kt
│       │   │               ├── entity/
│       │   │               │   ├── CharacteristicNode.kt
│       │   │               │   ├── LogEntity.kt
│       │   │               │   ├── OperateType.kt
│       │   │               │   ├── RefreshBleDevice.kt
│       │   │               │   └── ServiceNode.kt
│       │   │               ├── ui/
│       │   │               │   ├── DetailOperateActivity.kt
│       │   │               │   ├── MainActivity.kt
│       │   │               │   └── OptionSettingActivity.kt
│       │   │               └── vm/
│       │   │                   ├── DetailViewModel.kt
│       │   │                   └── MainViewModel.kt
│       │   └── res/
│       │       ├── drawable/
│       │       │   └── ic_launcher_background.xml
│       │       ├── drawable-v24/
│       │       │   └── ic_launcher_foreground.xml
│       │       ├── layout/
│       │       │   ├── activity_detail.xml
│       │       │   ├── activity_main.xml
│       │       │   ├── activity_setting.xml
│       │       │   ├── layout_recycler_characteristic.xml
│       │       │   ├── layout_recycler_item.xml
│       │       │   ├── layout_recycler_log.xml
│       │       │   └── layout_recycler_service.xml
│       │       ├── values/
│       │       │   ├── colors.xml
│       │       │   ├── strings.xml
│       │       │   └── themes.xml
│       │       ├── values-night/
│       │       │   └── themes.xml
│       │       └── xml/
│       │           ├── backup_rules.xml
│       │           └── data_extraction_rules.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── bhm/
│                       └── demo/
│                           └── ExampleUnitTest.kt
├── ble/
│   ├── .gitignore
│   ├── build.gradle
│   ├── consumer-rules.pro
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           └── java/
│               └── com/
│                   └── bhm/
│                       └── ble/
│                           ├── BleManager.kt
│                           ├── attribute/
│                           │   └── BleOptions.kt
│                           ├── callback/
│                           │   ├── BleBaseCallback.kt
│                           │   ├── BleConnectCallback.kt
│                           │   ├── BleEventCallback.kt
│                           │   ├── BleIndicateCallback.kt
│                           │   ├── BleMtuChangedCallback.kt
│                           │   ├── BleNotifyCallback.kt
│                           │   ├── BleReadCallback.kt
│                           │   ├── BleRssiCallback.kt
│                           │   ├── BleScanCallback.kt
│                           │   ├── BleWriteCallback.kt
│                           │   └── BluetoothCallback.kt
│                           ├── control/
│                           │   ├── BleLruHashMap.kt
│                           │   ├── BleTask.kt
│                           │   ├── BleTaskList.kt
│                           │   └── BleTaskQueue.kt
│                           ├── data/
│                           │   ├── BleConnectFailType.kt
│                           │   ├── BleConnectLastState.kt
│                           │   ├── BleDescriptorGetType.kt
│                           │   ├── BleException.kt
│                           │   ├── BleScanFailType.kt
│                           │   ├── BleTaskQueueType.kt
│                           │   ├── BleWriteData.kt
│                           │   ├── BleWriteQueueData.kt
│                           │   └── Constants.kt
│                           ├── device/
│                           │   ├── BleConnectedDevice.kt
│                           │   ├── BleConnectedDeviceManager.kt
│                           │   └── BleDevice.kt
│                           ├── log/
│                           │   ├── BleLogEvent.kt
│                           │   ├── BleLogLevel.kt
│                           │   ├── BleLogManager.kt
│                           │   └── BleLogger.kt
│                           ├── receiver/
│                           │   └── BluetoothReceiver.kt
│                           ├── request/
│                           │   ├── BleConnectRequest.kt
│                           │   ├── BleIndicateRequest.kt
│                           │   ├── BleMtuRequest.kt
│                           │   ├── BleNotifyRequest.kt
│                           │   ├── BleReadRequest.kt
│                           │   ├── BleRssiRequest.kt
│                           │   ├── BleScanRequest.kt
│                           │   ├── BleSetPriorityRequest.kt
│                           │   ├── BleWriteRequest.kt
│                           │   └── base/
│                           │       ├── BleBaseRequest.kt
│                           │       ├── BleRequestImp.kt
│                           │       ├── BleTaskQueueRequest.kt
│                           │       └── Request.kt
│                           └── utils/
│                               └── BleUtil.kt
├── build.gradle
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── maven_upload.gradle
├── settings.gradle
└── support/
    ├── .gitignore
    ├── build.gradle
    ├── consumer-rules.pro
    ├── proguard-rules.pro
    └── src/
        └── main/
            ├── AndroidManifest.xml
            ├── java/
            │   └── com/
            │       └── bhm/
            │           └── support/
            │               └── sdk/
            │                   ├── common/
            │                   │   ├── BaseActivity.kt
            │                   │   ├── BaseApplication.kt
            │                   │   ├── BaseFragment.kt
            │                   │   ├── BaseVBActivity.kt
            │                   │   ├── BaseVBFragment.kt
            │                   │   ├── BaseViewModel.kt
            │                   │   └── DefaultViewPagerAdapter.kt
            │                   ├── constants/
            │                   │   ├── AppConstants.kt
            │                   │   └── PermissionConstants.kt
            │                   ├── core/
            │                   │   ├── AppTheme.kt
            │                   │   ├── GlideCircleTransform.kt
            │                   │   ├── GlideRoundTransform.kt
            │                   │   ├── GridSpacingItemDecoration.kt
            │                   │   ├── MyStaggeredGridLayoutManager.kt
            │                   │   └── WeakHandler.kt
            │                   ├── entity/
            │                   │   └── MessageEvent.kt
            │                   ├── utils/
            │                   │   ├── ActivityUtil.kt
            │                   │   ├── DateUtil.kt
            │                   │   ├── DisplayUtil.kt
            │                   │   ├── NotificationUtil.kt
            │                   │   ├── SPUtil.kt
            │                   │   └── ViewUtil.kt
            │                   └── widget/
            │                       └── ChoseView.kt
            └── res/
                └── values/
                    ├── attrs.xml
                    ├── colors.xml
                    └── ids.xml

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
.idea/.gitignore
.idea/compiler.xml
.idea/gradle.xml
.idea/intellij-javadocs-4.0.1.xml
.idea/misc.xml
.idea/sbt.xml
.idea/vcs.xml
.idea/codeStyles/codeStyleConfig.xml
.idea/codeStyles/Project.xml
.idea/inspectionProfiles/Project_Default.xml
.idea/sonarlint/issuestore/index.pb
.idea/sonarlint/issuestore/0/5/05efc8b1657769a27696d478ded1e95f38737233
.idea/sonarlint/issuestore/0/7/0712df971a99ac4d2fccb8e0fb19f377f3374cca
.idea/sonarlint/issuestore/1/4/141f074f42b7618731e03860b83591695fc45b34
.idea/sonarlint/issuestore/1/7/17ebefcfefc1932c79193259a35c282e59f29e32
.idea/sonarlint/issuestore/2/a/2afbb999f001938c88fa43fc2ef52abf0f8213e4
.idea/sonarlint/issuestore/3/f/3f604333d77f906a746c7ba9f863d2139ac381ba
.idea/sonarlint/issuestore/4/0/40ce750d38060383ffa2ca21095e83385593ead2
.idea/sonarlint/issuestore/4/4/443bfeba92309bf514f3e39be0961d08b0b756a7
.idea/sonarlint/issuestore/4/7/47e330b37492be03483c7657e9fcec3513e5ec0f
.idea/sonarlint/issuestore/9/8/98cca74c1c0c3e8343788bf02a1700ac660f893a
.idea/sonarlint/issuestore/9/f/9f08693c85f95f731b2faec96449210cc5407271
.idea/sonarlint/issuestore/d/3/d396928ac15e23fca51b0277da6aac0508305f1d
.idea/sonarlint/issuestore/f/0/f07866736216be0ee2aba49e392191aeae700a35
.idea/sonarlint/issuestore/f/0/f098d04f62ef69cedfc781eb9212c5c5bbf1dc88
.idea/sonarlint/issuestore/f/2/f25afbb4385061bdb9965342e5eb4474aa843ff6
.idea/sonarlint/issuestore/f/4/f4a01d6a4fcb971362ec00a83903fd3902f52164
.idea/sonarlint/issuestore/f/6/f6706a18253c0aebf3b63b4f8147984ed99b3fb9
.idea/sonarlint/issuestore/f/b/fbe448ebfc3eb2d4e308f6b8b043666f5b57235e
.idea/sonarlint/issuestore/f/f/ff8b003b156eab859952b4376b9cfbfac02fd59d
.idea/


================================================
FILE: README.md
================================================
# BleCore Android蓝牙低功耗(BLE)快速开发框架

## 本项目持续维护更新

*   如果觉得对您有帮助,可以犒劳一下作者
*   ![1744788695450](https://github.com/user-attachments/assets/62a16ec4-e1d9-4bb3-9dc3-bc6e11cad38f)
 


*   当前版本[![](https://jitpack.io/v/buhuiming/BleCore.svg)](https://jitpack.io/#buhuiming/BleCore) 
*   minSdk 24
*   targetSdk 35
*   compileSdk 35

#### * 基于Kotlin、协程
#### * 基于sdk 35,最新API
#### * 详细的完整的容错机制
#### * 基于多个蓝牙库的设计思想
#### * 强大的Notify\Indicate\Read\Write任务队列

![20230613110126](https://github.com/buhuiming/BleCore/blob/main/screenshots/20230613110126.png)
![20230613110146](https://github.com/buhuiming/BleCore/blob/main/screenshots/20230613110146.png)
![20230614090104](https://github.com/buhuiming/BleCore/blob/main/screenshots/20230614090104.png)

### demo体验

![apk_address](https://github.com/buhuiming/BleCore/blob/main/screenshots/apk_address.png)

###
###

### 详细用法参考demo 
### 详细用法参考demo 
### 详细用法参考demo 

### 用法

        allprojects {
            repositories {
                ...
                maven { url 'https://jitpack.io' }
            }
        }

        dependencies {
            implementation 'com.github.buhuiming:BleCore:latest version'
        }

#### 添加权限

    //动态申请
    val LOCATION_PERMISSION = 
        if (VERSION.SDK_INT < VERSION_CODES.S) {
            arrayOf(
                //注册精准位置权限,否则可能Ble扫描不到设备
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION,
            )
        } else {
            arrayOf(
            Manifest.permission.BLUETOOTH_SCAN,
            Manifest.permission.BLUETOOTH_CONNECT,
            )
        }
*    特别注意:权限Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION
     在Android12及以上版本已经去掉、包括校验。权限Manifest.permission.BLUETOOTH_ADVERTISE已经去掉。

*    注意:
*    有些设备GPS是关闭状态的话,申请定位权限之后,GPS是依然关闭状态,这里要根据GPS是否打开来跳转页面
*    BleUtil.isGpsOpen(context) 判断GPS是否打开(可以通过BleOptions.setNeedCheckGps(boolean)来配置是否需要GPS检测)
*    跳转到系统GPS设置页面,GPS设置是全局的独立的,是否打开跟权限申请无关
     startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
*    跳转到系统蓝牙设置页面
     startActivity(Intent(Settings.ACTION_BLUETOOTH_SETTINGS))

#### 初始化
    val options =
            BleOptions.builder()
                .setScanServiceUuid("0000ff80-0000-1000-8000-00805f9b34fb", "0000ff90-0000-1000-8000-00805f9b34fb")
                .setScanDeviceName("midea", "BYD BLE3")
                .setScanDeviceAddress("70:86:CE:88:7A:AF", "5B:AE:65:88:59:5E", "B8:8C:29:8B:BE:07")
                .isContainScanDeviceName(true)
                .setAutoConnect(false)
                .setEnableLog(true)
                .setScanMillisTimeOut(12000)
                //这个机制是:不会因为扫描的次数导致上一次扫描到的数据被清空,也就是onScanStart和onScanComplete
                //都只会回调一次,而且扫描到的数据是所有扫描次数的总和
                .setScanRetryCountAndInterval(2, 1000)
                .setConnectMillisTimeOut(10000)
                .setConnectRetryCountAndInterval(2, 5000)
                .setOperateMillisTimeOut(6000)
                .setOperateInterval(80)
                .setMaxConnectNum(5)
                .setMtu(500)
                .setTaskQueueType(BleTaskQueueType.Operate)
                .build()
    BleManager.get().init(application, options)

    //或者使用默认配置
    BleManager.get().init(application)

setTaskQueueType方法,有3个选项分别是:
*   BleTaskQueueType.Default
    一个设备的Notify\Indicate\Read\Write\mtu操作所对应的任务共享同一个任务
    队列(共享队列)(不区分特征值),rssi在rssi队列
*   BleTaskQueueType.Operate
    一个设备每个操作独立一个任务队列(不区分特征值)
    Notify在Notify队列中,Indicate在Indicate队列中,Read在Read队列中,
    Write在Write队列中,mtu在共享队列,rssi在rssi队列中,
    不同操作任务之间相互不影响,相同操作任务之间先进先出按序执行
    例如特征值1的写操作和特征值2的写操作,在同一个任务队列当中;特征值1的写操作和特征值1的读操作,
    在两个不同的任务队列当中,特征值1的读操作和特征值2的写操作,在两个不同的任务队列当中。
*   BleTaskQueueType.Independent
    一个设备每个特征值下的每个操作独立一个任务队列(区分特征值)
    Notify\Indicate\Read\Write所对应的任务分别放入到独立的任务队列中,
    mtu在共享队列,rssi在rssi队列中,
    且按特征值区分,不同操作任务之间相互不影响,相同操作任务之间相互不影响
    例如特征值1的写操作和特征值2的写操作,在两个不同的任务队列当中;特征值1的写操作和特征值1的读操作,
    在两个不同的任务队列当中,特征值1的读操作和特征值2的写操作,在两个不同的任务队列当中。

注意:BleTaskQueueType.Operate、BleTaskQueueType.Independent这两种模式下
* 1、在Notify\Indicate\Read\Write 未完成的情况下,不要执行设置Mtu,否则会导致前者操作失败
* 2、同时执行Notify\Indicate\Read\Write其中两个以上操作,会可能报设备忙碌失败
 
建议:以上模式主要也是针对操作之间的问题,强烈建议不要同时执行2个及以上操作,模式BleTaskQueueType.Default就是为
     了让设备所有操作同一时间只执行一个,Rssi不受影响
    
#### 扫描
    注意:扫描之前先检查权限、检查GPS开关、检查蓝牙开关
    扫描及过滤过程是在工作线程中进行,所以不会影响主线程的UI操作,最终每一个回调结果都会回到主线程。
    开启扫描:
    BleManager.get().startScan {
        onScanStart {

        }
        onLeScan { bleDevice, currentScanCount ->
            //可以根据currentScanCount是否已有清空列表数据
        }
        onLeScanDuplicateRemoval { bleDevice, currentScanCount ->
            //与onLeScan区别之处在于:同一个设备只会出现一次
        }
        onScanComplete { bleDeviceList, bleDeviceDuplicateRemovalList ->
            //扫描到的数据是所有扫描次数的总和
        }
        onScanFail {
            val msg: String = when (it) {
                is BleScanFailType.UnSupportBle -> "BleScanFailType.UnSupportBle: 设备不支持蓝牙"
                is BleScanFailType.NoBlePermission -> "BleScanFailType.NoBlePermission: 权限不足,请检查"
                is BleScanFailType.GPSDisable -> "BleScanFailType.BleDisable: 设备未打开GPS定位"
                is BleScanFailType.BleDisable -> "BleScanFailType.BleDisable: 蓝牙未打开"
                is BleScanFailType.AlReadyScanning -> "BleScanFailType.AlReadyScanning: 正在扫描"
                is BleScanFailType.ScanError -> {
                    "BleScanFailType.ScanError: ${it.throwable?.message}"
                }
            }
            BleLogger.e(msg)
            Toast.makeText(application, msg, Toast.LENGTH_SHORT).show()
        }
    }

#### 停止扫描
    BleManager.get().stopScan()

#### 是否扫描中
    BleManager.get().isScanning()

#### 连接
    BleManager.get().connect(device)
    BleManager.get().connect(deviceAddress)

*    在某些型号手机上,connectGatt必须在主线程才能有效,所以把连接过程放在主线程,回调也在主线程。
*    为保证重连成功率,建议断开后间隔一段时间之后进行重连。(非常关键,因为断开后会有释放资源的等待时间,如果马上重连,会导致连接的资源会被释放掉,而产生错误)
*    v1.9.0添加字段isForeConnect,主要针对某些机型,当触发连接超时回调连接失败并释放资源之后,此时外设开启触发手机系统已连接,但BleCore资源被
     释放 (bluetoothGatt是null),或BleCore和系统的连接状态不一致,而导致setMtu和Notify/Indicate都失败。

#### 停止连接
    BleManager.get().stopConnect(device)

#### 断开连接
    BleManager.get().disConnect(device)
    BleManager.get().disConnect(deviceAddress)

*    断开后,并不会马上更新状态,所以马上连接会直接返回已连接,而且扫描不出来,要等待一定时间才可以
*    BleConnectCallback中onDisConnecting、onDisConnected分别是,断开连接时触发onDisConnecting,
     真正断开之后触发onDisConnected。(isActiveDisConnected = true的时候,触发onDisConnecting之后大约1秒左右
     才会触发onDisConnected;isActiveDisConnected = false的时候,触发onDisConnecting之后大约5毫秒左右
     才会触发onDisConnected)

#### 是否已连接
    BleManager.get().isConnected(bleDeviceAddress: String, simplySystemStatus: Boolean = true)
    BleManager.get().isConnected(bleDevice: BleDevice?, simplySystemStatus: Boolean = true)

*    simplySystemStatus 为true,只根据系统的状态规则;为false,会根据sdk的状态,换句话说,只根据系统的状态返回。
     此字段的意义在于:有时,sdk资源被系统回收(状态未连接),但是系统的状态是已连接。

#### 扫描并连接,如果扫描到多个设备,则会连接第一个
    BleManager.get().startScanAndConnect(bleScanCallback: BleScanCallback,
                                         bleConnectCallback: BleConnectCallback)

*    扫描到首个符合扫描规则的设备后,便停止扫描,然后连接该设备。

#### 获取设备的BluetoothGatt对象
    BleManager.get().getBluetoothGatt(device)

#### 设置Notify
    BleManager.get().notify(bleDevice: BleDevice,
                                  serviceUUID: String,
                                  notifyUUID: String,
                                  bleDescriptorGetType: BleDescriptorGetType = BleDescriptorGetType.Default,
                                  bleIndicateCallback: BleIndicateCallback)
BleDescriptorGetType设计原则
*    正常情况下,每个特征值下至少有一个默认描述符,并且遵循蓝牙联盟定义的UUID规则,如
     [com.bhm.ble.data.Constants.UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR]便是蓝牙联盟定义的
     客户端特性配置的描述符UUID,这样做是方便BLE终端在接入不同类型设备时,能够获取到正确的配置。
     比如有一个APP,需要 接入A商家的智能手表和B商家的智能手表来监听用户的心跳,而如果A商家的智能手表或者B商家的智能手表
     不遵循蓝牙联盟定义关于 心跳相关的UUID,则对APP来说就要分别去获取A商家的智能手表或者B商家的智能手表对应特征值的描述
     符UUID,显然是不合理的。当然这个是需要硬件设备支持的,也就是说硬件设备可以自定义UUID,但需要遵循规则。
*    在开发过程中,我们会遇到不同硬件设备定义UUID的情况,有的硬件设备通过特征值的UUID来获取描述符(用来writeDescriptor,
     打开或关闭notify、indicate),而非是通过系统提供接受通知自带的UUID获取描述符。此外特征值有多个描述符时,获取其中
     一个描述符来写入数据,可能会导致onCharacteristicChanged函数没有回调,我不确定是否是硬件设备需要支持修改的问题。
     因此[AllDescriptor]方式则是简单粗暴的将特征值下所有的描述符都写入数据,以保证onCharacteristicChanged函数回调,
     这个方法经过了一系列设备的验证可行,但不保证是完全有效的。

#### 取消Notify
    BleManager.get().stopNotify(bleDevice: BleDevice,
                                  serviceUUID: String,
                                  notifyUUID: String,
                                  bleDescriptorGetType: BleDescriptorGetType = BleDescriptorGetType.Default)

#### 设置Indicate
    BleManager.get().indicate(bleDevice: BleDevice,
                                  serviceUUID: String,
                                  indicateUUID: String,
                                  bleDescriptorGetType: BleDescriptorGetType = BleDescriptorGetType.Default,
                                  bleIndicateCallback: BleIndicateCallback)

#### 取消Indicate
    BleManager.get().stopIndicate(bleDevice: BleDevice,
                                  serviceUUID: String,
                                  indicateUUID: String,
                                  bleDescriptorGetType: BleDescriptorGetType = BleDescriptorGetType.Default)

#### 读取信号值
    BleManager.get().readRssi(bleDevice: BleDevice, bleRssiCallback: BleRssiCallback)
    
*    获取设备的信号强度,需要在设备连接之后进行。
*    某些设备可能无法读取Rssi,不会回调onRssiSuccess(),而会因为超时而回调onRssiFail()。

#### 设置Mtu值
    BleManager.get().setMtu(bleDevice: BleDevice, bleMtuChangedCallback: BleMtuChangedCallback) 

*    设置MTU,需要在设备连接之后进行操作。
*    默认每一个BLE设备都必须支持的MTU为23。
*    MTU为23,表示最多可以发送20个字节的数据。
*    该方法的参数mtu,最小设置为23,最大设置为512。
*    并不是每台设备都支持拓展MTU,需要通讯双方都支持才行,也就是说,需要设备硬件也支持拓展MTU该方法才会起效果。
     调用该方法后,可以通过onMtuChanged(int mtu)查看最终设置完后,设备的最大传输单元被拓展到多少。如果设备不支持,
     可能无论设置多少,最终的mtu还是23。 
*    建议在indicate、notify、read、write未完成的情况下,不要执行设置Mtu,否则会导致前者操作失败


#### 设置连接的优先级
    BleManager.get().setConnectionPriority(connectionPriority: Int)

*    设置连接的优先级,一般用于高速传输大量数据的时候可以进行设置。

#### 读特征值数据
    BleManager.get().readData(bleDevice: BleDevice,
                              serviceUUID: String,
                              readUUID: String,
                              bleIndicateCallback: BleReadCallback)

#### 写数据
     BleManager.get().writeData(bleDevice: BleDevice,
                                serviceUUID: String,
                                writeUUID: String,
                                data: ByteArray,
                                bleWriteCallback: BleWriteCallback)
     BleManager.get().writeData(bleDevice: BleDevice,
                                serviceUUID: String,
                                writeUUID: String,
                                data: SparseArray,
                                bleWriteCallback: BleWriteCallback)

*    因为分包后每一个包,可能是包含完整的协议,所以分包由业务层处理,组件只会根据包的长度和mtu值对比后是否拦截
*    特殊情况下:indicate\mtu\notify\read\rssi 这些操作,同一个特征值在不同地方调用(不同callback),最后面的操作
     对应的回调才会触发,其他地方先前的操作对应的回调不会触发
     解决方案:业务层每个特征值对应的操作维护一个单例的callback对象(假如为SingleCallback),在不同地方调用再传递callback
             (放入到SingleCallback中的集合CallbackList),SingleCallback 回调时循环CallbackList中的callback,这样就达到了
              同一个特征值在不同地方调用,都能收到回调
     
*    indicate\mtu\notify\read\rssi这些操作 ,同一个特征值在不同地方调用,后面的操作会取消前面未完成的操作;write操作比较
     特殊,每个写操作都会有回调,且write操作之间不会被取消。具体详情看taskId

*    一次写操作,分包后,假如某个数据包写失败,后面的数据包不会继续写,例如一次写操作分包后有10个数据包,第7个写失败,后面第8、9、10不会再写     

#### 断开某个设备的连接 释放资源
    BleManager.get().close(bleDevice: BleDevice)

#### 断开所有连接 释放资源
    BleManager.get().closeAll()

#### 一些移除监听的函数
    BleManager.get().removeBleScanCallback()
    BleManager.get().removeBleConnectCallback(bleDevice: BleDevice)
    BleManager.get().removeBleIndicateCallback(bleDevice: BleDevice, indicateUUID: String)
    BleManager.get().removeBleNotifyCallback(bleDevice: BleDevice, notifyUUID: String)
    BleManager.get().removeBleRssiCallback(bleDevice: BleDevice)
    BleManager.get().removeBleMtuChangedCallback(bleDevice: BleDevice)
    BleManager.get().removeBleReadCallback(bleDevice: BleDevice, readUUID: String)
    BleManager.get().removeBleWriteCallback(bleDevice: BleDevice, writeUUID: String)

#### v1.5.0新增addBleEventCallback方法
    有用户反馈,设置[connect]的bleConnectCallback、[notify]的bleNotifyCallback、
     [indicate]的bleIndicateCallback、[setMtu]的bleMtuChangedCallback之后,当其他地方需要监听这些回调时比较
    不方便,所以添加addBleEventCallback来实现。addBleEventCallback与上述回调共存

#### v1.7.0新增系统蓝牙变化广播监听
    BleManager.get().registerBluetoothStateReceiver()
    BleManager.get().unRegisterBluetoothStateReceiver()

#### v1.8.0新增stopConnect方法停止或者取消连接
    BleManager.get().stopConnect(device)

#### v2.0.0新增writeQueueData方法
    BleManager.get().writeQueueData(),此方法支持跳过空数据包,支持写失败后重试,提高成功率。可以用于OTA升级

#### 获取BleCore日志,使用自定义的日志框架打印日志或收集BleCore日志
    通过第一步初始化时候,setEnableLog方法来决定是否使用BleCore的日志打印;
    业务层,通过实现BleLogEvent接口,如下:

    class MainActivity : BaseActivity(), BleLogEvent {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            //添加BleCore日志监听
            BleLogManager.get().addLogListener(this)
        }

        override fun onDestroy() {
            super.onDestroy()
            //移除BleCore日志监听
            BleLogManager.get().removeLogListener(this)
        }

        /**
         * 获取BleCore库的日志,并统一使用Logger来打印日志获取其他收集功能
        */
        override fun onLog(level: BleLogLevel, tag: String, message: String?) {
            if (message.isNullOrEmpty()) {
                return
            }
           when (level) {
               is BleLogLevel.Debug ->  Logger.d(tag, message)
               is BleLogLevel.Info ->  Logger.i(tag, message)
               is BleLogLevel.Warn ->  Logger.w(tag, message)
               is BleLogLevel.Error ->  Logger.e(tag, message)
           }
        }
    } 
    

#### [问题锦集](https://juejin.cn/post/6844903896100372494),但愿对你有帮助
    https://blog.51cto.com/u_16213573/7811086
* 1、少部分机型会存在断开连接(gatt.disconnect)后,连接状态仍未刷新,导致其他机型连接不上外设。
  [参考](https://stackoverflow.com/questions/44521828/android-ble-gatt-disconnected-vs-device-disconnected)

#### 其他
* 1、关闭系统蓝牙,没有触发onConnectionStateChange
  解决方案:
  1、操作前判断蓝牙状态,
  2、系统蓝牙变化广播监听
     BleManager.get().registerBluetoothStateReceiver(getBluetoothCallback())
     BleManager.get().unRegisterBluetoothStateReceiver()


#### 考虑把Collections.synchronizedList换成其他不会导致死锁的集合

## License

```
Copyright (c) 2023 Bekie

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```


================================================
FILE: app/.gitignore
================================================
/build

================================================
FILE: app/build.gradle
================================================
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.bhm.demo'
    compileSdk 35

    defaultConfig {
        applicationId "com.bhm.ble"
        minSdk 24
        targetSdk 35
        versionCode 261
        versionName "2.6.1"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = '17'
    }
    buildFeatures {
        viewBinding true
    }
}

dependencies {

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.3.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'

    implementation "io.github.cymchad:BaseRecyclerViewAdapterHelper:3.0.13"
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'

    implementation project(":ble")
    implementation project(":support")
}

================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

================================================
FILE: app/src/androidTest/java/com/bhm/demo/ExampleInstrumentedTest.kt
================================================
package com.bhm.demo

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.bhm.ble", appContext.packageName)
    }
}

================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">

    <application
            android:name="com.bhm.support.sdk.common.BaseApplication"
            android:allowBackup="true"
            android:fullBackupContent="@xml/backup_rules"
            tools:targetApi="31"
            android:dataExtractionRules="@xml/data_extraction_rules"
            android:label="@string/app_name"
            android:icon="@mipmap/ic_launcher"
            android:supportsRtl="true"
            android:theme="@style/Theme.BleCore" >
        <activity
                android:name=".ui.MainActivity"
                android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.bhm.demo.ui.OptionSettingActivity" />
        <activity android:name="com.bhm.demo.ui.DetailOperateActivity" />
    </application>

</manifest>

================================================
FILE: app/src/main/java/com/bhm/demo/BaseActivity.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo

import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.view.KeyEvent
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.viewbinding.ViewBinding
import com.bhm.network.base.HttpActivity
import com.bhm.network.base.HttpLoadingDialog
import com.bhm.network.core.HttpOptions
import com.bhm.support.sdk.common.BaseViewModel
import com.bhm.support.sdk.core.AppTheme
import com.bhm.support.sdk.core.WeakHandler
import com.bhm.support.sdk.entity.MessageEvent
import com.bhm.support.sdk.utils.ActivityUtil
import com.bhm.support.sdk.utils.ViewUtil
import com.noober.background.BackgroundLibrary
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode


/**
 * demo activity基类
 *
 * @author Buhuiming
 * @date 2023年05月24日 16时06分
 */
abstract class BaseActivity<VM : BaseViewModel, B : ViewBinding> : HttpActivity(), Handler.Callback {

    lateinit var viewModel: VM

    private var activityLauncher: ActivityResultLauncher<Intent>? = null

    private var permissionLauncher: ActivityResultLauncher<Array<String>>? = null

    private var arCallback: ((resultCode: Int, resultIntent: Intent?) -> Unit)? = null

    private var permissionAgree: (() -> Unit)? = null

    private var permissionRefuse: ((refusePermissions: ArrayList<String>) -> Unit)? = null

    lateinit var mainHandler: WeakHandler

    lateinit var viewBinding: B

    lateinit var rootView: View

    private var httpOptions: HttpOptions? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        BackgroundLibrary.inject(this)
        super.onCreate(savedInstanceState)
        AppTheme.fitSystemWindow(this)
        ActivityUtil.addActivity(this)
        EventBus.getDefault().register(this)
        init()
        viewBinding = ViewUtil.inflateWithGeneric(this, layoutInflater)
        rootView = viewBinding.root
        setContentView(rootView)
        initData()
        initEvent()
    }

    fun showLoading(msg: String? = "") {
        httpOptions = HttpOptions.create(this)
            .setLoadingDialog(HttpLoadingDialog())
            .setLoadingTitle(msg)
            .setDialogAttribute(
                true,
                cancelable = false,
                dialogDismissInterruptRequest = true
            )
            .build()
        httpOptions?.let {
            it.dialog?.showLoading(it)
        }
    }

    fun dismissLoading() {
        httpOptions?.dialog?.dismissLoading(this)
    }

    protected open fun initData() {}

    protected open fun initEvent() {}

    override fun onDestroy() {
        super.onDestroy()
        EventBus.getDefault().unregister(this)
        ActivityUtil.removeActivity(this)
        mainHandler.removeCallbacksAndMessages(null)
    }

    /**
     *ViewModel绑定
     */
    private fun init() {
        viewModel = createViewModel(this, createViewModel())
        activityLauncher = registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()
        ) { result ->
            if (result != null) {
                arCallback?.let {
                    it(result.resultCode, result.data)
                }
            }
        }
        permissionLauncher = registerForActivityResult(
            ActivityResultContracts.RequestMultiplePermissions()
        ) {
            val refusePermission: ArrayList<String> = ArrayList()
            it.keys.forEach { res ->
                if (it[res] == false) {
                    refusePermission.add(res)
                }
            }

            if (refusePermission.isNotEmpty()) {
                permissionRefuse?.let {
                    it(refusePermission)
                }
            } else {
                permissionAgree?.let {
                    it()
                }
            }
        }
        mainHandler = WeakHandler(Looper.getMainLooper(), this)
    }

    /**
     * 创建ViewModel
     */
    abstract fun createViewModel(): VM

    /** 是否屏蔽返回键
     * @return
     */
    protected open fun isRefusedBackPress(): Boolean {
        return false
    }

    private fun createViewModel(owner: ViewModelStoreOwner, viewModel: VM): VM {
        return ViewModelProvider(owner)[viewModel.javaClass]
    }

    fun startActivity(intent: Intent, arCallback: (resultCode: Int, resultIntent: Intent?) -> Unit) {
        this.arCallback = arCallback
        activityLauncher?.launch(intent)
    }

    fun requestPermission(permissions: Array<String>,
                          agree: () -> Unit,
                          refuse: (refusePermissions: ArrayList<String>) -> Unit
    ) {
        this.permissionAgree = agree
        this.permissionRefuse = refuse
        var allAgree = true
        for (permission in permissions) {
            if( ContextCompat.checkSelfPermission(this, permission) !=
                PackageManager.PERMISSION_GRANTED) {
                allAgree=false
                break
            }
        }
        if (allAgree) {
            permissionAgree?.let {
                it()
            }
            return
        }
        permissionLauncher?.launch(permissions)
    }

    override fun handleMessage(msg: Message): Boolean {
        return false
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    open fun onMessageEvent(event: MessageEvent?) {
        //EventBus Do something
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if (isRefusedBackPress() && keyCode == KeyEvent.KEYCODE_BACK) {  //欢迎页 按物理返回键不能关闭APP
            return true
        } else if (keyCode == KeyEvent.KEYCODE_BACK) {
            finish()
        }
        return super.onKeyDown(keyCode, event)
    }
}

================================================
FILE: app/src/main/java/com/bhm/demo/adapter/DetailsExpandAdapter.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo.adapter

import android.bluetooth.BluetoothGattCharacteristic
import android.view.View
import android.widget.Button
import android.widget.CheckBox
import com.bhm.demo.R
import com.bhm.demo.entity.CharacteristicNode
import com.bhm.demo.entity.OperateType
import com.bhm.demo.entity.ServiceNode
import com.bhm.support.sdk.utils.ViewUtil
import com.chad.library.adapter.base.BaseNodeAdapter
import com.chad.library.adapter.base.entity.node.BaseNode
import com.chad.library.adapter.base.provider.BaseNodeProvider
import com.chad.library.adapter.base.viewholder.BaseViewHolder


/**
 * 折叠布局 显示服务 特征值
 *
 * @author Buhuiming
 * @date 2023年06月01日 10时10分
 */
class DetailsExpandAdapter(nodeList: MutableList<BaseNode>,
                           operateCallback: ((checkBox: CheckBox?,
                                              operateType: OperateType,
                                              isChecked: Boolean,
                                              node: CharacteristicNode) -> Unit)? = null
) : BaseNodeAdapter(nodeList) {

    init {
        // 需要占满一行的,使用此方法(例如section)
        addFullSpanNodeProvider(ServiceNodeProvider())
        // 普通的item provider
        addNodeProvider(CharacteristicProvider(operateCallback))
    }

    override fun getItemType(data: List<BaseNode>, position: Int): Int {
        return when (data[position]) {
            is ServiceNode -> 0
            is CharacteristicNode -> 1
            else -> -1
        }
    }

    class ServiceNodeProvider : BaseNodeProvider() {
        override val itemViewType: Int
            get() = 0
        override val layoutId: Int
            get() = R.layout.layout_recycler_service

        override fun convert(helper: BaseViewHolder, item: BaseNode) {
            val node = item as ServiceNode
            helper.setText(R.id.tvServiceName, "服务: (${node.serviceName})")
            helper.setText(R.id.tvServiceUUID, "ServiceUUID: ${node.serviceUUID}")
            helper.setVisible(R.id.ivExpand, node.childNode?.isNotEmpty() == true)
            if (node.isExpanded) {
                helper.setImageResource(R.id.ivExpand, R.drawable.icon_down)
            } else {
                helper.setImageResource(R.id.ivExpand, R.drawable.icon_right)
            }
        }

        override fun onClick(helper: BaseViewHolder, view: View, data: BaseNode, position: Int) {
            super.onClick(helper, view, data, position)
            getAdapter()?.expandOrCollapse(position, animate = true, notify = true)
        }
    }

    class CharacteristicProvider(private val operateCallback: ((checkBox: CheckBox?,
                                                                operateType: OperateType,
                                                                isChecked: Boolean,
                                                                node: CharacteristicNode) -> Unit)? = null
    ) : BaseNodeProvider() {
        override val itemViewType: Int
            get() = 1
        override val layoutId: Int
            get() = R.layout.layout_recycler_characteristic

        override fun convert(helper: BaseViewHolder, item: BaseNode) {
            val node = item as CharacteristicNode
            helper.setText(R.id.tvCharacteristicName, "特征(${node.characteristicName})")
            helper.setText(R.id.tvCharacteristicUUID, "CharacteristicUUID: ${node.characteristicUUID}")
            helper.setText(R.id.tvCharacteristicProperties, "CharacteristicProperties: ${node.characteristicProperties}")
            helper.setGone(R.id.tvCharacteristicProperties, node.characteristicProperties.isEmpty())

            val cbWrite = helper.getView<CheckBox>(R.id.cbWrite)
            val btnReadData = helper.getView<Button>(R.id.btnReadData)
            val cbNotify = helper.getView<CheckBox>(R.id.cbNotify)
            val cbIndicate = helper.getView<CheckBox>(R.id.cbIndicate)
            cbWrite.isChecked = node.enableWrite
            cbNotify.isChecked = node.enableNotify
            cbIndicate.isChecked = node.enableIndicate

            val charaProp: Int = node.characteristicIntProperties
            helper.setGone(R.id.btnReadData, charaProp and BluetoothGattCharacteristic.PROPERTY_READ <= 0)
            helper.setGone(R.id.cbWrite, charaProp and BluetoothGattCharacteristic.PROPERTY_WRITE <= 0 &&
                    charaProp and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE <= 0)
            helper.setGone(R.id.cbNotify, charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY <= 0)
            helper.setGone(R.id.cbIndicate, charaProp and BluetoothGattCharacteristic.PROPERTY_INDICATE <= 0)

            cbWrite.setOnClickListener { buttonView ->
                if (ViewUtil.isInvalidClick(buttonView)) {
                    return@setOnClickListener
                }
                node.enableWrite = cbWrite.isChecked
                val isChecked = cbWrite.isChecked
                operateCallback?.invoke(buttonView as CheckBox, OperateType.Write, isChecked, node)
            }
            btnReadData.setOnClickListener {
                if (ViewUtil.isInvalidClick(it)) {
                    return@setOnClickListener
                }
                operateCallback?.invoke(null, OperateType.Read, false, node)
            }
            cbNotify.setOnClickListener { buttonView ->
                if (ViewUtil.isInvalidClick(buttonView)) {
                    return@setOnClickListener
                }
                node.enableNotify = cbNotify.isChecked
                val isChecked = cbNotify.isChecked
                operateCallback?.invoke(buttonView as CheckBox, OperateType.Notify, isChecked, node)
            }
            cbIndicate.setOnClickListener { buttonView ->
                if (ViewUtil.isInvalidClick(buttonView)) {
                    return@setOnClickListener
                }
                node.enableIndicate = cbIndicate.isChecked
                val isChecked = cbIndicate.isChecked
                operateCallback?.invoke(buttonView as CheckBox, OperateType.Indicate, isChecked, node)
            }
        }
    }
}

================================================
FILE: app/src/main/java/com/bhm/demo/adapter/DeviceListAdapter.kt
================================================
package com.bhm.demo.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import com.bhm.ble.BleManager
import com.bhm.ble.device.BleDevice
import com.bhm.demo.R
import com.bhm.demo.databinding.LayoutRecyclerItemBinding
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder


/**
 * 设备列表
 *
 * @author Buhuiming
 * @date 2023年05月18日 11时06分
 */
class DeviceListAdapter(data: MutableList<BleDevice>?
) : BaseQuickAdapter<BleDevice, DeviceListAdapter.VH>(0, data) {

    class VH(
        parent: ViewGroup,
        val binding: LayoutRecyclerItemBinding = LayoutRecyclerItemBinding.inflate(
            LayoutInflater.from(parent.context), parent, false
        ),
    ) : BaseViewHolder(binding.root)

    override fun onCreateDefViewHolder(parent: ViewGroup, viewType: Int): VH {
        return VH(parent)
    }

    override fun convert(holder: VH, item: BleDevice) {
        holder.binding.tvName.text = buildString {
            append(item.deviceName)
            append(", ")
            append(item.deviceAddress)
        }
//        holder.binding.btnRssi.text = "${item.rssi ?: 0}"
        val rssi = item.rssi ?: 0
        when {
            rssi >= -65 -> {
                holder.binding.ivRssi.setImageResource(R.drawable.adddevice_device_signal_four_icon)
            }
            rssi >= -75 -> {
                holder.binding.ivRssi.setImageResource(R.drawable.adddevice_device_signal_three_icon)
            }
            rssi >= -85 -> {
                holder.binding.ivRssi.setImageResource(R.drawable.adddevice_device_signal_two_icon)
            }
            else -> {
                holder.binding.ivRssi.setImageResource(R.drawable.adddevice_device_signal_one_icon)
            }
        }
        if (BleManager.get().isConnected(item)) {
            holder.binding.btnConnect.text = "断开"
            holder.binding.btnOperate.isEnabled = true
            holder.binding.btnConnect.setBackgroundColor(
                ContextCompat
                .getColor(holder.binding.btnConnect.context, R.color.red))
        } else {
            holder.binding.btnConnect.text = "连接"
            holder.binding.btnOperate.isEnabled = false
            holder.binding.btnConnect.setBackgroundColor(ContextCompat
                .getColor(holder.binding.btnConnect.context, R.color.black))
        }
    }
}

================================================
FILE: app/src/main/java/com/bhm/demo/adapter/LoggerListAdapter.kt
================================================
package com.bhm.demo.adapter

import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import com.bhm.demo.databinding.LayoutRecyclerLogBinding
import com.bhm.demo.entity.LogEntity
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import java.text.SimpleDateFormat
import java.util.*
import java.util.logging.Level


/**
 * 日志输出列表
 *
 * @author Buhuiming
 * @date 2023年6月1日 15时51分
 */
class LoggerListAdapter(data: MutableList<LogEntity>?
) : BaseQuickAdapter<LogEntity, LoggerListAdapter.VH>(0, data) {

    class VH(
        parent: ViewGroup,
        val binding: LayoutRecyclerLogBinding = LayoutRecyclerLogBinding.inflate(
            LayoutInflater.from(parent.context), parent, false
        ),
    ) : BaseViewHolder(binding.root)

    override fun onCreateDefViewHolder(parent: ViewGroup, viewType: Int): VH {
        return VH(parent)
    }

    override fun convert(holder: VH, item: LogEntity) {
        val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CANADA)
        holder.binding.tvTime.text = buildString {
            append(df.format(System.currentTimeMillis()))
            append(": ")
        }
        holder.binding.tvName.text = item.msg
        when (item.level) {
            Level.INFO -> holder.binding.tvName.setTextColor(Color.BLUE)
            Level.WARNING -> holder.binding.tvName.setTextColor(Color.parseColor("#FF9800"))
            Level.OFF -> holder.binding.tvName.setTextColor(Color.RED)
            Level.FINE -> holder.binding.tvName.setTextColor(Color.BLACK)
            else -> holder.binding.tvName.setTextColor(Color.GRAY)
        }
    }
}

================================================
FILE: app/src/main/java/com/bhm/demo/constants/Constants.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo.constants

import android.Manifest
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES


/**
 * 常量
 *
 * @author Buhuiming
 * @date 2023年05月19日 13时37分
 */
val LOCATION_PERMISSION = if (VERSION.SDK_INT < VERSION_CODES.S) {
    arrayOf(
        //注册精准位置权限,否则可能Ble扫描不到设备
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION,
    )
} else {
    arrayOf(
        Manifest.permission.BLUETOOTH_SCAN,
        Manifest.permission.BLUETOOTH_CONNECT,
    )
}

================================================
FILE: app/src/main/java/com/bhm/demo/entity/CharacteristicNode.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo.entity

import com.chad.library.adapter.base.entity.node.BaseExpandNode
import com.chad.library.adapter.base.entity.node.BaseNode


/**
 * 特征值数据
 * 不能使用data class, 属性写是var,否则BaseNodeAdapter会因为折叠刷新数据闪退
 * @author Buhuiming
 * @date 2023年06月01日 10时16分
 */
class CharacteristicNode(var characteristicName: String,
                         var serviceUUID: String,
                         var characteristicUUID: String,
                         var characteristicProperties: String,
                         var characteristicIntProperties: Int,
                         var enableNotify: Boolean = false,
                         var enableIndicate: Boolean = false,
                         var enableWrite: Boolean = false,
)  : BaseExpandNode() {

    init {
        isExpanded = false
    }

    override val childNode: MutableList<BaseNode>?
        get() = null
}

================================================
FILE: app/src/main/java/com/bhm/demo/entity/LogEntity.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo.entity

import java.util.logging.Level


/**
 * 日志信息
 *
 * @author Buhuiming
 * @date 2023年06月01日 15时54分
 */
data class LogEntity(
    val level: Level,
    val msg: String,
    val time: Long = System.currentTimeMillis()
)

================================================
FILE: app/src/main/java/com/bhm/demo/entity/OperateType.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo.entity


/**
 * 操作类型
 *
 * @author Buhuiming
 * @date 2023年06月01日 14时11分
 */
sealed class OperateType {

    /**
     * 写操作
     */
    object Write : OperateType()

    /**
     * 读操作
     */
    object Read : OperateType()

    /**
     * Notify操作
     */
    object Notify : OperateType()

    /**
     * Indicate操作
     */
    object Indicate : OperateType()
}

================================================
FILE: app/src/main/java/com/bhm/demo/entity/RefreshBleDevice.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo.entity

import com.bhm.ble.device.BleDevice


/**
 * @author Buhuiming
 * @date 2023年05月30日 11时57分
 */
data class RefreshBleDevice(
    val bleDevice: BleDevice?,
    var tag: Long? = null
)

================================================
FILE: app/src/main/java/com/bhm/demo/entity/ServiceNode.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo.entity

import com.chad.library.adapter.base.entity.node.BaseExpandNode
import com.chad.library.adapter.base.entity.node.BaseNode


/**
 * 服务数据
 * 不能使用data class, 属性写是var,否则BaseNodeAdapter会因为折叠刷新数据闪退
 * @author Buhuiming
 * @date 2023年06月01日 10时11分
 */
class ServiceNode(var serviceName: String,
                  var serviceUUID: String,
                  var characteristicList: MutableList<BaseNode>?
) : BaseExpandNode() {

    init {
        isExpanded = false
    }

    override val childNode: MutableList<BaseNode>?
        get() = characteristicList
}

================================================
FILE: app/src/main/java/com/bhm/demo/ui/DetailOperateActivity.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo.ui

import android.annotation.SuppressLint
import android.bluetooth.BluetoothGatt
import android.content.Intent
import android.os.Build
import android.view.KeyEvent
import android.view.View
import android.widget.CheckBox
import android.widget.Toast
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.bhm.ble.BleManager
import com.bhm.ble.device.BleDevice
import com.bhm.demo.BaseActivity
import com.bhm.demo.R
import com.bhm.demo.adapter.DetailsExpandAdapter
import com.bhm.demo.adapter.LoggerListAdapter
import com.bhm.demo.databinding.ActivityDetailBinding
import com.bhm.demo.entity.CharacteristicNode
import com.bhm.demo.entity.OperateType
import com.bhm.demo.vm.DetailViewModel
import com.bhm.support.sdk.core.AppTheme
import com.bhm.support.sdk.entity.MessageEvent
import com.bhm.support.sdk.utils.ViewUtil
import kotlinx.coroutines.launch


/**
 * 服务,特征 操作页面
 *
 * @author Buhuiming
 * @date 2023年06月01日 09时17分
 */
class DetailOperateActivity : BaseActivity<DetailViewModel, ActivityDetailBinding>() {

    override fun createViewModel() = DetailViewModel(application)

    private var bleDevice: BleDevice? = null

    private var expandAdapter: DetailsExpandAdapter? = null

    private var loggerListAdapter: LoggerListAdapter? = null

    private var disConnectWhileClose = false // 关闭页面后是否断开连接

    private var currentSendNode: CharacteristicNode? = null

    private var connectionPriority = BluetoothGatt.CONNECTION_PRIORITY_BALANCED

    private var operateCallback: ((checkBox: CheckBox?,
                                   operateType: OperateType,
                                   isChecked: Boolean,
                                   node: CharacteristicNode) -> Unit)? = null

    override fun initData() {
        super.initData()
        val controller = WindowCompat.getInsetsController(
            window,
            window.decorView
        )
        controller.isAppearanceLightStatusBars = true
        controller.isAppearanceLightNavigationBars = true
        bleDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            intent.getParcelableExtra("data", BleDevice::class.java)
        } else {
            @Suppress("DEPRECATION")
            intent.getParcelableExtra("data")
        }
        disConnectWhileClose = intent.getBooleanExtra("disConnectWhileClose", false)

        if (bleDevice == null) {
            finish()
            return
        }
        viewBinding.tvName.text = buildString {
            append("设备广播名:")
            append(getBleDevice().deviceName)
            append("\r\n")
            append("地址:${getBleDevice().deviceAddress}")
        }
        initList()
    }

    private fun getBleDevice(): BleDevice {
        return bleDevice!!
    }

    private fun initList() {
        val layoutManager = LinearLayoutManager(applicationContext)
        layoutManager.orientation = LinearLayoutManager.VERTICAL
        viewBinding.recyclerView.layoutManager = layoutManager
        viewBinding.recyclerView.addItemDecoration(DividerItemDecoration(applicationContext, DividerItemDecoration.VERTICAL))
        operateCallback = { checkBox, operateType, isChecked, node ->
            when (operateType) {
                is OperateType.Write -> {
                    if (isChecked) {
                        if (viewBinding.btnSend.isEnabled) {
                            checkBox?.isChecked = false
                            Toast.makeText(applicationContext, "请取消其他特征值写操作", Toast.LENGTH_SHORT).show()
                        } else {
                            viewBinding.btnSend.isEnabled = true
                            viewBinding.etContent.isEnabled = true
                            currentSendNode = node
                        }
                    } else {
                        viewBinding.btnSend.isEnabled = false
                        viewBinding.etContent.isEnabled = false
                        currentSendNode = null
                    }
                }
                is OperateType.Read -> {
                    viewModel.readData(getBleDevice(), node)
                }
                is OperateType.Notify -> {
                    if (isChecked) {
                        viewModel.notify(getBleDevice(), node)
                    } else {
                        viewModel.stopNotify(getBleDevice(), node)
                    }
                }
                is OperateType.Indicate -> {
                    if (isChecked) {
                        viewModel.indicate(getBleDevice(), node)
                    } else {
                        viewModel.stopIndicate(getBleDevice(), node)
                    }
                }
            }
        }
        expandAdapter = DetailsExpandAdapter(viewModel.getListData(getBleDevice()), operateCallback)
        viewBinding.recyclerView.adapter = expandAdapter
        if ((expandAdapter?.data?.size?: 0) > 0) {
            expandAdapter?.expand(0)
        }

        val logLayoutManager = LinearLayoutManager(applicationContext)
        logLayoutManager.orientation = LinearLayoutManager.VERTICAL
        viewBinding.logRecyclerView.setHasFixedSize(true)
        viewBinding.logRecyclerView.layoutManager = logLayoutManager
        (viewBinding.recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
        loggerListAdapter = LoggerListAdapter(viewModel.listLogData)
        viewBinding.logRecyclerView.adapter = loggerListAdapter

    }

    @SuppressLint("NotifyDataSetChanged")
    override fun initEvent() {
        super.initEvent()
        lifecycleScope.launch {
            viewModel.listLogStateFlow.collect {
                viewModel.listLogData.add(it)
                val position = viewModel.listLogData.size - 1
                loggerListAdapter?.notifyItemInserted(position)
                viewBinding.logRecyclerView.smoothScrollToPosition(position)
            }
        }
        lifecycleScope.launch {
            viewModel.listRefreshStateFlow.collect {
                if (it.isNotEmpty()) {
                    expandAdapter?.notifyDataSetChanged()
                }
            }
        }

        viewBinding.btnConnectionPriority.setOnClickListener {
            if (ViewUtil.isInvalidClick(it)) {
                return@setOnClickListener
            }
            when (connectionPriority) {
                BluetoothGatt.CONNECTION_PRIORITY_BALANCED -> connectionPriority =
                    BluetoothGatt.CONNECTION_PRIORITY_HIGH
                BluetoothGatt.CONNECTION_PRIORITY_HIGH -> connectionPriority =
                    BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER
                BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER -> connectionPriority =
                    BluetoothGatt.CONNECTION_PRIORITY_BALANCED
            }
            viewModel.setConnectionPriority(getBleDevice(), connectionPriority)
        }

        viewBinding.btnClear.setOnClickListener {
            if (ViewUtil.isInvalidClick(it)) {
                return@setOnClickListener
            }
            loggerListAdapter?.notifyItemRangeRemoved(0, viewModel.listLogData.size)
            viewModel.listLogData.clear()
        }

        viewBinding.btnSetMtu.setOnClickListener {
            if (ViewUtil.isInvalidClick(it)) {
                return@setOnClickListener
            }
            viewModel.setMtu(getBleDevice())
        }

        viewBinding.btnReadRssi.setOnClickListener {
            if (ViewUtil.isInvalidClick(it)) {
                return@setOnClickListener
            }
            viewModel.readRssi(getBleDevice())
        }

        viewBinding.btnSend.setOnClickListener {
            if (ViewUtil.isInvalidClick(it)) {
                return@setOnClickListener
            }
            val content = viewBinding.etContent.text.toString()
            if (content.isEmpty()) {
                Toast.makeText(applicationContext, "请输入数据", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            if (currentSendNode == null) {
                Toast.makeText(applicationContext, "请选择特征值写操作", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            currentSendNode?.let { node ->
                viewModel.writeData(
                    getBleDevice(),
                    node,
                    content
                )
            }
        }
    }

    public fun showContent(view: View) {
        if (viewBinding.llContent.visibility == View.VISIBLE) {
            viewBinding.llContent.visibility = View.GONE
        } else {
            viewBinding.llContent.visibility = View.VISIBLE
        }
    }

    /**
     * 接收到断开通知
     */
    override fun onMessageEvent(event: MessageEvent?) {
        super.onMessageEvent(event)
        event?.let {
            val device = it.data as BleDevice
            if (getBleDevice() == device) {
                BleManager.get().close(getBleDevice())
                setResult(0, null)
                finish()
            }
        }
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            if (disConnectWhileClose) {
                BleManager.get().close(getBleDevice())
                setResult(0, Intent())
            } else {
                BleManager.get().removeAllCharacterCallback(getBleDevice())
                setResult(0, null)
            }
            finish()
            return true
        }
        return super.onKeyDown(keyCode, event)
    }

    override fun onDestroy() {
        super.onDestroy()
        expandAdapter = null
        operateCallback = null
    }
}

================================================
FILE: app/src/main/java/com/bhm/demo/ui/MainActivity.kt
================================================
package com.bhm.demo.ui

import android.annotation.SuppressLint
import android.content.Intent
import android.view.View
import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.bhm.ble.device.BleDevice
import com.bhm.ble.log.BleLogger
import com.bhm.demo.BaseActivity
import com.bhm.demo.R
import com.bhm.demo.adapter.DeviceListAdapter
import com.bhm.demo.constants.LOCATION_PERMISSION
import com.bhm.demo.databinding.ActivityMainBinding
import com.bhm.demo.vm.MainViewModel
import com.bhm.support.sdk.utils.ViewUtil
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
 * 主页面
 * @author Buhuiming
 * @date :2023/5/24 15:39
 */
class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>() {

    private var listAdapter: DeviceListAdapter? = null

    private var autoOpenDetailsActivity = false

    override fun createViewModel() = MainViewModel(application)

    override fun initData() {
        super.initData()
        WindowCompat.setDecorFitsSystemWindows(window, false)
        val controller = WindowCompat.getInsetsController(
            window,
            window.decorView
        )
        controller.isAppearanceLightStatusBars = false
        controller.isAppearanceLightNavigationBars = false
        ViewCompat.setOnApplyWindowInsetsListener(viewBinding.vTop) { _: View, insets: WindowInsetsCompat ->
            val statusBars = insets.getInsets(WindowInsetsCompat.Type.statusBars())
            val navBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
            viewBinding.vTop.layoutParams.height = statusBars.top
            rootView.setPadding(0, 0, 0, navBars.bottom)
            insets
        }
        ViewCompat.requestApplyInsets(viewBinding.vTop)
        initList()
        viewModel.initBle()
    }

    @SuppressLint("NotifyDataSetChanged")
    override fun initEvent() {
        super.initEvent()
        lifecycleScope.launch {
            //添加扫描到的设备 刷新列表
            viewModel.listDRStateFlow.collect {
                if (it.deviceName != null && it.deviceAddress != null) {
                    val position = (listAdapter?.itemCount?: 1) - 1
                    listAdapter?.notifyItemInserted(position)
                    viewBinding.recyclerView.smoothScrollToPosition(position)
                }
            }
        }

        lifecycleScope.launch {
            viewModel.scanStopStateFlow.collect {
                viewBinding.pbLoading.visibility = if (it) { View.INVISIBLE } else { View.VISIBLE }
                viewBinding.btnStart.text = if (it) { "开启扫描" } else { "扫描中..." }
                viewBinding.btnStart.isEnabled = it
                viewBinding.btnConnect.isEnabled = it
                viewBinding.btnSetting.isEnabled = it
                viewBinding.btnStop.isEnabled = !it
            }
        }

        lifecycleScope.launch {
            //连接设备后 刷新列表
            viewModel.refreshStateFlow.collect {
                delay(300)
                dismissLoading()
                if (it?.bleDevice == null) {
                    listAdapter?.notifyDataSetChanged()
                    return@collect
                }
                it.bleDevice.let { bleDevice ->
                    val position = listAdapter?.data?.indexOf(bleDevice) ?: -1
                    if (position >= 0) {
                        listAdapter?.notifyItemChanged(position)
                    }
                    val isConnected= viewModel.isConnected(bleDevice)
                    if (it.bleDevice.deviceAddress == viewBinding.etAddress.text.toString()) {
                        viewBinding.btnConnect.isEnabled = !isConnected
                    }
                    if (isConnected && autoOpenDetailsActivity) {
                        openDetails(it.bleDevice)
                    }
                    autoOpenDetailsActivity = false
                }
            }
        }

        listAdapter?.addChildClickViewIds(R.id.btnConnect, R.id.btnOperate)
        listAdapter?.setOnItemChildClickListener { adapter, view, position ->
            if (ViewUtil.isInvalidClick(view)) {
                return@setOnItemChildClickListener
            }
            val bleDevice: BleDevice? = adapter.data[position] as BleDevice?
            if (view.id == R.id.btnConnect) {
                if (viewModel.isConnected(bleDevice)) {
                    showLoading("断开中...")
                    viewModel.disConnect(bleDevice)
                } else {
                    showLoading("连接中...")
                    viewModel.connect(bleDevice)
                }
            } else if (view.id == R.id.btnOperate) {
                openDetails(bleDevice)
            }
        }

        viewBinding.btnConnect.setOnClickListener {
            if (ViewUtil.isInvalidClick(it)) {
                return@setOnClickListener
            }
            requestPermission(
                LOCATION_PERMISSION,
                {
                    BleLogger.d("获取到了权限")
                    val address = viewBinding.etAddress.text.toString()
                    if (address.isEmpty()) {
                        Toast.makeText(application, "请输入设备地址", Toast.LENGTH_SHORT).show()
                        return@requestPermission
                    }
                    if (!Regex("^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$").matches(address)) {
                        Toast.makeText(application, "请输入正确的设备地址", Toast.LENGTH_SHORT).show()
                        return@requestPermission
                    }
                    autoOpenDetailsActivity = true
                    showLoading("连接中...")
//            viewModel.startScanAndConnect(this@MainActivity)
                    viewModel.connect(viewBinding.etAddress.text.toString())

                }, {
                    BleLogger.w("缺少定位权限")
                }
            )
        }

        viewBinding.btnSetting.setOnClickListener {
            if (ViewUtil.isInvalidClick(it)) {
                return@setOnClickListener
            }
            startActivity(Intent(this@MainActivity, OptionSettingActivity::class.java))
        }

        viewBinding.btnStart.setOnClickListener {
            if (ViewUtil.isInvalidClick(it)) {
                return@setOnClickListener
            }
            listAdapter?.notifyItemRangeRemoved(0, viewModel.listDRData.size)
            viewModel.listDRData.clear()
            viewModel.startScan(this@MainActivity)
        }

        viewBinding.btnStop.setOnClickListener {
            if (ViewUtil.isInvalidClick(it)) {
                return@setOnClickListener
            }
            viewModel.stopScan()
        }
    }

    private fun initList() {
        val layoutManager = LinearLayoutManager(this)
        layoutManager.orientation = LinearLayoutManager.VERTICAL
        viewBinding.recyclerView.setHasFixedSize(true)
        viewBinding.recyclerView.layoutManager = layoutManager
        viewBinding.recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
        //解决RecyclerView局部刷新时闪烁
        (viewBinding.recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
        listAdapter = DeviceListAdapter(viewModel.listDRData)
        viewBinding.recyclerView.adapter = listAdapter
    }

    /**
     * 打开操作页面
     */
    private fun openDetails(bleDevice: BleDevice?) {
        if (viewModel.isConnected(bleDevice)) {
            val intent = Intent(this@MainActivity, DetailOperateActivity::class.java)
            intent.putExtra("data", bleDevice)
            intent.putExtra("disConnectWhileClose", autoOpenDetailsActivity)
            startActivity(intent) { _, resultIntent ->
                if (resultIntent != null) {
                    showLoading("断开中...")
                    //断开需要一定的时间,才可以连接,这里防止没断开完成,马上点击连接
                    lifecycleScope.launch {
                        delay(1200)
                        dismissLoading()
                    }
                }
            }
        } else {
            Toast.makeText(application, "设备未连接", Toast.LENGTH_SHORT).show()
            val index = listAdapter?.data?.indexOf(bleDevice) ?: -1
            if (index >= 0) {
                listAdapter?.notifyItemChanged(index)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        viewModel.stopScan()
        viewModel.close()
    }
}

================================================
FILE: app/src/main/java/com/bhm/demo/ui/OptionSettingActivity.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo.ui

import android.widget.ArrayAdapter
import androidx.core.view.WindowCompat
import com.bhm.ble.BleManager
import com.bhm.ble.attribute.BleOptions
import com.bhm.ble.data.BleTaskQueueType
import com.bhm.ble.data.Constants.AUTO_CONNECT
import com.bhm.ble.data.Constants.CONTAIN_SCAN_DEVICE_NAME
import com.bhm.ble.data.Constants.DEFAULT_AUTO_SET_MTU
import com.bhm.ble.data.Constants.DEFAULT_CONNECT_MILLIS_TIMEOUT
import com.bhm.ble.data.Constants.DEFAULT_CONNECT_RETRY_COUNT
import com.bhm.ble.data.Constants.DEFAULT_CONNECT_RETRY_INTERVAL
import com.bhm.ble.data.Constants.DEFAULT_MAX_CONNECT_NUM
import com.bhm.ble.data.Constants.DEFAULT_MTU
import com.bhm.ble.data.Constants.DEFAULT_OPERATE_INTERVAL
import com.bhm.ble.data.Constants.DEFAULT_OPERATE_MILLIS_TIMEOUT
import com.bhm.ble.data.Constants.DEFAULT_SCAN_MILLIS_TIMEOUT
import com.bhm.ble.data.Constants.DEFAULT_SCAN_RETRY_COUNT
import com.bhm.ble.data.Constants.DEFAULT_SCAN_RETRY_INTERVAL
import com.bhm.ble.data.Constants.ENABLE_LOG
import com.bhm.ble.data.Constants.SCAN_NEED_CHECK_GPS
import com.bhm.demo.BaseActivity
import com.bhm.demo.R
import com.bhm.demo.databinding.ActivitySettingBinding
import com.bhm.support.sdk.common.BaseViewModel
import com.bhm.support.sdk.core.AppTheme
import com.bhm.support.sdk.utils.ViewUtil


/**
 * 设置扫描配置项页面
 *
 * @author Buhuiming
 * @date 2023年05月24日 15时38分
 */
class OptionSettingActivity : BaseActivity<BaseViewModel, ActivitySettingBinding>() {

    override fun createViewModel() = BaseViewModel (application)

    override fun initData() {
        super.initData()
        val controller = WindowCompat.getInsetsController(
            window,
            window.decorView
        )
        controller.isAppearanceLightStatusBars = true
        controller.isAppearanceLightNavigationBars = true
        val taskQueueTypes = arrayOf("Default", "Operate", "Independent")
        viewBinding.spTaskQueueType.adapter =
            ArrayAdapter(this, android.R.layout.simple_spinner_item, taskQueueTypes)
        viewBinding.spTaskQueueType.setSelection(0)
        val options = BleManager.get().getOptions()
        options?.let {
            val etScanServiceUuidText = StringBuilder()
            options.scanServiceUuids.forEach { string ->
                if (string.isNotEmpty()) {
                    etScanServiceUuidText.append(string)
                    etScanServiceUuidText.append(",")
                }
            }

            if (etScanServiceUuidText.isNotEmpty()) {
                etScanServiceUuidText.delete(etScanServiceUuidText.length - 1,
                    etScanServiceUuidText.length)
            }
            val etScanDeviceNameText = StringBuilder()
            options.scanDeviceNames.forEach { string ->
                if (string.isNotEmpty()) {
                    etScanDeviceNameText.append(string)
                    etScanDeviceNameText.append(",")
                }
            }
            if (etScanDeviceNameText.isNotEmpty()) {
                etScanDeviceNameText.delete(etScanDeviceNameText.length - 1,
                    etScanDeviceNameText.length)
            }
            val etScanDeviceAddressText = StringBuilder()
            options.scanDeviceAddresses.forEach { string ->
                if (string.isNotEmpty()) {
                    etScanDeviceAddressText.append(string)
                    etScanDeviceAddressText.append(",")
                }
            }
            if (etScanDeviceAddressText.isNotEmpty()) {
                etScanDeviceAddressText.delete(etScanDeviceAddressText.length - 1,
                    etScanDeviceAddressText.length)
            }
            viewBinding.etScanServiceUuid.setText(etScanServiceUuidText.toString())
            viewBinding.etScanDeviceName.setText(etScanDeviceNameText.toString())
            viewBinding.etScanDeviceAddress.setText(etScanDeviceAddressText.toString())
            viewBinding.etScanOutTime.setText(it.scanMillisTimeOut.toString())
            viewBinding.etScanRetryCount.setText(it.scanRetryCount.toString())
            viewBinding.etScanRetryInterval.setText(it.scanRetryInterval.toString())
            viewBinding.etConnectOutTime.setText(it.connectMillisTimeOut.toString())
            viewBinding.etConnectRetryCount.setText(it.connectRetryCount.toString())
            viewBinding.etConnectRetryInterval.setText(it.connectRetryInterval.toString())
            viewBinding.etOperateMillisTimeOut.setText(it.operateMillisTimeOut.toString())
            viewBinding.etOperateInterval.setText(it.operateInterval.toString())
            viewBinding.etMaxConnectNum.setText(it.maxConnectNum.toString())
            viewBinding.etMTU.setText(it.mtu.toString())
            viewBinding.cbContainScanDeviceName.isChecked = it.containScanDeviceName
            viewBinding.cbLogger.isChecked = it.enableLog
            viewBinding.cbNeedCheckGPS.isChecked = it.needCheckGps
            viewBinding.cbMtu.isChecked = it.autoSetMtu
            viewBinding.cbAutoConnect.isChecked = it.autoConnect
            viewBinding.spTaskQueueType.setSelection(getTaskQueueType(it.taskQueueType))
        }
    }

    override fun initEvent() {
        super.initEvent()
        viewBinding.btnReSet.setOnClickListener {
            if (ViewUtil.isInvalidClick(it)) {
                return@setOnClickListener
            }
            viewBinding.etScanServiceUuid.setText("")
            viewBinding.etScanDeviceName.setText("")
            viewBinding.etScanDeviceAddress.setText("")
            viewBinding.etScanOutTime.setText(DEFAULT_SCAN_MILLIS_TIMEOUT.toString())
            viewBinding.etScanRetryCount.setText(DEFAULT_SCAN_RETRY_COUNT.toString())
            viewBinding.etScanRetryInterval.setText(DEFAULT_SCAN_RETRY_INTERVAL.toString())
            viewBinding.etConnectOutTime.setText(DEFAULT_CONNECT_MILLIS_TIMEOUT.toString())
            viewBinding.etConnectRetryCount.setText(DEFAULT_CONNECT_RETRY_COUNT.toString())
            viewBinding.etConnectRetryInterval.setText(DEFAULT_CONNECT_RETRY_INTERVAL.toString())
            viewBinding.etOperateMillisTimeOut.setText(DEFAULT_OPERATE_MILLIS_TIMEOUT.toString())
            viewBinding.etOperateInterval.setText(DEFAULT_OPERATE_INTERVAL.toString())
            viewBinding.etMaxConnectNum.setText(DEFAULT_MAX_CONNECT_NUM.toString())
            viewBinding.etMTU.setText(DEFAULT_MTU.toString())
            viewBinding.cbContainScanDeviceName.isChecked = CONTAIN_SCAN_DEVICE_NAME
            viewBinding.cbLogger.isChecked = ENABLE_LOG
            viewBinding.cbNeedCheckGPS.isChecked = SCAN_NEED_CHECK_GPS
            viewBinding.cbMtu.isChecked = DEFAULT_AUTO_SET_MTU
            viewBinding.cbAutoConnect.isChecked = AUTO_CONNECT
            viewBinding.spTaskQueueType.setSelection(0)
            BleManager.get().init(application)
        }
        viewBinding.btnSave.setOnClickListener { view ->
            if (ViewUtil.isInvalidClick(view)) {
                return@setOnClickListener
            }
            if (viewBinding.etMaxConnectNum.text.toString().toInt() > 7) {
                viewBinding.etMaxConnectNum.setText("7")
            }

            val builder = BleOptions.builder()
            val scanServiceUuids = viewBinding.etScanServiceUuid.text.toString().split(",")
            scanServiceUuids.forEach {
                builder.setScanServiceUuid(it)
            }
            val scanDeviceNames = viewBinding.etScanDeviceName.text.toString().split(",")
            scanDeviceNames.forEach {
                builder.setScanDeviceName(it)
            }
            val scanDeviceAddresses = viewBinding.etScanDeviceAddress.text.toString().split(",")
            scanDeviceAddresses.forEach {
                builder.setScanDeviceAddress(it)
            }

            builder
                .isContainScanDeviceName(viewBinding.cbContainScanDeviceName.isChecked)
                .setEnableLog(viewBinding.cbLogger.isChecked)
                .setScanMillisTimeOut(viewBinding.etScanOutTime.text.toString().toLong())
//                //这个机制是:不会因为扫描的次数导致上一次扫描到的数据被清空,也就是onScanStart和onScanComplete
//                //都只会回调一次,而且扫描到的数据是所有扫描次数的总和
                .setScanRetryCountAndInterval(viewBinding.etScanRetryCount.text.toString().toInt(),
                    viewBinding.etScanRetryInterval.text.toString().toLong())
                .setConnectMillisTimeOut(viewBinding.etConnectOutTime.text.toString().toLong())
                .setConnectRetryCountAndInterval(viewBinding.etConnectRetryCount.text.toString().toInt(),
                    viewBinding.etConnectRetryInterval.text.toString().toLong())
                .setAutoConnect(viewBinding.cbAutoConnect.isChecked)
                .setOperateMillisTimeOut(viewBinding.etOperateMillisTimeOut.text.toString().toLong())
                .setOperateInterval(viewBinding.etOperateInterval.text.toString().toLong())
                .setMaxConnectNum(viewBinding.etMaxConnectNum.text.toString().toInt())
                .setMtu(viewBinding.etMTU.text.toString().toInt(), viewBinding.cbMtu.isChecked)
                .setTaskQueueType(getTaskQueueType(viewBinding.spTaskQueueType.selectedItemPosition))
                .setNeedCheckGps(viewBinding.cbNeedCheckGPS.isChecked)
            BleManager.get().init(application, builder.build())
            BleManager.get().disConnectAll()
            finish()
        }
    }

    private fun getTaskQueueType(taskQueueType: BleTaskQueueType): Int {
        return when (taskQueueType) {
            BleTaskQueueType.Default -> 0
            BleTaskQueueType.Operate -> 1
            BleTaskQueueType.Independent -> 2
        }
    }

    private fun getTaskQueueType(taskQueueType: Int): BleTaskQueueType {
        return when (taskQueueType) {
            1 -> BleTaskQueueType.Operate
            2 -> BleTaskQueueType.Independent
            else -> BleTaskQueueType.Default
        }
    }
}

================================================
FILE: app/src/main/java/com/bhm/demo/vm/DetailViewModel.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.demo.vm

import android.app.Application
import android.bluetooth.BluetoothGattCharacteristic
import android.util.SparseArray
import com.bhm.ble.BleManager
import com.bhm.ble.data.BleDescriptorGetType
import com.bhm.ble.data.Constants.DEFAULT_MTU
import com.bhm.ble.device.BleDevice
import com.bhm.ble.log.BleLogger
import com.bhm.ble.utils.BleUtil
import com.bhm.demo.entity.CharacteristicNode
import com.bhm.demo.entity.LogEntity
import com.bhm.demo.entity.ServiceNode
import com.bhm.support.sdk.common.BaseViewModel
import com.chad.library.adapter.base.entity.node.BaseNode
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.logging.Level


/**
 *
 * @author Buhuiming
 * @date 2023年06月01日 09时18分
 */
class DetailViewModel(application: Application) : BaseViewModel(application) {

    private val listLogMutableStateFlow = MutableStateFlow(LogEntity(Level.INFO, "数据适配完毕"))

    val listLogStateFlow: StateFlow<LogEntity> = listLogMutableStateFlow

    private val listRefreshMutableStateFlow = MutableStateFlow("")

    val listRefreshStateFlow: StateFlow<String> = listRefreshMutableStateFlow

    val listLogData = mutableListOf<LogEntity>()

    /**
     * 根据bleDevice拿到服务特征值数据
     */
    fun getListData(bleDevice: BleDevice): MutableList<BaseNode> {
        val gatt = BleManager.get().getBluetoothGatt(bleDevice)
        val list: MutableList<BaseNode> = arrayListOf()
        gatt?.services?.forEachIndexed { index, service ->
            val childList: MutableList<BaseNode> = arrayListOf()
            service.characteristics?.forEachIndexed { position, characteristics ->
                val characteristicNode = CharacteristicNode(
                    position.toString(),
                    service.uuid.toString(),
                    characteristics.uuid.toString(),
                    getOperateType(characteristics),
                    characteristics.properties,
                    enableNotify = false,
                    enableIndicate = false,
                    enableWrite = false
                )
                childList.add(characteristicNode)
            }
            val serviceNode = ServiceNode(
                index.toString(),
                service.uuid.toString(),
                childList
            )
            list.add(serviceNode)
        }
        return list
    }

    /**
     * 获取特征值的属性
     */
    private fun getOperateType(characteristic: BluetoothGattCharacteristic): String {
        val property = StringBuilder()
        val charaProp: Int = characteristic.properties
        if (charaProp and BluetoothGattCharacteristic.PROPERTY_READ != 0) {
            property.append("Read")
            property.append(" , ")
        }
        if (charaProp and BluetoothGattCharacteristic.PROPERTY_WRITE != 0) {
            property.append("Write")
            property.append(" , ")
        }
        if (charaProp and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE != 0) {
            property.append("Write No Response")
            property.append(" , ")
        }
        if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0) {
            property.append("Notify")
            property.append(" , ")
        }
        if (charaProp and BluetoothGattCharacteristic.PROPERTY_INDICATE != 0) {
            property.append("Indicate")
            property.append(" , ")
        }
        if (property.length > 1) {
            property.delete(property.length - 2, property.length - 1)
        }
        return if (property.isNotEmpty()) {
            property.toString()
        } else {
            ""
        }
    }

    /**
     * 添加日志显示
     */
    @Synchronized
    fun addLogMsg(logEntity: LogEntity) {
        listLogMutableStateFlow.value = logEntity
    }

    /**
     * notify
     */
    fun notify(bleDevice: BleDevice,
               node: CharacteristicNode
    ) {
        BleManager.get().notify(bleDevice, node.serviceUUID, node.characteristicUUID, 2000, BleDescriptorGetType.AllDescriptor) {
            onNotifyFail { _, _, t ->
                addLogMsg(LogEntity(Level.OFF, "notify失败:${t.message}"))
                node.enableNotify = false
                listRefreshMutableStateFlow.value = System.currentTimeMillis().toString()
            }
            onNotifySuccess { _, _ ->
                addLogMsg(LogEntity(Level.FINE, "notify成功:${node.characteristicUUID}"))
            }
            onCharacteristicChanged {_, _, data ->
                //数据处理在IO线程,显示UI要切换到主线程
                launchInMainThread {
                    addLogMsg(
                        LogEntity(
                            Level.INFO, "Notify接收到${node.characteristicUUID}的数据:" +
                                    BleUtil.bytesToHex(data)
                        )
                    )
                }
            }
        }
    }

    /**
     * stop notify
     */
    fun stopNotify(
        bleDevice: BleDevice,
        node: CharacteristicNode
    ) {
        val success = BleManager.get().stopNotify(bleDevice, node.serviceUUID, node.characteristicUUID, BleDescriptorGetType.AllDescriptor)
        if (success == true) {
            addLogMsg(LogEntity(Level.FINE, "notify取消成功:${node.characteristicUUID}"))
        } else {
            addLogMsg(LogEntity(Level.OFF, "notify取消失败:${node.characteristicUUID}"))
        }
    }

    /**
     * indicate
     */
    fun indicate(bleDevice: BleDevice,
                 node: CharacteristicNode
    ) {
        BleManager.get().indicate(bleDevice, node.serviceUUID, node.characteristicUUID, BleDescriptorGetType.AllDescriptor) {
            onIndicateFail {_, _, t ->
                addLogMsg(LogEntity(Level.OFF, "indicate失败:${t.message}"))
                node.enableIndicate = false
                listRefreshMutableStateFlow.value = System.currentTimeMillis().toString()
            }
            onIndicateSuccess { _, _, ->
                addLogMsg(LogEntity(Level.FINE, "indicate成功:${node.characteristicUUID}"))
            }
            onCharacteristicChanged {_, _, data ->
                //数据处理在IO线程,显示UI要切换到主线程
                launchInMainThread {
                    addLogMsg(
                        LogEntity(
                            Level.INFO, "Indicate接收到${node.characteristicUUID}的数据:" +
                                    BleUtil.bytesToHex(data)
                        )
                    )
                }
            }
        }
    }

    /**
     * stop indicate
     */
    fun stopIndicate(
        bleDevice: BleDevice,
        node: CharacteristicNode
    ) {
        val success = BleManager.get().stopIndicate(bleDevice, node.serviceUUID, node.characteristicUUID, BleDescriptorGetType.AllDescriptor)
        if (success == true) {
            addLogMsg(LogEntity(Level.FINE, "indicate取消成功:${node.characteristicUUID}"))
        } else {
            addLogMsg(LogEntity(Level.OFF, "indicate取消失败:${node.characteristicUUID}"))
        }
    }

    /**
     * 设置设备的传输优先级
     */
    fun setConnectionPriority(bleDevice: BleDevice, connectionPriority: Int) {
        val success = BleManager.get().setConnectionPriority(bleDevice, connectionPriority)
        if (success) {
            addLogMsg(LogEntity(Level.FINE, "设置设备的传输优先级成功:$connectionPriority"))
        } else {
            addLogMsg(LogEntity(Level.OFF, "设置设备的传输优先级失败:$connectionPriority"))
        }
    }

    /**
     * 读取信号值
     */
    fun readRssi(bleDevice: BleDevice) {
        BleManager.get().readRssi(bleDevice) {
            onRssiFail {_, t ->
                addLogMsg(LogEntity(Level.OFF, "读取信号值失败:${t.message}"))
            }
            onRssiSuccess {_, rssi ->
                addLogMsg(LogEntity(Level.FINE, "${bleDevice.deviceAddress} -> 读取信号值成功:${rssi}"))
            }
        }
    }

    /**
     * 设置mtu
     */
    fun setMtu(bleDevice: BleDevice) {
        BleManager.get().setMtu(bleDevice) {
            onSetMtuFail {_, t ->
                addLogMsg(LogEntity(Level.OFF, "设置mtu值失败:${t.message}"))
            }
            onMtuChanged {_, mtu ->
                addLogMsg(LogEntity(Level.FINE, "${bleDevice.deviceAddress} -> 设置mtu值成功:${mtu}"))
            }
        }
    }

    /**
     * 读特征值数据
     */
    fun readData(bleDevice: BleDevice,
                 node: CharacteristicNode
    ) {
        BleManager.get().readData(bleDevice, node.serviceUUID, node.characteristicUUID) {
            onReadFail {_, t ->
                addLogMsg(LogEntity(Level.OFF, "读特征值数据失败:${t.message}"))
            }
            onReadSuccess {_, data ->
                addLogMsg(LogEntity(Level.FINE, "${node.characteristicUUID} -> 读特征值数据成功:${BleUtil.bytesToHex(data)}"))
            }
        }
    }

    /**
     * 写数据
     * 注意:因为分包后每一个包,可能是包含完整的协议,所以分包由业务层处理,组件只会根据包的长度和mtu值对比后是否拦截
     */
    fun writeData(bleDevice: BleDevice,
                  node: CharacteristicNode,
                  text: String) {

        val data = text.toByteArray()
        BleLogger.i("data is: ${BleUtil.bytesToHex(data)}")
        val mtu = BleManager.get().getOptions()?.mtu?: DEFAULT_MTU
        //mtu长度包含了ATT的opcode一个字节以及ATT的handle2个字节
        val maxLength = mtu - 3
        val listData: SparseArray<ByteArray> = BleUtil.subpackage(data, maxLength)
        BleManager.get()
            .writeData(bleDevice, node.serviceUUID, node.characteristicUUID, listData) {
                onWriteFail { _, currentPackage, _, t ->
                    addLogMsg(
                        LogEntity(
                            Level.OFF,
                            "第${currentPackage}包数据写失败:${t.message}"
                        )
                    )
                }
                onWriteSuccess { _, currentPackage, _, justWrite ->
                    addLogMsg(
                        LogEntity(
                            Level.FINE,
                            "${node.characteristicUUID} -> 第${currentPackage}包数据写成功:" +
                                    BleUtil.bytesToHex(justWrite)
                        )
                    )
                }
                onWriteComplete { _, allSuccess ->
                    //代表所有数据写成功,可以在这个方法中处理成功的逻辑
                    addLogMsg(
                        LogEntity(
                            Level.FINE,
                            "${node.characteristicUUID} -> 写数据完成,是否成功:$allSuccess"
                        )
                    )
                }
            }
    }
}

================================================
FILE: app/src/main/java/com/bhm/demo/vm/MainViewModel.kt
================================================
package com.bhm.demo.vm

import android.app.Application
import android.content.Intent
import android.provider.Settings
import android.widget.Toast
import androidx.lifecycle.viewModelScope
import com.bhm.ble.BleManager
import com.bhm.ble.attribute.BleOptions
import com.bhm.ble.callback.BleConnectCallback
import com.bhm.ble.callback.BleScanCallback
import com.bhm.ble.data.BleConnectFailType
import com.bhm.ble.data.BleScanFailType
import com.bhm.ble.device.BleDevice
import com.bhm.ble.log.BleLogger
import com.bhm.ble.utils.BleUtil
import com.bhm.demo.BaseActivity
import com.bhm.demo.constants.LOCATION_PERMISSION
import com.bhm.demo.entity.RefreshBleDevice
import com.bhm.support.sdk.common.BaseViewModel
import com.bhm.support.sdk.entity.MessageEvent
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine


/**
 * @author Buhuiming
 * @date 2023年05月18日 10时49分
 */
class MainViewModel(private val application: Application) : BaseViewModel(application) {

    private val listDRMutableStateFlow = MutableStateFlow(
        BleDevice(null, null, null, null, null, null, null)
    )

    val listDRStateFlow: StateFlow<BleDevice> = listDRMutableStateFlow

    val listDRData = mutableListOf<BleDevice>()

    private val scanStopMutableStateFlow = MutableStateFlow(true)

    val scanStopStateFlow: StateFlow<Boolean> = scanStopMutableStateFlow

    private val refreshMutableStateFlow = MutableStateFlow(
        RefreshBleDevice(null, null)
    )

    val refreshStateFlow: StateFlow<RefreshBleDevice?> = refreshMutableStateFlow

    /**
     * 初始化蓝牙组件
     */
    fun initBle() {
        BleManager.get().init(application,
            BleOptions.Builder()
                .setScanMillisTimeOut(5000)
                .setConnectMillisTimeOut(5000)
                //一般不推荐autoSetMtu,因为如果设置的等待时间会影响其他操作
//                .setMtu(100, true)
                .setMaxConnectNum(2)
                .setConnectRetryCountAndInterval(2, 1000)
                .setStopScanWhenStartConnect(false)
                .setNeedCheckGps(true)
                .build()
        )
        BleManager.get().registerBluetoothStateReceiver {
            onStateOff {
                refreshMutableStateFlow.value = RefreshBleDevice(null, System.currentTimeMillis())
            }
        }
    }

    /**
     * 检查权限、检查GPS开关、检查蓝牙开关
     */
    private suspend fun hasScanPermission(activity: BaseActivity<*, *>): Boolean {
        val isBleSupport = BleManager.get().isBleSupport()
        BleLogger.e("设备是否支持蓝牙: $isBleSupport")
        if (!isBleSupport) {
            return false
        }
        var hasScanPermission = suspendCoroutine { continuation ->
            activity.requestPermission(
                LOCATION_PERMISSION,
                {
                    BleLogger.d("获取到了权限")
                    try {
                        continuation.resume(true)
                    } catch (e: Exception) {
                        BleLogger.e(e.message)
                    }
                }, {
                    BleLogger.w("缺少定位权限")
                    try {
                        continuation.resume(false)
                    } catch (e: Exception) {
                        BleLogger.e(e.message)
                    }
                }
            )
        }
        //有些设备GPS是关闭状态的话,申请定位权限之后,GPS是依然关闭状态,这里要根据GPS是否打开来跳转页面
        if (hasScanPermission && !BleUtil.isGpsOpen(application)) {
            //跳转到系统GPS设置页面,GPS设置是全局的独立的,是否打开跟权限申请无关
            hasScanPermission = suspendCoroutine {
                activity.startActivity(
                    Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
                ) { _, _ ->
                    val enable = BleUtil.isGpsOpen(application)
                    BleLogger.i("是否打开了GPS: $enable")
                    try {
                        it.resume(enable)
                    } catch (e: Exception) {
                        BleLogger.e(e.message)
                    }
                }
            }
        }
        if (hasScanPermission && !BleManager.get().isBleEnable()) {
            //跳转到系统GPS设置页面,GPS设置是全局的独立的,是否打开跟权限申请无关
            hasScanPermission = suspendCoroutine {
                activity.startActivity(
                    Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
                ) { _, _ ->
                    viewModelScope.launch {
                        //打开蓝牙后需要一些时间才能获取到时开启状态,这里延时一下处理
                        delay(1000)
                        val enable = BleManager.get().isBleEnable()
                        BleLogger.i("是否打开了蓝牙: $enable")
                        try {
                            it.resume(enable)
                        } catch (e: Exception) {
                            BleLogger.e(e.message)
                        }
                    }
                }
            }
        }
        return hasScanPermission
    }

    /**
     * 开始扫描
     */
    fun startScan(activity: BaseActivity<*, *>) {
        viewModelScope.launch {
            val hasScanPermission = hasScanPermission(activity)
            if (hasScanPermission) {
                BleManager.get().startScan(getScanCallback(true))
            } else {
                BleLogger.e("请检查权限、检查GPS开关、检查蓝牙开关")
            }
        }
    }

    private fun getScanCallback(showData: Boolean): BleScanCallback.() -> Unit {
        return {
            onScanStart {
                BleLogger.d("onScanStart")
                scanStopMutableStateFlow.value = false
            }
            onLeScan { bleDevice, _ ->
                //可以根据currentScanCount是否已有清空列表数据
                bleDevice.deviceName?.let { _ ->

                }
            }
            onLeScanDuplicateRemoval { bleDevice, _ ->
                bleDevice.deviceName?.let { _ ->
                    if (showData) {
                        listDRData.add(bleDevice)
                        listDRMutableStateFlow.value = bleDevice
                    }
                }
            }
            onScanComplete { bleDeviceList, bleDeviceDuplicateRemovalList ->
                //扫描到的数据是所有扫描次数的总和
                bleDeviceList.forEach {
                    it.deviceName?.let { deviceName ->
                        BleLogger.i("bleDeviceList-> $deviceName, ${it.deviceAddress}")
                    }
                }
                bleDeviceDuplicateRemovalList.forEach {
                    it.deviceName?.let { deviceName ->
                        BleLogger.e("bleDeviceDuplicateRemovalList-> $deviceName, ${it.deviceAddress}")
                    }
                }
                scanStopMutableStateFlow.value = true
                if (listDRData.isEmpty() && showData) {
                    Toast.makeText(application, "没有扫描到数据", Toast.LENGTH_SHORT).show()
                }
            }
            onScanFail {
                val msg: String = when (it) {
                    is BleScanFailType.UnSupportBle -> "设备不支持蓝牙"
                    is BleScanFailType.NoBlePermission -> "权限不足,请检查"
                    is BleScanFailType.GPSDisable -> "设备未打开GPS定位"
                    is BleScanFailType.BleDisable -> "蓝牙未打开"
                    is BleScanFailType.AlReadyScanning -> "正在扫描"
                    is BleScanFailType.ScanError -> {
                        "${it.throwable?.message}"
                    }
                }
                BleLogger.e(msg)
                Toast.makeText(application, msg, Toast.LENGTH_SHORT).show()
                scanStopMutableStateFlow.value = true
            }
        }
    }

    /**
     * 停止扫描
     */
    fun stopScan() {
        BleManager.get().stopScan()
    }

    /**
     * 是否已连接
     */
    fun isConnected(bleDevice: BleDevice?) = BleManager.get().isConnected(bleDevice)

    /**
     * 开始连接
     */
    fun connect(address: String) {
        connect(BleManager.get().buildBleDeviceByDeviceAddress(address))
    }

    /**
     * 扫描并连接,如果扫描到多个设备,则会连接第一个
     */
    fun startScanAndConnect(activity: BaseActivity<*, *>) {
        viewModelScope.launch {
            val hasScanPermission = hasScanPermission(activity)
            if (hasScanPermission) {
                BleManager.get().startScanAndConnect(
                    false,
                    getScanCallback(false),
                    connectCallback
                )
            }
        }
    }

    /**
     * 开始连接
     */
    fun connect(bleDevice: BleDevice?) {
        bleDevice?.let { device ->
            BleManager.get().connect(device, false, connectCallback)
        }
    }

    private val connectCallback: BleConnectCallback.() -> Unit = {
        onConnectStart {
            BleLogger.e("-----onConnectStart")
        }
        onConnectFail { bleDevice, connectFailType ->
            val msg: String = when (connectFailType) {
                is BleConnectFailType.UnSupportBle -> "设备不支持蓝牙"
                is BleConnectFailType.NoBlePermission -> "权限不足,请检查"
                is BleConnectFailType.NullableBluetoothDevice -> "设备为空"
                is BleConnectFailType.BleDisable -> "蓝牙未打开"
                is BleConnectFailType.ConnectException -> "连接异常(${connectFailType.throwable.message})"
                is BleConnectFailType.ConnectTimeOut -> "连接超时"
                is BleConnectFailType.AlreadyConnecting -> "连接中"
                is BleConnectFailType.ScanNullableBluetoothDevice -> "连接失败,扫描数据为空"
            }
            BleLogger.e(msg)
            Toast.makeText(application, msg, Toast.LENGTH_SHORT).show()
            refreshMutableStateFlow.value = RefreshBleDevice(bleDevice, System.currentTimeMillis())
        }
        onDisConnecting { isActiveDisConnected, bleDevice, _, _ ->
            BleLogger.e("-----${bleDevice.deviceAddress} -> onDisConnecting: $isActiveDisConnected")
        }
        onDisConnected { isActiveDisConnected, bleDevice, _, _ ->
            Toast.makeText(application, "断开连接(${bleDevice.deviceAddress},isActiveDisConnected: " +
                    "$isActiveDisConnected)", Toast.LENGTH_SHORT).show()
            BleLogger.e("-----${bleDevice.deviceAddress} -> onDisConnected: $isActiveDisConnected")
            refreshMutableStateFlow.value = RefreshBleDevice(bleDevice, System.currentTimeMillis())
            //发送断开的通知
            val message = MessageEvent()
            message.data = bleDevice
            EventBus.getDefault().post(message)
        }
        onConnectSuccess { bleDevice, _ ->
            Toast.makeText(application, "连接成功(${bleDevice.deviceAddress})", Toast.LENGTH_SHORT).show()
            refreshMutableStateFlow.value = RefreshBleDevice(bleDevice, System.currentTimeMillis())
        }
    }

    /**
     * 断开连接
     */
    fun disConnect(bleDevice: BleDevice?) {
        bleDevice?.let { device ->
            BleManager.get().disConnect(device)
        }
    }

    /**
     * 断开所有连接 释放资源
     */
    fun close() {
        BleManager.get().closeAll()
    }
}

================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:height="108dp"
        android:width="108dp"
        android:viewportHeight="108"
        android:viewportWidth="108">
    <path
            android:fillColor="#3DDC84"
            android:pathData="M0,0h108v108h-108z" />
    <path
            android:fillColor="#00000000"
            android:pathData="M9,0L9,108"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M19,0L19,108"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M29,0L29,108"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M39,0L39,108"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M49,0L49,108"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M59,0L59,108"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M69,0L69,108"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M79,0L79,108"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M89,0L89,108"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M99,0L99,108"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M0,9L108,9"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M0,19L108,19"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M0,29L108,29"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M0,39L108,39"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M0,49L108,49"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M0,59L108,59"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M0,69L108,69"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M0,79L108,79"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M0,89L108,89"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M0,99L108,99"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M19,29L89,29"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M19,39L89,39"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M19,49L89,49"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M19,59L89,59"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M19,69L89,69"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M19,79L89,79"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M29,19L29,89"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M39,19L39,89"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M49,19L49,89"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M59,19L59,89"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M69,19L69,89"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
    <path
            android:fillColor="#00000000"
            android:pathData="M79,19L79,89"
            android:strokeColor="#33FFFFFF"
            android:strokeWidth="0.8" />
</vector>


================================================
FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:aapt="http://schemas.android.com/aapt"
        android:width="108dp"
        android:height="108dp"
        android:viewportWidth="108"
        android:viewportHeight="108">
    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
        <aapt:attr name="android:fillColor">
            <gradient
                    android:startY="49.59793"
                    android:startX="42.9492"
                    android:endY="92.4963"
                    android:endX="85.84757"
                    android:type="linear">
                <item
                        android:color="#44000000"
                        android:offset="0.0" />
                <item
                        android:color="#00000000"
                        android:offset="1.0" />
            </gradient>
        </aapt:attr>
    </path>
    <path
            android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
            android:fillColor="#FFFFFF"
            android:fillType="nonZero"
            android:strokeWidth="1"
            android:strokeColor="#00000000" />
</vector>

================================================
FILE: app/src/main/res/layout/activity_detail.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical"
    tools:context=".ui.MainActivity">

    <TextView
        android:id="@+id/tvName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="@color/black"
        android:paddingHorizontal="16dp"
        android:paddingVertical="8dp"
        android:textSize="16sp"/>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_marginHorizontal="16dp"
        android:layout_marginVertical="12dp"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="折叠或收起"
        android:onClick="showContent"
        android:paddingBottom="12dp"
        android:gravity="center"/>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:id="@+id/llContent"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2"
        android:orientation="vertical">

        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="end"
            android:layout_marginHorizontal="16dp"
            android:layout_marginBottom="5dp"
            android:orientation="horizontal">

            <Button
                android:id="@+id/btnConnectionPriority"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="40dp"
                android:layout_marginEnd="16dp"
                android:textColor="@color/white"
                android:gravity="center"
                android:layout_gravity="end"
                android:textAllCaps="false"
                android:textSize="13sp"
                android:padding="0dp"
                android:background="@color/black"
                android:text="Priority"/>

            <Button
                android:id="@+id/btnReadRssi"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="40dp"
                android:layout_marginEnd="20dp"
                android:textColor="@color/white"
                android:gravity="center"
                android:layout_gravity="end"
                android:textSize="13sp"
                android:padding="0dp"
                android:background="@color/black"
                android:text="读取Rssi"/>

            <Button
                android:id="@+id/btnSetMtu"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="40dp"
                android:layout_marginEnd="20dp"
                android:textColor="@color/white"
                android:gravity="center"
                android:layout_gravity="end"
                android:textSize="13sp"
                android:padding="0dp"
                android:background="@color/black"
                android:text="设置Mtu"/>

            <Button
                android:id="@+id/btnClear"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="40dp"
                android:textColor="@color/white"
                android:gravity="center"
                android:layout_gravity="end"
                android:textSize="13sp"
                android:padding="0dp"
                android:background="@color/black"
                android:text="清除日志"/>
        </androidx.appcompat.widget.LinearLayoutCompat>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/logRecyclerView"
            android:layout_width="match_parent"
            android:layout_marginHorizontal="16dp"
            android:layout_height="0dp"
            android:layout_weight="1"/>

        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_marginTop="6dp"
            android:layout_marginHorizontal="10dp"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/etContent"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:hint="数据格式:aa00bb11cc22 (支持空格)"
                android:textSize="14sp"
                android:enabled="false"
                android:textColor="@color/black"
                android:textColorHint="#999999"
                android:maxLines="1"/>

            <Button
                android:id="@+id/btnSend"
                android:layout_width="60dp"
                android:layout_height="40dp"
                android:layout_marginStart="10dp"
                android:textColor="@color/white"
                android:gravity="center"
                android:textSize="13sp"
                android:padding="0dp"
                android:enabled="false"
                android:background="@color/black"
                android:text="发送"/>
        </androidx.appcompat.widget.LinearLayoutCompat>
    </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>

================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ui.MainActivity">

    <View
        android:id="@+id/vTop"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:background="@color/black"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="56dp"
        android:background="@color/black"
        android:text="BleCore"
        android:textSize="17sp"
        android:textColor="@color/white"
        android:gravity="center_vertical"
        android:paddingHorizontal="20dp"/>

    <ProgressBar
        android:id="@+id/pbLoading"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_gravity="center"
        android:layout_marginTop="14dp"
        android:visibility="invisible"
        android:indeterminate="true"
        android:indeterminateTint="@color/black"/>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_margin="20dp"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <EditText
        android:id="@+id/etAddress"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_marginHorizontal="28dp"
        android:text="49:A6:84:E0:4D:33"
        android:hint="输入连接指定设备的地址"/>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_margin="20dp"
        android:gravity="center">

        <Button
            android:id="@+id/btnConnect"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="40dp"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="10dp"
            android:textColor="@color/white"
            android:gravity="center"
            android:textSize="12sp"
            android:padding="0dp"
            android:background="@color/black"
            android:text="连接指定设备"/>

        <Button
            android:id="@+id/btnSetting"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="40dp"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="10dp"
            android:textColor="@color/white"
            android:gravity="center"
            android:textSize="14sp"
            android:padding="0dp"
            android:background="@color/black"
            android:text="设置"/>

        <Button
            android:id="@+id/btnStart"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="40dp"
            android:layout_marginStart="0dp"
            android:layout_marginEnd="10dp"
            android:textColor="@color/white"
            android:gravity="center"
            android:textSize="14sp"
            android:padding="0dp"
            android:background="@color/black"
            android:text="开启扫描"/>

        <Button
            android:id="@+id/btnStop"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="40dp"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="10dp"
            android:textColor="@color/white"
            android:enabled="false"
            android:gravity="center"
            android:textSize="14sp"
            android:padding="0dp"
            android:background="@color/black"
            android:text="停着扫描"/>

    </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>

================================================
FILE: app/src/main/res/layout/activity_setting.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:orientation="vertical"
        tools:context=".ui.MainActivity">

    <androidx.core.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:paddingHorizontal="15dp">

        <androidx.appcompat.widget.LinearLayoutCompat
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginVertical="20dp"
                android:orientation="vertical">

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="扫描服务UUID:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etScanServiceUuid"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="设置扫描过滤服务UUID,多个以英文逗号分开,默认空"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="扫描设备名称:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etScanDeviceName"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="设置扫描过滤设备名称,多个以英文逗号分开,默认空"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="扫描设备地址:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etScanDeviceAddress"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="设置扫描过设备地址,多个以英文逗号分开,默认空"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="扫描超时时间:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etScanOutTime"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="默认10000毫秒,单位毫秒"
                        android:text="10000"
                        android:textSize="16sp"
                        android:inputType="number"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="扫描重试次数和间隔:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etScanRetryCount"
                        android:layout_width="0dp"
                        android:layout_weight="1"
                        android:layout_height="wrap_content"
                        android:hint="默认0次"
                        android:text="0"
                        android:inputType="number"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

                <EditText
                        android:id="@+id/etScanRetryInterval"
                        android:layout_width="0dp"
                        android:layout_weight="1"
                        android:layout_height="wrap_content"
                        android:hint="默认1000毫秒,单位毫秒"
                        android:text="1000"
                        android:inputType="number"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="连接超时时间:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etConnectOutTime"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="默认10000毫秒,单位毫秒"
                        android:text="10000"
                        android:inputType="number"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="连接重试次数和间隔:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etConnectRetryCount"
                        android:layout_width="0dp"
                        android:layout_weight="1"
                        android:layout_height="wrap_content"
                        android:hint="默认0次"
                        android:text="0"
                        android:inputType="number"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

                <EditText
                        android:id="@+id/etConnectRetryInterval"
                        android:layout_width="0dp"
                        android:layout_weight="1"
                        android:layout_height="wrap_content"
                        android:hint="默认1000毫秒,单位毫秒"
                        android:text="1000"
                        android:inputType="number"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="交互超时时间:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etOperateMillisTimeOut"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="默认5000毫秒,单位毫秒"
                        android:inputType="number"
                        android:text="5000"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="写数据间隔时间:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etOperateInterval"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="默认100毫秒,单位毫秒"
                        android:text="100"
                        android:inputType="number"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="最大连接设备数:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etMaxConnectNum"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:hint="默认7个"
                        android:inputType="number"
                        android:text="7"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="MTU值:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <EditText
                        android:id="@+id/etMTU"
                        android:layout_width="0dp"
                        android:layout_weight="1"
                        android:layout_height="wrap_content"
                        android:hint="默认20"
                        android:text="20"
                        android:inputType="number"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:textColorHint="#999999"
                        android:maxLines="4"/>

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="是否连接后自动设置mtu:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <CheckBox
                        android:id="@+id/cbMtu"
                        android:layout_width="40dp"
                        android:layout_height="40dp"
                        android:checked="true"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="是否模糊匹配设备名称:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <CheckBox
                        android:id="@+id/cbContainScanDeviceName"
                        android:layout_width="40dp"
                        android:layout_height="40dp"
                        android:checked="false"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:layout_marginVertical="10dp"
                android:orientation="horizontal">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="扫描设备是否需要检测GPS开关:"
                    android:textSize="16sp"
                    android:textColor="@color/black"
                    android:layout_marginEnd="12dp"/>

                <CheckBox
                    android:id="@+id/cbNeedCheckGPS"
                    android:layout_width="40dp"
                    android:layout_height="40dp"
                    android:checked="true"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginVertical="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="是否日志输出:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <CheckBox
                        android:id="@+id/cbLogger"
                        android:layout_width="40dp"
                        android:layout_height="40dp"
                        android:checked="true"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <androidx.appcompat.widget.LinearLayoutCompat
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginTop="10dp"
                    android:orientation="horizontal">

                <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="非主动断开后是否自动连接:"
                        android:textSize="16sp"
                        android:textColor="@color/black"
                        android:layout_marginEnd="12dp"/>

                <CheckBox
                        android:id="@+id/cbAutoConnect"
                        android:layout_width="40dp"
                        android:layout_height="40dp"
                        android:checked="false"/>

            </androidx.appcompat.widget.LinearLayoutCompat>

            <Spinner
                    android:id="@+id/spTaskQueueType"
                    android:layout_width="match_parent"
                    android:layout_height="50dp"
                    android:layout_marginTop="10dp"/>

        </androidx.appcompat.widget.LinearLayoutCompat>

    </androidx.core.widget.NestedScrollView>

    <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_margin="20dp"
            android:gravity="center">

        <Button
                android:id="@+id/btnReSet"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="40dp"
                android:layout_marginStart="0dp"
                android:layout_marginEnd="10dp"
                android:textColor="@color/white"
                android:gravity="center"
                android:textSize="14sp"
                android:padding="0dp"
                android:background="@color/black"
                android:text="重置"/>

        <Button
                android:id="@+id/btnSave"
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="40dp"
                android:layout_marginStart="10dp"
                android:layout_marginEnd="10dp"
                android:textColor="@color/white"
                android:gravity="center"
                android:textSize="14sp"
                android:padding="0dp"
                android:background="@color/black"
                android:text="保存"/>

    </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>

================================================
FILE: app/src/main/res/layout/layout_recycler_characteristic.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingVertical="10dp"
        android:gravity="center_vertical">

    <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="10dp"
            android:gravity="center_vertical">

        <TextView
                android:id="@+id/tvCharacteristicName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textColor="@color/color_main"
                android:textSize="14sp"
                android:textIsSelectable="true"
                android:gravity="center_vertical"
                android:ellipsize="end"
                android:maxLines="3"
                tools:text="特征值名称"/>

        <TextView
                android:id="@+id/tvCharacteristicUUID"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textColor="#666666"
                android:layout_marginTop="4dp"
                android:textSize="12sp"
                android:textIsSelectable="true"
                android:gravity="center_vertical"
                android:ellipsize="end"
                android:maxLines="3"
                tools:text="特征值名称"/>

        <TextView
                android:id="@+id/tvCharacteristicProperties"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textColor="#666666"
                android:layout_marginTop="4dp"
                android:textSize="12sp"
                android:textIsSelectable="true"
                android:gravity="center_vertical"
                android:ellipsize="end"
                android:maxLines="3"
                tools:text="特征值属性"/>

    </androidx.appcompat.widget.LinearLayoutCompat>

    <Button
            android:id="@+id/btnReadData"
            android:layout_width="70dp"
            android:layout_height="40dp"
            android:layout_marginEnd="10dp"
            android:textColor="@color/white"
            android:gravity="center"
            android:textSize="13sp"
            android:padding="0dp"
            android:visibility="gone"
            android:background="@color/black"
            android:text="读数据"/>

    <CheckBox
            android:id="@+id/cbWrite"
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:text="写"
            android:textColor="@color/black"
            android:textSize="14sp"
            android:visibility="gone"
            android:layout_marginEnd="10dp"
            android:checked="false"/>

    <CheckBox
            android:id="@+id/cbNotify"
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:text="Notify"
            android:textColor="@color/black"
            android:textSize="14sp"
            android:visibility="gone"
            android:layout_marginEnd="10dp"
            android:checked="false"/>

    <CheckBox
            android:id="@+id/cbIndicate"
            android:layout_width="wrap_content"
            android:layout_height="40dp"
            android:text="Indicate"
            android:textColor="@color/black"
            android:textSize="14sp"
            android:visibility="gone"
            android:layout_marginEnd="10dp"
            android:checked="false"/>
</LinearLayout>

================================================
FILE: app/src/main/res/layout/layout_recycler_item.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:orientation="horizontal"
        android:gravity="center_vertical"
        tools:ignore="UseCompoundDrawables">

    <TextView
            android:id="@+id/tvName"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="60dp"
            android:textColor="@color/black"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="4dp"
            android:textSize="14sp"
            android:textIsSelectable="true"
            android:gravity="center_vertical"
            android:ellipsize="end"
            android:maxLines="3"
            android:text="设备名称"/>

    <ImageView
            android:id="@+id/ivRssi"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="12dp"
            android:gravity="center"/>

    <TextView
            android:id="@+id/btnConnect"
            android:layout_width="80dp"
            android:layout_height="36dp"
            android:layout_marginStart="0dp"
            android:layout_marginEnd="10dp"
            android:textColor="@color/white"
            android:gravity="center"
            android:textSize="13sp"
            android:padding="0dp"
            android:background="@color/black"
            android:text="连接"/>

    <Button
            android:id="@+id/btnOperate"
            android:layout_width="80dp"
            android:layout_height="36dp"
            android:layout_marginStart="0dp"
            android:layout_marginEnd="10dp"
            android:textColor="@color/white"
            android:enabled="false"
            android:gravity="center"
            android:textSize="13sp"
            android:padding="0dp"
            android:background="@color/black"
            android:text="操作"/>
</androidx.appcompat.widget.LinearLayoutCompat>

================================================
FILE: app/src/main/res/layout/layout_recycler_log.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        tools:ignore="UseCompoundDrawables">

    <TextView
            android:id="@+id/tvTime"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#666666"
            android:textSize="12sp"
            android:textIsSelectable="true"/>

    <TextView
            android:id="@+id/tvName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="12dp"
            android:layout_marginHorizontal="5dp"
            android:textColor="#666666"
            android:textSize="12sp"
            android:textIsSelectable="true"/>

</androidx.appcompat.widget.LinearLayoutCompat>

================================================
FILE: app/src/main/res/layout/layout_recycler_service.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingVertical="10dp"
        android:gravity="center_vertical">

    <ImageView
            android:id="@+id/ivExpand"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:src="@drawable/icon_right"
            android:visibility="invisible"/>

    <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:gravity="center_vertical">

        <TextView
                android:id="@+id/tvServiceName"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/red"
                android:textSize="16sp"
                android:textIsSelectable="true"
                android:gravity="center_vertical"
                android:ellipsize="end"
                android:maxLines="3"
                tools:text="服务名称"/>

        <TextView
                android:id="@+id/tvServiceUUID"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="#666666"
                android:layout_marginTop="4dp"
                android:textSize="14sp"
                android:textIsSelectable="true"
                android:gravity="center_vertical"
                android:ellipsize="end"
                android:maxLines="3"
                tools:text="服务名称"/>
    </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>

================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="red">#FFFF0000</color>
    <color name="white">#FFFFFFFF</color>
</resources>

================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
    <string name="app_name">BleCore</string>
</resources>

================================================
FILE: app/src/main/res/values/themes.xml
================================================
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.BleCore" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/black</item>
        <item name="colorPrimaryVariant">@color/black</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor">@color/black</item>
        <!-- Customize your theme here. -->
    </style>
</resources>

================================================
FILE: app/src/main/res/values-night/themes.xml
================================================
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.BleCore" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/black</item>
        <item name="colorPrimaryVariant">@color/black</item>
        <item name="colorOnPrimary">@color/black</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_200</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor">@color/black</item>
        <!-- Customize your theme here. -->
    </style>
</resources>

================================================
FILE: app/src/main/res/xml/backup_rules.xml
================================================
<?xml version="1.0" encoding="utf-8"?><!--
   Sample backup rules file; uncomment and customize as necessary.
   See https://developer.android.com/guide/topics/data/autobackup
   for details.
   Note: This file is ignored for devices older that API 31
   See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
    <!--
   <include domain="sharedpref" path="."/>
   <exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

================================================
FILE: app/src/main/res/xml/data_extraction_rules.xml
================================================
<?xml version="1.0" encoding="utf-8"?><!--
   Sample data extraction rules file; uncomment and customize as necessary.
   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
   for details.
-->
<data-extraction-rules>
    <cloud-backup>
        <!--
        <include .../>
        <exclude .../>
        -->
    </cloud-backup>
    <!--
    <device-transfer>
        <include .../>
        <exclude .../>
    </device-transfer>
    -->
</data-extraction-rules>

================================================
FILE: app/src/test/java/com/bhm/demo/ExampleUnitTest.kt
================================================
package com.bhm.demo

import org.junit.Test

import org.junit.Assert.*

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}

================================================
FILE: ble/.gitignore
================================================
/build

================================================
FILE: ble/build.gradle
================================================
plugins {
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
    id 'maven-publish'
}

//ext.GROUP = "com.lute.ble"
//ext.POM_ARTIFACT_ID = "BleCore"
//ext.VERSION_NAME = "2.6.1"
////引用gradle_upload.gradle
//apply from: "${project.rootDir}/maven_upload.gradle"

afterEvaluate {
    publishing {
        publications {
            // 这个mavenJava可以随便填,只是一个任务名字而已
            // MavenPublication必须有,这个是调用的任务类
            mavenJava(MavenPublication) {
                // 这里头是artifacts的配置信息,不填会采用默认的
                groupId = 'com.github.buhuiming'
                artifactId = 'BleCore'
                version = '2.6.1'

                from components.release
                if (!project.plugins.hasPlugin('kotlin-android')) {
                    artifact tasks.named("androidSourcesJar").get()
                }
            }
        }
    }
    // 显式声明任务依赖关系
    tasks.matching { it.name.startsWith("generateMetadataFileFor") }.configureEach {
        dependsOn tasks.named("androidSourcesJar")
    }
}
// 用于打包源代码的任务
tasks.register('androidSourcesJar', Jar) {
    archiveClassifier.set('sources')
    from android.sourceSets.main.java.srcDirs
}

android {
    namespace 'com.bhm.ble'
    compileSdk 35

    defaultConfig {
        minSdk 24
        targetSdk 35

    }
    publishing {
        singleVariant("release")
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = '17'
    }
}

dependencies {
//    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.10"
    implementation 'androidx.lifecycle:lifecycle-common:2.9.2'
}

================================================
FILE: ble/consumer-rules.pro
================================================


================================================
FILE: ble/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

================================================
FILE: ble/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">


    <!-- android 30之前 静态声明的蓝牙权限 -->
    <uses-permission
        android:name="android.permission.BLUETOOTH"
        tools:remove="android:maxSdkVersion" />

    <uses-permission
        android:name="android.permission.BLUETOOTH_ADMIN"
        android:maxSdkVersion="30" />
    <!-- 使用蓝牙还需要 静态声明+动态申请位置(前台位置)权限 -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <!-- android 30之后 静态声明+动态申请的蓝牙权限 -->
    <uses-permission
        android:name="android.permission.BLUETOOTH_SCAN"
        android:usesPermissionFlags="neverForLocation"
        tools:targetApi="s" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

    <!--使用动态注册,避免国内应用市场对于此注册导致的应用自启动问题,而拒绝上架-->
    <!--<application>
        &lt;!&ndash;注册系统蓝牙广播&ndash;&gt;
        <receiver android:name=".receiver.BluetoothReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.bluetooth.adapter.action.STATE_CHANGED"/>
            </intent-filter>
        </receiver>
    </application>-->
</manifest>

================================================
FILE: ble/src/main/java/com/bhm/ble/BleManager.kt
================================================
@file:Suppress("SENSELESS_COMPARISON", "unused")

package com.bhm.ble

import android.annotation.SuppressLint
import android.app.Application
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.Context
import android.util.SparseArray
import com.bhm.ble.attribute.BleOptions
import com.bhm.ble.callback.*
import com.bhm.ble.data.BleDescriptorGetType
import com.bhm.ble.data.Constants.DEFAULT_MTU
import com.bhm.ble.device.BleDevice
import com.bhm.ble.request.base.BleBaseRequest
import com.bhm.ble.request.base.BleRequestImp
import com.bhm.ble.log.BleLogger
import com.bhm.ble.utils.BleUtil


/**
 * Android蓝牙低功耗核心类
 * @author Buhuiming
 * @date 2023年05月18日 13时37分
 */
class BleManager private constructor() {

    private var application: Application? = null

    private var bleOptions: BleOptions? = null

    private var bluetoothManager: BluetoothManager? = null

    private var bleBaseRequest: BleBaseRequest? = null

    companion object {

        private var instance: BleManager = BleManager()

        fun get(): BleManager {
            if (instance == null) {
                instance = BleManager()
            }
            return instance
        }
    }

    /**
     * 初始化,使用BleManager其他方法前,需先调用此方法
     */
    fun init(context: Application, option: BleOptions? = null) {
        application = context
        bleOptions = option
        if (bleOptions == null) {
            bleOptions = BleOptions.getDefaultBleOptions()
        }
        bleBaseRequest = BleRequestImp.get()
        bluetoothManager = application?.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?
        BleLogger.isLogger = bleOptions?.enableLog?: false
        BleLogger.i("ble Successful initialization")
    }

    /**
     * 设备是否支持蓝牙
     *  @return true = 支持
     */
    fun isBleSupport(): Boolean {
        return BleUtil.isBleSupport(application?.applicationContext)
    }

    /**
     * 蓝牙是否打开
     * @return true = 打开
     */
    fun isBleEnable(): Boolean {
        val bluetoothAdapter = bluetoothManager?.adapter
        return isBleSupport() && (bluetoothAdapter?.isEnabled?: false)
    }

    /**
     * 开始扫描
     * @param scanMillisTimeOut 扫描超时时间,单位毫秒,只对单次扫描有效
     * @param scanRetryCount 设置扫描重试次数,只对单次扫描有效
     * @param scanRetryInterval 设置扫描重试间隔,单位毫秒,只对单次扫描有效
     */
    @Synchronized
    fun startScan(
        scanMillisTimeOut: Long?,
        scanRetryCount: Int?,
        scanRetryInterval: Long?,
        bleScanCallback: BleScanCallback.() -> Unit
    ) {
        checkInitialize()
        bleBaseRequest?.startScan(
            scanMillisTimeOut,
            scanRetryCount,
            scanRetryInterval,
            bleScanCallback
        )
    }

    /**
     * 开始扫描
     */
    fun startScan(
        bleScanCallback: BleScanCallback.() -> Unit
    ) {
        startScan(null, null, null, bleScanCallback)
    }

    /**
     * 是否扫描中
     * @return true = 扫描中
     */
    fun isScanning(): Boolean {
        checkInitialize()
        return bleBaseRequest?.isScanning()?: false
    }

    /**
     * 停止扫描
     */
    @Synchronized
    fun stopScan() {
        checkInitialize()
        bleBaseRequest?.stopScan()
    }

    /**
     * 是否已连接,确保已获取到权限
     *
     * 操作断开连接后,getConnectionState马上回去到的状态还是连接状态,所以需要bleBaseRequest?.isConnected判断
     *  @param simplySystemStatus 为true,只根据系统的状态规则;为false,会根据sdk的状态;
     *  此字段的意义在于:有时,sdk资源被系统回收(状态未连接),但是系统的状态是已连接。
     */
    @SuppressLint("MissingPermission")
    fun isConnected(bleDeviceAddress: String, simplySystemStatus: Boolean = true): Boolean {
        return isConnected(buildBleDeviceByDeviceAddress(bleDeviceAddress), simplySystemStatus)
    }

    /**
     * 是否已连接,确保已获取到权限
     */
    @SuppressLint("MissingPermission")
    fun isConnected(bleDevice: BleDevice?, simplySystemStatus: Boolean = true): Boolean {
        checkInitialize()
        if (!BleUtil.isPermission(application)) {
            return false
        }
        bleDevice?.let {
            val connectedDevices: List<BluetoothDevice>? = bluetoothManager?.getConnectedDevices(BluetoothProfile.GATT)
            if (connectedDevices.isNullOrEmpty()) {
                return false
            }
            for (connectedDevice in connectedDevices) {
                if (it.deviceAddress == connectedDevice.address) {
                    return if (simplySystemStatus) {
                        true
                    } else {
                        bleBaseRequest?.isConnected(it) == true
                    }
                }
            }
        }
        return false
    }

    /**
     * 连接
     * @param connectMillisTimeOut 连接超时时间,单位毫秒,只对单次连接有效
     * @param connectRetryCount 设置连接重试次数,只对单次连接有效
     * @param connectRetryInterval 设置连接重试间隔,只对单次连接有效
     * @param isForceConnect 是否强制连接(针对已连接情况,是否重连)
     */
    @Synchronized
    fun connect(bleDevice: BleDevice,
                connectMillisTimeOut: Long?,
                connectRetryCount: Int?,
                connectRetryInterval: Long?,
                isForceConnect: Boolean = false,
                bleConnectCallback: BleConnectCallback.() -> Unit
    ) {
        checkInitialize()
        if (getOptions()?.stopScanWhenStartConnect == true) {
            stopScan()
        }
        bleBaseRequest?.connect(
            bleDevice,
            connectMillisTimeOut,
            connectRetryCount,
            connectRetryInterval,
            isForceConnect,
            bleConnectCallback
        )
    }

    /**
     * 通过地址连接
     * @param connectMillisTimeOut 连接超时时间,单位毫秒,只对单次连接有效
     * @param connectRetryCount 设置连接重试次数,只对单次连接有效
     * @param connectRetryInterval 设置连接重试间隔,单位毫秒,只对单次连接有效
     */
    fun connect(address: String,
                connectMillisTimeOut: Long?,
                connectRetryCount: Int?,
                connectRetryInterval: Long?,
                isForceConnect: Boolean = false,
                bleConnectCallback: BleConnectCallback.() -> Unit
    ) {
        connect(
            buildBleDeviceByDeviceAddress(address),
            connectMillisTimeOut,
            connectRetryCount,
            connectRetryInterval,
            isForceConnect,
            bleConnectCallback
        )
    }

    /**
     * 连接
     */
    fun connect(
        bleDevice: BleDevice,
        isForceConnect: Boolean = false,
        bleConnectCallback: BleConnectCallback.() -> Unit
    ) {
        connect(
            bleDevice,
            null,
            null,
            null,
            isForceConnect,
            bleConnectCallback
        )
    }

    /**
     * 通过地址连接
     */
    fun connect(
        address: String,
        isForceConnect: Boolean = false,
        bleConnectCallback: BleConnectCallback.() -> Unit
    ) {
        connect(
            address,
            null,
            null,
            null,
            isForceConnect,
            bleConnectCallback
        )
    }

    @Synchronized
    fun startScanAndConnect(scanMillisTimeOut: Long?,
                            scanRetryCount: Int?,
                            scanRetryInterval: Long?,
                            connectMillisTimeOut: Long?,
                            connectRetryCount: Int?,
                            connectRetryInterval: Long?,
                            isForceConnect: Boolean = false,
                            bleScanCallback: BleScanCallback.() -> Unit,
                            bleConnectCallback: BleConnectCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.startScanAndConnect(
            scanMillisTimeOut,
            scanRetryCount,
            scanRetryInterval,
            connectMillisTimeOut,
            connectRetryCount,
            connectRetryInterval,
            isForceConnect,
            bleScanCallback,
            bleConnectCallback
        )
    }

    fun startScanAndConnect(
        isForceConnect: Boolean = false,
        bleScanCallback: BleScanCallback.() -> Unit,
        bleConnectCallback: BleConnectCallback.() -> Unit
    ) {
        startScanAndConnect(
            null,
            null,
            null,
            null,
            null,
            null,
            isForceConnect,
            bleScanCallback,
            bleConnectCallback
        )
    }

    /**
     * 取消/停止连接
     */
    @Synchronized
    fun stopConnect(bleDevice: BleDevice?) {
        checkInitialize()
        bleDevice?.let {
            bleBaseRequest?.stopConnect(it)
        }
    }

    /**
     * 断开连接
     */
    @Synchronized
    fun disConnect(bleDevice: BleDevice) {
        checkInitialize()
        bleBaseRequest?.disConnect(bleDevice)
    }

    /**
     * 通过地址断开连接
     */
    fun disConnect(address: String) {
        disConnect(buildBleDeviceByDeviceAddress(address))
    }

    /**
     * 获取设备的BluetoothGatt对象
     */
    @Synchronized
    fun getBluetoothGatt(bleDevice: BleDevice): BluetoothGatt? {
        checkInitialize()
        return bleBaseRequest?.getBluetoothGatt(bleDevice)
    }

    /**
     * notify
     */
    @Deprecated(message = "请使用BleDescriptorGetType参数方式",
        replaceWith = ReplaceWith(
            "notify(BleDevice, String, String, BleDescriptorGetType, BleNotifyCallback)"
        )
    )
    fun notify(bleDevice: BleDevice,
               serviceUUID: String,
               notifyUUID: String,
               useCharacteristicDescriptor: Boolean = false,
               bleNotifyCallback: BleNotifyCallback.() -> Unit) {
        notify(
            bleDevice = bleDevice,
            serviceUUID = serviceUUID,
            notifyUUID = notifyUUID,
            bleDescriptorGetType = if (useCharacteristicDescriptor) {
                BleDescriptorGetType.CharacteristicDescriptor
            } else {
                BleDescriptorGetType.Default
            },
            bleNotifyCallback = bleNotifyCallback,
        )
    }

    /**
     * notify
     */
    fun notify(bleDevice: BleDevice,
               serviceUUID: String,
               notifyUUID: String,
               timeoutMillis: Long? = bleOptions?.operateMillisTimeOut,
               bleDescriptorGetType: BleDescriptorGetType = BleDescriptorGetType.Default,
               bleNotifyCallback: BleNotifyCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.notify(
            bleDevice,
            serviceUUID,
            notifyUUID,
            timeoutMillis,
            bleDescriptorGetType,
            bleNotifyCallback
        )
    }

    /**
     * notify
     */
    fun notify(bleDevice: BleDevice,
               serviceUUID: String,
               notifyUUID: String,
               bleDescriptorGetType: BleDescriptorGetType = BleDescriptorGetType.Default,
               bleNotifyCallback: BleNotifyCallback.() -> Unit) {
        notify(
            bleDevice,
            serviceUUID,
            notifyUUID,
            null,
            bleDescriptorGetType,
            bleNotifyCallback
        )
    }

    /**
     * stop notify
     */
    @Deprecated(message = "请使用BleDescriptorGetType参数方式",
        replaceWith = ReplaceWith(
            "stopNotify(BleDevice, String, String, BleDescriptorGetType)"
        )
    )
    fun stopNotify(
        bleDevice: BleDevice,
        serviceUUID: String,
        notifyUUID: String,
        useCharacteristicDescriptor: Boolean = false
    ): Boolean? {
        return stopNotify(
            bleDevice = bleDevice,
            serviceUUID = serviceUUID,
            notifyUUID = notifyUUID,
            bleDescriptorGetType = if (useCharacteristicDescriptor) {
                BleDescriptorGetType.CharacteristicDescriptor
            } else {
                BleDescriptorGetType.Default
            },
        )
    }

    /**
     * stop notify
     */
    fun stopNotify(
        bleDevice: BleDevice,
        serviceUUID: String,
        notifyUUID: String,
        bleDescriptorGetType: BleDescriptorGetType = BleDescriptorGetType.Default,
    ): Boolean? {
        checkInitialize()
        return bleBaseRequest?.stopNotify(
            bleDevice,
            serviceUUID,
            notifyUUID,
            bleDescriptorGetType
        )
    }

    /**
     * indicate
     */
    @Deprecated(message = "请使用BleDescriptorGetType参数方式",
        replaceWith = ReplaceWith(
            "indicate(BleDevice, String, String, BleDescriptorGetType, BleIndicateCallback)"
        )
    )
    fun indicate(bleDevice: BleDevice,
                 serviceUUID: String,
                 indicateUUID: String,
                 useCharacteristicDescriptor: Boolean = false,
                 bleIndicateCallback: BleIndicateCallback.() -> Unit) {
        indicate(
            bleDevice = bleDevice,
            serviceUUID = serviceUUID,
            indicateUUID = indicateUUID,
            bleDescriptorGetType = if (useCharacteristicDescriptor) {
                BleDescriptorGetType.CharacteristicDescriptor
            } else {
                BleDescriptorGetType.Default
            },
            bleIndicateCallback = bleIndicateCallback
        )
    }

    /**
     * indicate
     */
    fun indicate(bleDevice: BleDevice,
                 serviceUUID: String,
                 indicateUUID: String,
                 timeoutMillis: Long? = bleOptions?.operateMillisTimeOut,
                 bleDescriptorGetType: BleDescriptorGetType = BleDescriptorGetType.Default,
                 bleIndicateCallback: BleIndicateCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.indicate(
            bleDevice,
            serviceUUID,
            indicateUUID,
            timeoutMillis,
            bleDescriptorGetType,
            bleIndicateCallback
        )
    }

    /**
     * indicate
     */
    fun indicate(bleDevice: BleDevice,
                 serviceUUID: String,
                 indicateUUID: String,
                 bleDescriptorGetType: BleDescriptorGetType = BleDescriptorGetType.Default,
                 bleIndicateCallback: BleIndicateCallback.() -> Unit) {
        indicate(
            bleDevice,
            serviceUUID,
            indicateUUID,
            null,
            bleDescriptorGetType,
            bleIndicateCallback
        )
    }

    /**
     * stop indicate
     */
    @Deprecated(message = "请使用BleDescriptorGetType参数方式",
        replaceWith = ReplaceWith(
            "stopIndicate(BleDevice, String, String, BleDescriptorGetType)"
        )
    )
    fun stopIndicate(
        bleDevice: BleDevice,
        serviceUUID: String,
        indicateUUID: String,
        useCharacteristicDescriptor: Boolean = false
    ): Boolean? {
        return stopIndicate(
            bleDevice = bleDevice,
            serviceUUID = serviceUUID,
            indicateUUID = indicateUUID,
            bleDescriptorGetType = if (useCharacteristicDescriptor) {
                BleDescriptorGetType.CharacteristicDescriptor
            } else {
                BleDescriptorGetType.Default
            },
        )
    }

    /**
     * stop indicate
     */
    fun stopIndicate(
        bleDevice: BleDevice,
        serviceUUID: String,
        indicateUUID: String,
        bleDescriptorGetType: BleDescriptorGetType = BleDescriptorGetType.Default
    ): Boolean? {
        checkInitialize()
        return bleBaseRequest?.stopIndicate(
            bleDevice,
            serviceUUID,
            indicateUUID,
            bleDescriptorGetType
        )
    }

    /**
     * 读取信号值
     */
    fun readRssi(bleDevice: BleDevice, bleRssiCallback: BleRssiCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.readRssi(bleDevice, bleRssiCallback)
    }

    /**
     * 设置mtu
     */
    fun setMtu(bleDevice: BleDevice, bleMtuChangedCallback: BleMtuChangedCallback.() -> Unit) {
        setMtu(bleDevice, getOptions()?.mtu?: DEFAULT_MTU, bleMtuChangedCallback)
    }

    /**
     * 设置mtu
     */
    fun setMtu(bleDevice: BleDevice, mtu: Int, bleMtuChangedCallback: BleMtuChangedCallback.() -> Unit) {
        checkInitialize()
        if (mtu > 512) {
            BleLogger.w("requiredMtu should lower than 512 !")
        }

        if (mtu < 23) {
            BleLogger.w("requiredMtu should higher than 23 !")
        }
        bleBaseRequest?.setMtu(bleDevice, mtu, bleMtuChangedCallback)
    }

    /**
     * 设置设备的传输优先级
     * connectionPriority 必须是以下的其中一个
     * [BluetoothGatt.CONNECTION_PRIORITY_BALANCED] (默认)、
     * [BluetoothGatt.CONNECTION_PRIORITY_HIGH] (高优先级,低延迟,传输完请求设置
     * CONNECTION_PRIORITY_BALANCED,以减少能源使用)、
     * [BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER] (低功耗)
     *
     */
    fun setConnectionPriority(bleDevice: BleDevice, connectionPriority: Int): Boolean {
        checkInitialize()
        if (connectionPriority != BluetoothGatt.CONNECTION_PRIORITY_BALANCED &&
            connectionPriority != BluetoothGatt.CONNECTION_PRIORITY_HIGH &&
            connectionPriority != BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER) {
            return false
        }
        return bleBaseRequest?.setConnectionPriority(bleDevice, connectionPriority)?: false
    }

    /**
     * 读特征值数据
     */
    fun readData(bleDevice: BleDevice,
                 serviceUUID: String,
                 readUUID: String,
                 bleReadCallback: BleReadCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.readData(bleDevice, serviceUUID, readUUID, bleReadCallback)
    }

    /**
     * 写数据
     * 注意:因为分包后每一个包,可能是包含完整的协议,所以分包由业务层处理,组件只会根据包的长度和mtu值对比后是否拦截
     */
    fun writeData(bleDevice: BleDevice,
                  serviceUUID: String,
                  writeUUID: String,
                  data: ByteArray,
                  writeType: Int? = null,
                  bleWriteCallback: BleWriteCallback.() -> Unit) {
        writeData(
            bleDevice,
            serviceUUID,
            writeUUID,
            SparseArray<ByteArray>(1).apply {
                put(0, data)
            },
            writeType,
            bleWriteCallback
        )
    }

    /**
     * 写数据
     * 注意:因为分包后每一个包,可能是包含完整的协议,所以分包由业务层处理,组件只会根据包的长度和mtu值对比后是否拦截
     */
    fun writeData(bleDevice: BleDevice,
                  serviceUUID: String,
                  writeUUID: String,
                  dataArray: SparseArray<ByteArray>,
                  writeType: Int? = null,
                  bleWriteCallback: BleWriteCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.writeData(bleDevice, serviceUUID, writeUUID, dataArray, writeType, bleWriteCallback)
    }

    /**
     * OTA推荐此方法
     * 放入一个写队列,写成功,则从队列中取下一个数据,写失败,则重试[retryWriteCount]次
     * 与[writeData]的区别在于,[writeData]写成功,则从队列中取下一个数据,写失败,则不再继续写后面的数据
     * 注意:因为分包后每一个包,可能是包含完整的协议,所以分包由业务层处理,组件只会根据包的长度和mtu值对比后是否拦截
     *
     * @param skipErrorPacketData 是否跳过数据长度为0的数据包
     * @param retryWriteCount 写失败后重试的次数
     */
    fun writeQueueData(bleDevice: BleDevice,
                       serviceUUID: String,
                       writeUUID: String,
                       data: ByteArray,
                       skipErrorPacketData: Boolean = false,
                       retryWriteCount: Int = 0,
                       retryDelayTime: Long = 0L,
                       writeType: Int? = null,
                       bleWriteCallback: BleWriteCallback.() -> Unit) {
        writeQueueData(
            bleDevice,
            serviceUUID,
            writeUUID,
            SparseArray<ByteArray>(1).apply {
                put(0, data)
            },
            skipErrorPacketData,
            retryWriteCount,
            retryDelayTime,
            writeType,
            bleWriteCallback
        )
    }

    /**
     * OTA推荐此方法
     * 放入一个写队列,写成功,则从队列中取下一个数据,写失败,则重试[retryWriteCount]次
     * 与[writeData]的区别在于,[writeData]写成功,则从队列中取下一个数据,写失败,则不再继续写后面的数据
     * 注意:因为分包后每一个包,可能是包含完整的协议,所以分包由业务层处理,组件只会根据包的长度和mtu值对比后是否拦截
     *
     * @param skipErrorPacketData 是否跳过数据长度为0的数据包
     * @param retryWriteCount 写失败后重试的次数
     */
    fun writeQueueData(bleDevice: BleDevice,
                       serviceUUID: String,
                       writeUUID: String,
                       dataArray: SparseArray<ByteArray>,
                       skipErrorPacketData: Boolean = false,
                       retryWriteCount: Int = 0,
                       retryDelayTime: Long = 0L,
                       writeType: Int? = null,
                       bleWriteCallback: BleWriteCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.writeQueueData(
            bleDevice,
            serviceUUID,
            writeUUID,
            dataArray,
            skipErrorPacketData,
            retryWriteCount,
            retryDelayTime,
            writeType,
            bleWriteCallback
        )
    }

    /**
     * 取消写操作
     * 注意:不区分具体的特征值,因为不同的写方式和不同的写队列类型处理不一致。同个设备的写队列为Independent时,
     * 为导致写忙碌(这个时候根据特征值取消写队列没有意义);其他类型的写队列,是共用一个队列,而队列中的数据没有
     * 区分特征值,所以无法取消某个特征值的写操作。
     */
    fun cancelWriting(bleDevice: BleDevice) {
        checkInitialize()
        bleBaseRequest?.cancelWriting(bleDevice)
    }

    /**
     * 获取所有已连接设备集合(不包含其他应用连接的设备、系统连接的设备)
     */
    fun getAllConnectedDevice(): MutableList<BleDevice>? {
        checkInitialize()
        return bleBaseRequest?.getAllConnectedDevice()
    }

    /**
     * 获取系统已连接设备集合,确保已获取到权限
     */
    @SuppressLint("MissingPermission")
    fun getSystemAllConnectedDevice(): MutableList<BluetoothDevice>? {
        checkInitialize()
        if (!BleUtil.isPermission(application)) {
            return null
        }
        return bluetoothManager?.getConnectedDevices(BluetoothProfile.GATT)
    }

    /**
     * 添加设备的连接状态发生变化、indicate/notify收到数据、mtu改变的回调
     *  这个回调会独立存在,与[connect]的bleConnectCallback、[notify]的bleNotifyCallback、
     *  [indicate]的bleIndicateCallback、[setMtu]的bleMtuChangedCallback不冲突
     */
    fun addBleEventCallback(bleDevice: BleDevice, bleEventCallback: BleEventCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.addBleEventCallback(bleDevice, bleEventCallback)
    }

    /**
     * 移除该设备的连接回调
     */
    fun removeBleConnectCallback(bleDevice: BleDevice) {
        checkInitialize()
        bleBaseRequest?.removeBleConnectCallback(bleDevice)
    }

    /**
     * 替换该设备的连接回调
     */
    fun replaceBleConnectCallback(bleDevice: BleDevice, bleConnectCallback: BleConnectCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.replaceBleConnectCallback(bleDevice, bleConnectCallback)
    }

    /**
     * 替换该设备的连接回调
     */
    fun replaceBleConnectCallback(address: String, bleConnectCallback: BleConnectCallback.() -> Unit) {
        replaceBleConnectCallback(buildBleDeviceByDeviceAddress(address), bleConnectCallback)
    }

    /**
     * 移除该设备的Indicate回调
     */
    fun removeBleIndicateCallback(bleDevice: BleDevice, indicateUUID: String) {
        checkInitialize()
        bleBaseRequest?.removeBleIndicateCallback(bleDevice, indicateUUID)
    }

    /**
     * 移除该设备的Notify回调
     */
    fun removeBleNotifyCallback(bleDevice: BleDevice, notifyUUID: String) {
        checkInitialize()
        bleBaseRequest?.removeBleNotifyCallback(bleDevice, notifyUUID)
    }

    /**
     * 移除该设备的Read回调
     */
    fun removeBleReadCallback(bleDevice: BleDevice, readUUID: String) {
        checkInitialize()
        bleBaseRequest?.removeBleReadCallback(bleDevice, readUUID)
    }

    /**
     * 移除该设备的MtuChanged回调
     */
    fun removeBleMtuChangedCallback(bleDevice: BleDevice) {
        checkInitialize()
        bleBaseRequest?.removeBleMtuChangedCallback(bleDevice)
    }

    /**
     * 移除该设备的Rssi回调
     */
    fun removeBleRssiCallback(bleDevice: BleDevice) {
        checkInitialize()
        bleBaseRequest?.removeBleRssiCallback(bleDevice)
    }

    /**
     * 移除该设备的Write回调
     * bleWriteCallback为空,则会移除writeUUID下的所有callback
     */
    fun removeBleWriteCallback(bleDevice: BleDevice,
                               writeUUID: String,
                               bleWriteCallback: BleWriteCallback? = null
    ) {
        checkInitialize()
        bleBaseRequest?.removeBleWriteCallback(bleDevice, writeUUID, bleWriteCallback)
    }

    /**
     * 移除该设备的Scan回调
     */
    @Synchronized
    fun removeBleScanCallback() {
        checkInitialize()
        bleBaseRequest?.removeBleScanCallback()
    }

    /**
     * 添加一个新的扫描回调
     */
    fun addBleScanCallback(bleScanCallback: BleScanCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.addBleScanCallback(bleScanCallback)
    }

    /**
     * 移除该设备回调,BleConnectCallback除外
     */
    fun removeAllCharacterCallback(bleDevice: BleDevice) {
        checkInitialize()
        bleBaseRequest?.removeAllCharacterCallback(bleDevice)
    }

    /**
     * 移除该设备Event回调
     */
    fun removeBleEventCallback(bleDevice: BleDevice) {
        checkInitialize()
        bleBaseRequest?.removeBleEventCallback(bleDevice)
    }

    /**
     * 断开所有设备的连接,先回调状态,再close
     */
    @Synchronized
    fun disConnectAll() {
        checkInitialize()
        bleBaseRequest?.disConnectAll()
    }

    /**
     * 断开所有连接 释放资源
     */
    @Synchronized
    fun closeAll() {
        checkInitialize()
        bleBaseRequest?.closeAll()
        application = null
        bleOptions = null
        bluetoothManager = null
        bleBaseRequest = null
        BleLogger.i("资源释放完毕,BleCore SDK退出")
    }

    /**
     * 注册系统蓝牙广播
     */
    fun registerBluetoothStateReceiver(bluetoothCallback: BluetoothCallback.() -> Unit) {
        checkInitialize()
        bleBaseRequest?.registerBluetoothStateReceiver(bluetoothCallback)
    }

    /**
     * 取消注册系统蓝牙广播
     */
    fun unRegisterBluetoothStateReceiver() {
        checkInitialize()
        bleBaseRequest?.unRegisterBluetoothStateReceiver()
    }

    /**
     * 断开某个设备的连接 释放资源
     */
    @Synchronized
    fun close(bleDevice: BleDevice) {
        checkInitialize()
        bleBaseRequest?.close(bleDevice)
        BleLogger.i("${bleDevice}资源释放完毕")
    }

    fun getOptions() = bleOptions

    fun getContext() = application

    fun getBluetoothManager() = bluetoothManager

    private fun checkInitialize() {
        if (bleBaseRequest == null) {
            BleLogger.e("未初始化,请调用BleManager.init()")
        }
    }

    /**
     * 通过设备地址构建BleDevice对象,确保已获取到权限
     */
    @SuppressLint("MissingPermission")
    fun buildBleDeviceByDeviceAddress(deviceAddress: String): BleDevice {
        val deviceInfo = bluetoothManager?.adapter?.getRemoteDevice(deviceAddress)
        return BleDevice(
            deviceInfo,
            deviceInfo?.name?: "",
            deviceAddress,
            0,
            0,
            null,
            null
        )
    }
}

================================================
FILE: ble/src/main/java/com/bhm/ble/attribute/BleOptions.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.ble.attribute

import com.bhm.ble.data.BleTaskQueueType
import com.bhm.ble.data.Constants.AUTO_CONNECT
import com.bhm.ble.data.Constants.CONTAIN_SCAN_DEVICE_NAME
import com.bhm.ble.data.Constants.DEFAULT_AUTO_SET_MTU
import com.bhm.ble.data.Constants.DEFAULT_CONNECT_MILLIS_TIMEOUT
import com.bhm.ble.data.Constants.DEFAULT_CONNECT_RETRY_COUNT
import com.bhm.ble.data.Constants.DEFAULT_CONNECT_RETRY_INTERVAL
import com.bhm.ble.data.Constants.DEFAULT_MAX_CONNECT_NUM
import com.bhm.ble.data.Constants.DEFAULT_MTU
import com.bhm.ble.data.Constants.DEFAULT_OPERATE_INTERVAL
import com.bhm.ble.data.Constants.DEFAULT_OPERATE_MILLIS_TIMEOUT
import com.bhm.ble.data.Constants.DEFAULT_SCAN_MILLIS_TIMEOUT
import com.bhm.ble.data.Constants.DEFAULT_SCAN_RETRY_COUNT
import com.bhm.ble.data.Constants.DEFAULT_SCAN_RETRY_INTERVAL
import com.bhm.ble.data.Constants.DEFAULT_TASK_QUEUE_TYPE
import com.bhm.ble.data.Constants.ENABLE_LOG
import com.bhm.ble.data.Constants.SCAN_NEED_CHECK_GPS
import com.bhm.ble.data.Constants.STOP_SCAN_WHEN_START_CONNECT


/**
 * 配置项
 *
 * @author Buhuiming
 * @date 2023年05月18日 15时04分
 */
class BleOptions private constructor(builder: Builder) {

    var scanServiceUuids = builder.scanServiceUuids

    var scanDeviceNames = builder.scanDeviceNames

    var scanDeviceAddresses = builder.scanDeviceAddresses

    var containScanDeviceName = builder.containScanDeviceName

    var autoConnect = builder.autoConnect

    var enableLog = builder.enableLog

    var scanMillisTimeOut = builder.scanMillisTimeOut

    var scanRetryCount = builder.scanRetryCount

    var scanRetryInterval = builder.scanRetryInterval

    var connectMillisTimeOut = builder.connectMillisTimeOut

    var connectRetryCount = builder.connectRetryCount

    var connectRetryInterval = builder.connectRetryInterval

    var operateMillisTimeOut = builder.operateMillisTimeOut

    var operateInterval = builder.operateInterval

    var maxConnectNum = builder.maxConnectNum

    var mtu = builder.mtu

    var autoSetMtu = builder.autoSetMtu

    var taskQueueType = builder.taskQueueType

    var stopScanWhenStartConnect = builder.stopScanWhenStartConnect

    var needCheckGps = builder.needCheckGps

    companion object {

        @JvmStatic
        fun getDefaultBleOptions() : BleOptions = BleOptions(Builder())

        @JvmStatic
        fun builder() = Builder()
    }

    class Builder {

        internal var scanServiceUuids: ArrayList<String> = ArrayList(1)

        internal var scanDeviceNames: ArrayList<String> = ArrayList(1)

        internal var scanDeviceAddresses: ArrayList<String> = ArrayList(1)

        internal var containScanDeviceName = CONTAIN_SCAN_DEVICE_NAME

        internal var autoConnect = AUTO_CONNECT

        internal var enableLog = ENABLE_LOG

        internal var scanMillisTimeOut: Long = DEFAULT_SCAN_MILLIS_TIMEOUT

        internal var scanRetryCount: Int = DEFAULT_SCAN_RETRY_COUNT

        internal var scanRetryInterval: Long = DEFAULT_SCAN_RETRY_INTERVAL

        internal var connectMillisTimeOut: Long = DEFAULT_CONNECT_MILLIS_TIMEOUT

        internal var connectRetryCount: Int = DEFAULT_CONNECT_RETRY_COUNT

        internal var connectRetryInterval: Long = DEFAULT_CONNECT_RETRY_INTERVAL

        internal var operateMillisTimeOut: Long = DEFAULT_OPERATE_MILLIS_TIMEOUT

        internal var operateInterval: Long = DEFAULT_OPERATE_INTERVAL

        internal var maxConnectNum: Int = DEFAULT_MAX_CONNECT_NUM

        internal var mtu: Int = DEFAULT_MTU

        internal var autoSetMtu: Boolean = DEFAULT_AUTO_SET_MTU

        internal var taskQueueType: BleTaskQueueType = DEFAULT_TASK_QUEUE_TYPE

        internal var stopScanWhenStartConnect = STOP_SCAN_WHEN_START_CONNECT

        internal var needCheckGps = SCAN_NEED_CHECK_GPS

        /**
         * 设置扫描过滤规则:只查询对应ServiceUuid的设备
         */
        fun setScanServiceUuid(vararg scanServiceUuids: String) = apply {
            scanServiceUuids.forEach {
                if (it.isNotEmpty()) {
                    this.scanServiceUuids.add(it)
                }
            }
        }

        /**
         * 设置扫描过滤规则:只查询对应设备名的设备
         */
        fun setScanDeviceName(vararg scanDeviceNames: String) = apply {
            scanDeviceNames.forEach {
                if (it.isNotEmpty()) {
                    this.scanDeviceNames.add(it)
                }
            }
        }

        /**
         * 设置扫描过滤规则:只查询对应设备Mac的设备
         */
        fun setScanDeviceAddress(vararg scanDeviceAddresses: String) = apply {
            scanDeviceAddresses.forEach {
                if (it.isNotEmpty()) {
                    this.scanDeviceAddresses.add(it)
                }
            }
        }

        /**
         * 设置扫描过滤规则:是否模糊匹配设备名,默认[CONTAIN_SCAN_DEVICE_NAME]
         */
        fun isContainScanDeviceName(containScanDeviceName: Boolean) = apply {
            this.containScanDeviceName = containScanDeviceName
        }

        /**
         * 非主动断开后是否自动连接,默认为[AUTO_CONNECT]
         */
        @Deprecated(message = "请在业务层处理自动重连,autoConnect设计的初衷是为了断开重连,利用bluetoothGatt" +
                "的重连,但BleCore在断开连接或连接失败后,bluetoothGatt会被close掉释放资源,bluetoothGatt的重连" +
                "不再作用,此函数将会被删除",
            replaceWith = ReplaceWith(
                "BleConnectCallback.onDisConnected、BleManager.connect"
            )
        )
        fun setAutoConnect(autoConnect: Boolean) = apply {
            this.autoConnect = autoConnect
        }

        /**
         * 扫描超时时间,单位毫秒,默认为[DEFAULT_SCAN_MILLIS_TIMEOUT]
         */
        fun setScanMillisTimeOut(scanMillisTimeOut: Long) = apply {
            this.scanMillisTimeOut = scanMillisTimeOut
        }

        /**
         * 这个机制是:不会因为扫描的次数导致上一次扫描到的数据被清空,也就是onScanStart和onScanComplete
         * 都只会回调一次,而且扫描到的数据是所有扫描次数的总和
         * 设置扫描重试次数,默认为[DEFAULT_SCAN_RETRY_COUNT]次,总扫描次数=scanRetryCount+1次
         * 设置扫描重试间隔,默认为[DEFAULT_SCAN_RETRY_INTERVAL]
         */
        fun setScanRetryCountAndInterval(scanRetryCount: Int, scanRetryInterval: Long) = apply {
            this.scanRetryCount = scanRetryCount
            this.scanRetryInterval = scanRetryInterval
        }

        /**
         * 默认打开库中的运行日志 默认[ENABLE_LOG]
         */
        fun setEnableLog(enableLog: Boolean) = apply {
            this.enableLog = enableLog
        }

        /**
         * 连接超时时间,单位毫秒,默认为[DEFAULT_CONNECT_MILLIS_TIMEOUT]
         */
        fun setConnectMillisTimeOut(connectMillisTimeOut: Long) = apply {
            this.connectMillisTimeOut = connectMillisTimeOut
        }

        /**
         * 设置连接重试次数,默认为[DEFAULT_CONNECT_RETRY_COUNT]次
         * 设置连接重试间隔,单位毫秒,默认为[DEFAULT_CONNECT_RETRY_INTERVAL]
         */
        fun setConnectRetryCountAndInterval(connectRetryCount: Int, connectRetryInterval: Long) = apply {
            this.connectRetryCount = connectRetryCount
            this.connectRetryInterval = connectRetryInterval
        }

        /**
         * 设置readRssi、setMtu、write、read、notify、indicate的超时时间,
         * 单位毫秒,默认为[DEFAULT_OPERATE_MILLIS_TIMEOUT]
         */
        fun setOperateMillisTimeOut(operateMillisTimeOut: Long) = apply {
            this.operateMillisTimeOut = operateMillisTimeOut
        }

        /**
         * 设置操作之间的间隔,单位毫秒,默认为[DEFAULT_OPERATE_INTERVAL]
         */
        fun setOperateInterval(operateInterval: Long) = apply {
            this.operateInterval = operateInterval
        }

        /**
         * 设置最大连接数,默认为[DEFAULT_MAX_CONNECT_NUM]
         */
        fun setMaxConnectNum(maxConnectNum: Int) = apply {
            this.maxConnectNum = maxConnectNum
        }

        /**
         * 设置mtu,默认为[DEFAULT_MTU]
         */
        fun setMtu(mtu: Int) = apply {
            setMtu(mtu, DEFAULT_AUTO_SET_MTU)
        }

        /**
         * 设置mtu,默认为[DEFAULT_MTU]
         * @param autoSetMtu 是否自动设置mtu,true:连接成功之后会自动设置mtu,默认为[DEFAULT_AUTO_SET_MTU]
         */
        fun setMtu(mtu: Int, autoSetMtu: Boolean) = apply {
            this.mtu = mtu
            this.autoSetMtu = autoSetMtu
        }

        /**
         * 设置任务队列类型,默认为[BleTaskQueueType.Default],设置完需断开所有设备才可生效
         * [BleTaskQueueType.Default] 一个设备的Notify\Indicate\Read\Write\mtu操作所对应的
         * 任务共享同一个任务队列(共享队列)(不区分特征值),rssi在rssi队列
         *
         * [BleTaskQueueType.Operate] 一个设备每个操作独立一个任务队列(不区分特征值)
         * Notify在Notify队列中,Indicate在Indicate队列中,Read在Read队列中,
         * Write在Write队列中,mtu在共享队列,rssi在rssi队列中,
         * 不同操作任务之间相互不影响,相同操作任务之间先进先出按序执行
         * 例如特征值1的写操作和特征值2的写操作,在同一个任务队列当中;特征值1的写操作和特征值1的读操作,
         * 在两个不同的任务队列当中,特征值1的读操作和特征值2的写操作,在两个不同的任务队列当中。
         *
         * [BleTaskQueueType.Independent] 一个设备每个特征值下的每个操作独立一个任务队列(区分特征值)
         * Notify\Indicate\Read\Write所对应的任务分别放入到独立的任务队列中,
         * mtu在共享队列,rssi在rssi队列中,
         * 且按特征值区分,不同操作任务之间相互不影响,相同操作任务之间相互不影响
         * 例如特征值1的写操作和特征值2的写操作,在两个不同的任务队列当中;特征值1的写操作和特征值1的读操作,
         * 在两个不同的任务队列当中,特征值1的读操作和特征值2的写操作,在两个不同的任务队列当中。
         */
        fun setTaskQueueType(taskQueueType: BleTaskQueueType) = apply {
            this.taskQueueType = taskQueueType
        }

        /**
         * 当连接设备时是否停止扫描,默认为[STOP_SCAN_WHEN_START_CONNECT]
         * 注意:该参数对startScanAndConnect方法无效
         */
        fun setStopScanWhenStartConnect(stopScanWhenStartConnect: Boolean) = apply {
            this.stopScanWhenStartConnect = stopScanWhenStartConnect
        }

        /**
         * 设置是否需要打开GPS,默认为[SCAN_NEED_CHECK_GPS]
         *  有些设备不具备GPS模块,如果需要打开或关闭GPS,请调用该方法设置
         */
        fun setNeedCheckGps(needCheckGps: Boolean) = apply {
            this.needCheckGps = needCheckGps
        }

        fun build(): BleOptions {
            return BleOptions(this)
        }
    }
}

================================================
FILE: ble/src/main/java/com/bhm/ble/callback/BleBaseCallback.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.ble.callback

import com.bhm.ble.request.base.BleRequestImp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch


/**
 * 回调的基类
 *
 * @author Buhuiming
 * @date 2023年05月26日 16时04分
 */
open class BleBaseCallback {

    private val mainScope = BleRequestImp.get().getMainScope()

    private val ioScope = BleRequestImp.get().getIOScope()

    private val defaultScope = BleRequestImp.get().getDefaultScope()

    private var key: String? = null

    fun launchInMainThread(block: suspend CoroutineScope.() -> Unit): Job {
        return mainScope.launch {
            block.invoke(this)
        }
    }

    fun launchInIOThread(block: suspend CoroutineScope.() -> Unit): Job {
        return ioScope.launch {
            block.invoke(this)
        }
    }

    fun launchInDefaultThread(block: suspend CoroutineScope.() -> Unit): Job {
        return defaultScope.launch {
            block.invoke(this)
        }
    }

    open fun setKey(key: String) {
        this.key = key
    }

    open fun getKey() = key
}

================================================
FILE: ble/src/main/java/com/bhm/ble/callback/BleConnectCallback.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.ble.callback

import android.bluetooth.BluetoothGatt
import com.bhm.ble.BleManager
import com.bhm.ble.data.BleConnectFailType
import com.bhm.ble.device.BleDevice
import com.bhm.ble.log.BleLogger
import kotlinx.coroutines.delay


/**
 * Ble连接回调
 * 在某些型号手机上,connectGatt必须在主线程才能有效,所以把连接过程放在主线程,回调也在主线程
 * @author Buhuiming
 * @date 2023年05月24日 14时00分
 */
open class BleConnectCallback : BleBaseCallback() {

    private var start: ((bleDevice: BleDevice) -> Unit)? = null

    private var connectSuccess: ((bleDevice: BleDevice, gatt: BluetoothGatt?) -> Unit)? = null

    private var connectFail: ((bleDevice: BleDevice, connectFailType: BleConnectFailType) -> Unit)? = null

    private var disConnecting: ((isActiveDisConnected: Boolean, bleDevice: BleDevice,
                                gatt: BluetoothGatt?, status: Int) -> Unit)? = null

    private var disConnected: ((isActiveDisConnected: Boolean, bleDevice: BleDevice,
                                gatt: BluetoothGatt?, status: Int) -> Unit)? = null

    /**
     * 开始连接
     */
    fun onConnectStart(value: (bleDevice: BleDevice) -> Unit) {
        start = value
    }

    /**
     * 连接成功
     */
    fun onConnectSuccess(value: (bleDevice: BleDevice, gatt: BluetoothGatt?) -> Unit) {
        connectSuccess = value
    }

    /**
     * 连接失败
     */
    fun onConnectFail(value: (bleDevice: BleDevice, connectFailType: BleConnectFailType) -> Unit) {
        connectFail = value
    }

    /**
     * 触发断开,此时的设备有可能还是连接状态,未完全断开
     */
    fun onDisConnecting(value: (isActiveDisConnected: Boolean, bleDevice: BleDevice,
                               gatt: BluetoothGatt?, status: Int) -> Unit) {
        disConnecting = value
    }

    /**
     * 连接断开,特指连接后再断开的情况。在这里可以监控设备的连接状态,一旦连接断开,可以根据自身情况考虑对BleDevice
     * 对象进行重连操作。需要注意的是,断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况。此外,
     * 如果通过调用[com.bhm.ble.BleManager.disConnect]方法,主动断开蓝牙连接的结果也会在这个方法中回调,
     * 此时isActiveDisConnected将会是true。
     */
    fun onDisConnected(value: (isActiveDisConnected: Boolean, bleDevice: BleDevice,
                               gatt: BluetoothGatt?, status: Int) -> Unit) {
        disConnected = value
    }

    open fun callConnectStart(bleDevice: BleDevice) {
        launchInMainThread {
            start?.invoke(bleDevice)
        }
    }

    open fun callConnectFail(bleDevice: BleDevice, connectFailType: BleConnectFailType) {
        launchInMainThread {
            connectFail?.invoke(bleDevice, connectFailType)
        }
    }

    open fun callConnectSuccess(bleDevice: BleDevice, gatt: BluetoothGatt?) {
        launchInMainThread {
            connectSuccess?.invoke(bleDevice, gatt)
        }
    }

    open fun callDisConnecting(isActiveDisConnected: Boolean, bleDevice: BleDevice,
                              gatt: BluetoothGatt?, status: Int) {
        launchInMainThread {
            disConnecting?.invoke(isActiveDisConnected, bleDevice, gatt, status)
        }
        callDisConnected(isActiveDisConnected, bleDevice, gatt, status)
    }

    open fun callDisConnected(isActiveDisConnected: Boolean, bleDevice: BleDevice,
                              gatt: BluetoothGatt?, status: Int) {
        launchInMainThread {
            val start = System.currentTimeMillis()
            while (BleManager.get().isConnected(bleDevice, true)) {
                //主动断开,需要等待gatt释放的时间更长一些
                delay(if (isActiveDisConnected) 80 else 4)
            }
            val end = System.currentTimeMillis()
            BleLogger.i("触发onDisConnecting,${(end - start)}毫秒后触发onDisConnected")
            disConnected?.invoke(isActiveDisConnected, bleDevice, gatt, status)
        }
    }
}

================================================
FILE: ble/src/main/java/com/bhm/ble/callback/BleEventCallback.kt
================================================
/*
 * Copyright (c) 2022-2032 buhuiming
 * 不能修改和删除上面的版权声明
 * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制
 */
package com.bhm.ble.callback

import android.bluetooth.BluetoothGatt
import com.bhm.ble.BleManager
import com.bhm.ble.data.BleConnectFailType
import com.bhm.ble.device.BleDevice
import com.bhm.ble.log.BleLogger
import kotlinx.coroutines.delay

/**
 * @description 这是个独立的回调,设备的连接状态发生变化、
 * indicate/notify收到数据、mtu改变会触发
 * 不影响其他callback的回调
 * @author Buhuiming
 * @date 2023年10月31日 15:07:38
 */
open class BleEventCallback : BleBaseCallback() {

    private var startConnect: ((bleDevice: BleDevice) -> Unit)? = null

    private var connectFail: ((bleDevice: BleDevice, connectFailType: BleConnectFailType) -> Unit)? = null

    private var disConnecting: ((isActiveDisConnected: Boolean, bleDevice: BleDevice,
                                 gatt: BluetoothGatt?, status: Int) -> Unit)? = null

    private var connected: ((bleDevice: BleDevice, gatt: BluetoothGatt?) -> Unit)? = null

    private var disConnected: ((isActiveDisConnected: Boolean, bleDevice: BleDevice,
                                gatt: BluetoothGatt?, status: Int) -> Unit)? = null

    private var characteristicChanged: ((uuid: String?, type: Int, bleDevice: BleDevice, data: ByteArray) -> Unit)? = null

    private var mtuChanged: ((mtu: Int, bleDevice: BleDevice) -> Unit)? = null

    /**
     * 已连接
     */
    fun onConnected(value: (bleDevice: BleDevice, gatt: BluetoothGatt?) -> Unit) {
        connected = value
    }

    /**
     * 收到数据
     * type = 1 notify方式;type = 2 indicate方式
     */
    fun onCharacteristicChanged(value: ((uuid: String?, type: Int, bleDevice: BleDevice, data: ByteArray) -> Unit)) {
        characteristicChanged = value
    }

    fun onMtuChanged(value: ((mtu: Int, bleDevice: BleDevice) -> Unit)) {
        mtuChanged = value
    }

    /**
     * 开始连接
     */
    fun onConnectStart(value: (bleDevice: BleDevice) -> Unit) {
        startConnect = value
    }

    /**
     * 连接失败
     */
    fun onConnectFail(value: (bleDevice: BleDevice, connectFailType: BleConnectFailType) -> Unit) {
        connectFail = value
    }

    /**
     * 触发断开,此时的设备有可能还是连接状态,未完全断开
     */
    fun onDisConnecting(value: (isActiveDisConnected: Boolean, bleDevice: BleDevice,
                                gatt: BluetoothGatt?, status: Int) -> Unit) {
        disConnecting = value
    }

    /**
     * 连接断开,特指连接后再断开的情况。在这里可以监控设备的连接状态,一旦连接断开,可以根据自身情况考虑对BleDevice
     * 对象进行重连操作。需要注意的是,断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况。此外,
     * 如果通过调用[com.bhm.ble.BleManager.disConnect]方法,主动断开蓝牙连接的结果也会在这个方法中回调,
     * 此时isActiveDisConnected将会是true。
     */
    fun onDisConnected(value: (isActiveDisConnected: Boolean, bleDevice: BleDevice,
                               gatt: BluetoothGatt?, status: Int) -> Unit) {
        disConnected = value
    }

    open fun callConnectStart(bleDevice: BleDevice) {
        launchInMainThread {
            startConnect?.invoke(bleDevice)
        }
    }

    open fun callConnectFail(bleDevice: BleDevice, connectFailType: BleConnectFailType) {
        launchInMainThread {
            connectFail?.invoke(bleDevice, connectFailType)
        }
    }

    open fun callDisConnecting(isActiveDisConnected: Boolean, bleDevice: BleDevice,
                               gatt: Blu
Download .txt
gitextract_p5rasqxh/

├── .gitignore
├── README.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── com/
│       │           └── bhm/
│       │               └── demo/
│       │                   └── ExampleInstrumentedTest.kt
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── bhm/
│       │   │           └── demo/
│       │   │               ├── BaseActivity.kt
│       │   │               ├── adapter/
│       │   │               │   ├── DetailsExpandAdapter.kt
│       │   │               │   ├── DeviceListAdapter.kt
│       │   │               │   └── LoggerListAdapter.kt
│       │   │               ├── constants/
│       │   │               │   └── Constants.kt
│       │   │               ├── entity/
│       │   │               │   ├── CharacteristicNode.kt
│       │   │               │   ├── LogEntity.kt
│       │   │               │   ├── OperateType.kt
│       │   │               │   ├── RefreshBleDevice.kt
│       │   │               │   └── ServiceNode.kt
│       │   │               ├── ui/
│       │   │               │   ├── DetailOperateActivity.kt
│       │   │               │   ├── MainActivity.kt
│       │   │               │   └── OptionSettingActivity.kt
│       │   │               └── vm/
│       │   │                   ├── DetailViewModel.kt
│       │   │                   └── MainViewModel.kt
│       │   └── res/
│       │       ├── drawable/
│       │       │   └── ic_launcher_background.xml
│       │       ├── drawable-v24/
│       │       │   └── ic_launcher_foreground.xml
│       │       ├── layout/
│       │       │   ├── activity_detail.xml
│       │       │   ├── activity_main.xml
│       │       │   ├── activity_setting.xml
│       │       │   ├── layout_recycler_characteristic.xml
│       │       │   ├── layout_recycler_item.xml
│       │       │   ├── layout_recycler_log.xml
│       │       │   └── layout_recycler_service.xml
│       │       ├── values/
│       │       │   ├── colors.xml
│       │       │   ├── strings.xml
│       │       │   └── themes.xml
│       │       ├── values-night/
│       │       │   └── themes.xml
│       │       └── xml/
│       │           ├── backup_rules.xml
│       │           └── data_extraction_rules.xml
│       └── test/
│           └── java/
│               └── com/
│                   └── bhm/
│                       └── demo/
│                           └── ExampleUnitTest.kt
├── ble/
│   ├── .gitignore
│   ├── build.gradle
│   ├── consumer-rules.pro
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           └── java/
│               └── com/
│                   └── bhm/
│                       └── ble/
│                           ├── BleManager.kt
│                           ├── attribute/
│                           │   └── BleOptions.kt
│                           ├── callback/
│                           │   ├── BleBaseCallback.kt
│                           │   ├── BleConnectCallback.kt
│                           │   ├── BleEventCallback.kt
│                           │   ├── BleIndicateCallback.kt
│                           │   ├── BleMtuChangedCallback.kt
│                           │   ├── BleNotifyCallback.kt
│                           │   ├── BleReadCallback.kt
│                           │   ├── BleRssiCallback.kt
│                           │   ├── BleScanCallback.kt
│                           │   ├── BleWriteCallback.kt
│                           │   └── BluetoothCallback.kt
│                           ├── control/
│                           │   ├── BleLruHashMap.kt
│                           │   ├── BleTask.kt
│                           │   ├── BleTaskList.kt
│                           │   └── BleTaskQueue.kt
│                           ├── data/
│                           │   ├── BleConnectFailType.kt
│                           │   ├── BleConnectLastState.kt
│                           │   ├── BleDescriptorGetType.kt
│                           │   ├── BleException.kt
│                           │   ├── BleScanFailType.kt
│                           │   ├── BleTaskQueueType.kt
│                           │   ├── BleWriteData.kt
│                           │   ├── BleWriteQueueData.kt
│                           │   └── Constants.kt
│                           ├── device/
│                           │   ├── BleConnectedDevice.kt
│                           │   ├── BleConnectedDeviceManager.kt
│                           │   └── BleDevice.kt
│                           ├── log/
│                           │   ├── BleLogEvent.kt
│                           │   ├── BleLogLevel.kt
│                           │   ├── BleLogManager.kt
│                           │   └── BleLogger.kt
│                           ├── receiver/
│                           │   └── BluetoothReceiver.kt
│                           ├── request/
│                           │   ├── BleConnectRequest.kt
│                           │   ├── BleIndicateRequest.kt
│                           │   ├── BleMtuRequest.kt
│                           │   ├── BleNotifyRequest.kt
│                           │   ├── BleReadRequest.kt
│                           │   ├── BleRssiRequest.kt
│                           │   ├── BleScanRequest.kt
│                           │   ├── BleSetPriorityRequest.kt
│                           │   ├── BleWriteRequest.kt
│                           │   └── base/
│                           │       ├── BleBaseRequest.kt
│                           │       ├── BleRequestImp.kt
│                           │       ├── BleTaskQueueRequest.kt
│                           │       └── Request.kt
│                           └── utils/
│                               └── BleUtil.kt
├── build.gradle
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── maven_upload.gradle
├── settings.gradle
└── support/
    ├── .gitignore
    ├── build.gradle
    ├── consumer-rules.pro
    ├── proguard-rules.pro
    └── src/
        └── main/
            ├── AndroidManifest.xml
            ├── java/
            │   └── com/
            │       └── bhm/
            │           └── support/
            │               └── sdk/
            │                   ├── common/
            │                   │   ├── BaseActivity.kt
            │                   │   ├── BaseApplication.kt
            │                   │   ├── BaseFragment.kt
            │                   │   ├── BaseVBActivity.kt
            │                   │   ├── BaseVBFragment.kt
            │                   │   ├── BaseViewModel.kt
            │                   │   └── DefaultViewPagerAdapter.kt
            │                   ├── constants/
            │                   │   ├── AppConstants.kt
            │                   │   └── PermissionConstants.kt
            │                   ├── core/
            │                   │   ├── AppTheme.kt
            │                   │   ├── GlideCircleTransform.kt
            │                   │   ├── GlideRoundTransform.kt
            │                   │   ├── GridSpacingItemDecoration.kt
            │                   │   ├── MyStaggeredGridLayoutManager.kt
            │                   │   └── WeakHandler.kt
            │                   ├── entity/
            │                   │   └── MessageEvent.kt
            │                   ├── utils/
            │                   │   ├── ActivityUtil.kt
            │                   │   ├── DateUtil.kt
            │                   │   ├── DisplayUtil.kt
            │                   │   ├── NotificationUtil.kt
            │                   │   ├── SPUtil.kt
            │                   │   └── ViewUtil.kt
            │                   └── widget/
            │                       └── ChoseView.kt
            └── res/
                └── values/
                    ├── attrs.xml
                    ├── colors.xml
                    └── ids.xml
Condensed preview — 131 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (569K chars).
[
  {
    "path": ".gitignore",
    "chars": 1877,
    "preview": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor."
  },
  {
    "path": "README.md",
    "chars": 15400,
    "preview": "# BleCore Android蓝牙低功耗(BLE)快速开发框架\n\n## 本项目持续维护更新\n\n*   如果觉得对您有帮助,可以犒劳一下作者\n*   ![1744788695450](https://github.com/user-att"
  },
  {
    "path": "app/.gitignore",
    "chars": 6,
    "preview": "/build"
  },
  {
    "path": "app/build.gradle",
    "chars": 1217,
    "preview": "plugins {\n    id 'com.android.application'\n    id 'org.jetbrains.kotlin.android'\n}\n\nandroid {\n    namespace 'com.bhm.dem"
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "app/src/androidTest/java/com/bhm/demo/ExampleInstrumentedTest.kt",
    "chars": 650,
    "preview": "package com.bhm.demo\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.A"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 1143,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmln"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/BaseActivity.kt",
    "chars": 6154,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo\n\n"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/adapter/DetailsExpandAdapter.kt",
    "chars": 6215,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo.a"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/adapter/DeviceListAdapter.kt",
    "chars": 2450,
    "preview": "package com.bhm.demo.adapter\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.core.cont"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/adapter/LoggerListAdapter.kt",
    "chars": 1698,
    "preview": "package com.bhm.demo.adapter\n\nimport android.graphics.Color\nimport android.view.LayoutInflater\nimport android.view.ViewG"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/constants/Constants.kt",
    "chars": 633,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo.c"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/entity/CharacteristicNode.kt",
    "chars": 987,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo.e"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/entity/LogEntity.kt",
    "chars": 342,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo.e"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/entity/OperateType.kt",
    "chars": 483,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo.e"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/entity/RefreshBleDevice.kt",
    "chars": 309,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo.e"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/entity/ServiceNode.kt",
    "chars": 679,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo.e"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/ui/DetailOperateActivity.kt",
    "chars": 9987,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo.u"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/ui/MainActivity.kt",
    "chars": 8673,
    "preview": "package com.bhm.demo.ui\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.view.View\ni"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/ui/OptionSettingActivity.kt",
    "chars": 10029,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo.u"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/vm/DetailViewModel.kt",
    "chars": 10559,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.demo.v"
  },
  {
    "path": "app/src/main/java/com/bhm/demo/vm/MainViewModel.kt",
    "chars": 11062,
    "preview": "package com.bhm.demo.vm\n\nimport android.app.Application\nimport android.content.Intent\nimport android.provider.Settings\ni"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "chars": 6142,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        androi"
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "chars": 1778,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        xmlns:aapt=\"http://schemas.android.com/aapt\"\n"
  },
  {
    "path": "app/src/main/res/layout/activity_detail.xml",
    "chars": 5769,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.appcompat.widget.LinearLayoutCompat xmlns:android=\"http://schemas.andro"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "chars": 4048,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.appcompat.widget.LinearLayoutCompat xmlns:android=\"http://schemas.andro"
  },
  {
    "path": "app/src/main/res/layout/activity_setting.xml",
    "chars": 22709,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.appcompat.widget.LinearLayoutCompat xmlns:android=\"http://schemas.andro"
  },
  {
    "path": "app/src/main/res/layout/layout_recycler_characteristic.xml",
    "chars": 3865,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        "
  },
  {
    "path": "app/src/main/res/layout/layout_recycler_item.xml",
    "chars": 2273,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.appcompat.widget.LinearLayoutCompat xmlns:android=\"http://schemas.andro"
  },
  {
    "path": "app/src/main/res/layout/layout_recycler_log.xml",
    "chars": 1051,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.appcompat.widget.LinearLayoutCompat xmlns:android=\"http://schemas.andro"
  },
  {
    "path": "app/src/main/res/layout/layout_recycler_service.xml",
    "chars": 1960,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.appcompat.widget.LinearLayoutCompat xmlns:android=\"http://schemas.andro"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 277,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"teal_200\">#FF03DAC5</color>\n    <color name=\"teal_70"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 69,
    "preview": "<resources>\n    <string name=\"app_name\">BleCore</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "chars": 772,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Base application theme. -->\n    <style name=\"Theme.B"
  },
  {
    "path": "app/src/main/res/values-night/themes.xml",
    "chars": 772,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Base application theme. -->\n    <style name=\"Theme.B"
  },
  {
    "path": "app/src/main/res/xml/backup_rules.xml",
    "chars": 478,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample backup rules file; uncomment and customize as necessary.\n   See htt"
  },
  {
    "path": "app/src/main/res/xml/data_extraction_rules.xml",
    "chars": 487,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample data extraction rules file; uncomment and customize as necessary.\n "
  },
  {
    "path": "app/src/test/java/com/bhm/demo/ExampleUnitTest.kt",
    "chars": 336,
    "preview": "package com.bhm.demo\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execu"
  },
  {
    "path": "ble/.gitignore",
    "chars": 6,
    "preview": "/build"
  },
  {
    "path": "ble/build.gradle",
    "chars": 1856,
    "preview": "plugins {\n    id 'com.android.library'\n    id 'org.jetbrains.kotlin.android'\n    id 'maven-publish'\n}\n\n//ext.GROUP = \"co"
  },
  {
    "path": "ble/consumer-rules.pro",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "ble/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "ble/src/main/AndroidManifest.xml",
    "chars": 1324,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:to"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/BleManager.kt",
    "chars": 26725,
    "preview": "@file:Suppress(\"SENSELESS_COMPARISON\", \"unused\")\n\npackage com.bhm.ble\n\nimport android.annotation.SuppressLint\nimport and"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/attribute/BleOptions.kt",
    "chars": 9792,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.at"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BleBaseCallback.kt",
    "chars": 1180,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BleConnectCallback.kt",
    "chars": 3766,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BleEventCallback.kt",
    "chars": 4694,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BleIndicateCallback.kt",
    "chars": 1673,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BleMtuChangedCallback.kt",
    "chars": 1004,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BleNotifyCallback.kt",
    "chars": 1625,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BleReadCallback.kt",
    "chars": 1028,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BleRssiCallback.kt",
    "chars": 992,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BleScanCallback.kt",
    "chars": 2760,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BleWriteCallback.kt",
    "chars": 1645,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/callback/BluetoothCallback.kt",
    "chars": 1163,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ca"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/control/BleLruHashMap.kt",
    "chars": 1387,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.co"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/control/BleTask.kt",
    "chars": 2495,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.co"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/control/BleTaskList.kt",
    "chars": 1207,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.co"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/control/BleTaskQueue.kt",
    "chars": 7255,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n\npackage com.bhm.ble.c"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/data/BleConnectFailType.kt",
    "chars": 937,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.da"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/data/BleConnectLastState.kt",
    "chars": 624,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.da"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/data/BleDescriptorGetType.kt",
    "chars": 1359,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.da"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/data/BleException.kt",
    "chars": 1667,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"unused"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/data/BleScanFailType.kt",
    "chars": 1501,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.da"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/data/BleTaskQueueType.kt",
    "chars": 1113,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.da"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/data/BleWriteData.kt",
    "chars": 2273,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.da"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/data/BleWriteQueueData.kt",
    "chars": 1640,
    "preview": "package com.bhm.ble.data\n\n/**\n * @description [com.bhm.ble.request.BleWriteRequest.writeQueueData]方法中队列存放的数据结构\n * @autho"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/data/Constants.kt",
    "chars": 1816,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.da"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/device/BleConnectedDevice.kt",
    "chars": 16300,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.de"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/device/BleConnectedDeviceManager.kt",
    "chars": 2814,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"SENSEL"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/device/BleDevice.kt",
    "chars": 2700,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"DEPREC"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/log/BleLogEvent.kt",
    "chars": 341,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.lo"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/log/BleLogLevel.kt",
    "chars": 261,
    "preview": "package com.bhm.ble.log\n\n/**\n * @description 日志级别\n * @author Buhuiming\n * @date 2024/05/08/ 10:28\n */\nsealed class BleLo"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/log/BleLogManager.kt",
    "chars": 1078,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"SENSEL"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/log/BleLogger.kt",
    "chars": 2133,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.lo"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/receiver/BluetoothReceiver.kt",
    "chars": 1649,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.re"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/BleConnectRequest.kt",
    "chars": 25627,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n\npackage com.bhm.ble.r"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/BleIndicateRequest.kt",
    "chars": 13199,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"Remove"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/BleMtuRequest.kt",
    "chars": 4117,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"Remove"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/BleNotifyRequest.kt",
    "chars": 12964,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"Remove"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/BleReadRequest.kt",
    "chars": 5650,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"Remove"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/BleRssiRequest.kt",
    "chars": 4173,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"Remove"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/BleScanRequest.kt",
    "chars": 14725,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"SENSEL"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/BleSetPriorityRequest.kt",
    "chars": 1339,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.re"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/BleWriteRequest.kt",
    "chars": 24954,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n@file:Suppress(\"Remove"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/base/BleBaseRequest.kt",
    "chars": 7035,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.re"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/base/BleRequestImp.kt",
    "chars": 23051,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\n\npackage com.bhm.ble.r"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/base/BleTaskQueueRequest.kt",
    "chars": 2879,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.re"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/request/base/Request.kt",
    "chars": 3280,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.re"
  },
  {
    "path": "ble/src/main/java/com/bhm/ble/utils/BleUtil.kt",
    "chars": 5479,
    "preview": "/*\n * Copyright (c) 2022-2032 buhuiming\n * 不能修改和删除上面的版权声明\n * 此代码属于buhuiming编写,在未经允许的情况下不得传播复制\n */\npackage com.bhm.ble.ut"
  },
  {
    "path": "build.gradle",
    "chars": 715,
    "preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nplugins {\n    id 'co"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 230,
    "preview": "#Tue Aug 12 09:31:03 CST 2025\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://"
  },
  {
    "path": "gradle.properties",
    "chars": 1358,
    "preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
  },
  {
    "path": "gradlew",
    "chars": 5766,
    "preview": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0"
  },
  {
    "path": "gradlew.bat",
    "chars": 2674,
    "preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "jitpack.yml",
    "chars": 96,
    "preview": "jdk:\n  - openjdk17\nbefore_install:\n  - sdk install java 17.0.7-open\n  - sdk use java 17.0.7-open"
  },
  {
    "path": "maven_upload.gradle",
    "chars": 2309,
    "preview": "def RELEASE_REPOSITORY_URL = \"http://localhost:8081/nexus/content/repositories/releases/\"\ndef SNAPSHOT_REPOSITORY_URL = "
  },
  {
    "path": "settings.gradle",
    "chars": 401,
    "preview": "pluginManagement {\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\ndepen"
  },
  {
    "path": "support/.gitignore",
    "chars": 6,
    "preview": "/build"
  },
  {
    "path": "support/build.gradle",
    "chars": 2445,
    "preview": "plugins {\n    id 'com.android.library'\n    id 'org.jetbrains.kotlin.android'\n}\n\nandroid {\n    compileSdk 33\n\n    default"
  },
  {
    "path": "support/consumer-rules.pro",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "support/proguard-rules.pro",
    "chars": 750,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "support/src/main/AndroidManifest.xml",
    "chars": 227,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package="
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/common/BaseActivity.kt",
    "chars": 4948,
    "preview": "package com.bhm.support.sdk.common\n\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport androi"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/common/BaseApplication.kt",
    "chars": 870,
    "preview": "package com.bhm.support.sdk.common\n\nimport android.annotation.SuppressLint\nimport android.app.Application\nimport android"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/common/BaseFragment.kt",
    "chars": 1937,
    "preview": "@file:Suppress(\"UNCHECKED_CAST\")\n\npackage com.bhm.support.sdk.common\n\nimport android.content.Context\nimport android.os.H"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/common/BaseVBActivity.kt",
    "chars": 776,
    "preview": "package com.bhm.support.sdk.common\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.viewbinding.ViewBi"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/common/BaseVBFragment.kt",
    "chars": 818,
    "preview": "package com.bhm.support.sdk.common\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/common/BaseViewModel.kt",
    "chars": 273,
    "preview": "package com.bhm.support.sdk.common\n\nimport android.app.Application\nimport androidx.lifecycle.AndroidViewModel\n\n/**\n * @a"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/common/DefaultViewPagerAdapter.kt",
    "chars": 1040,
    "preview": "package com.bhm.support.sdk.common\n\nimport androidx.fragment.app.Fragment\nimport androidx.viewpager2.adapter.FragmentSta"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/constants/AppConstants.kt",
    "chars": 147,
    "preview": "package com.bhm.support.sdk.constants\n\n\n/**\n * @author Buhuiming\n * @description: App常用静态参数\n * @date :2022/6/28 14:26\n *"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/constants/PermissionConstants.kt",
    "chars": 265,
    "preview": "package com.bhm.support.sdk.constants\n\nimport android.Manifest\n\n/**\n * @author Buhuiming\n * @description:\n * @date :2022"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/core/AppTheme.kt",
    "chars": 4270,
    "preview": "package com.bhm.support.sdk.core\n\nimport android.app.Activity\nimport android.content.Context\nimport android.os.Build\nimp"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/core/GlideCircleTransform.kt",
    "chars": 1532,
    "preview": "package com.bhm.support.sdk.core\n\nimport android.content.Context\nimport android.graphics.*\nimport com.bumptech.glide.loa"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/core/GlideRoundTransform.kt",
    "chars": 2059,
    "preview": "package com.bhm.support.sdk.core\n\nimport android.content.res.Resources\nimport android.graphics.*\nimport com.bumptech.gli"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/core/GridSpacingItemDecoration.kt",
    "chars": 1480,
    "preview": "package com.bhm.support.sdk.core\n\nimport android.graphics.Rect\nimport android.view.View\nimport androidx.recyclerview.wid"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/core/MyStaggeredGridLayoutManager.kt",
    "chars": 481,
    "preview": "package com.bhm.support.sdk.core\n\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager\n\n/**\n * 禁止滑动\n */\nclass "
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/core/WeakHandler.kt",
    "chars": 693,
    "preview": "package com.bhm.support.sdk.core\n\nimport android.os.Handler\nimport android.os.Looper\nimport android.os.Message\nimport ja"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/entity/MessageEvent.kt",
    "chars": 203,
    "preview": "package com.bhm.support.sdk.entity\n\nimport java.io.Serializable\n\n/**\n * @author Buhuiming\n */\nclass MessageEvent : Seria"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/utils/ActivityUtil.kt",
    "chars": 1799,
    "preview": "package com.bhm.support.sdk.utils\n\nimport android.app.Activity\nimport android.app.ActivityManager\nimport android.content"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/utils/DateUtil.kt",
    "chars": 1230,
    "preview": "package com.bhm.support.sdk.utils\n\nimport java.text.SimpleDateFormat\nimport java.util.*\n\n/**\n * @author Buhuiming\n * @da"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/utils/DisplayUtil.kt",
    "chars": 1164,
    "preview": "package com.bhm.support.sdk.utils\n\nimport android.content.Context\n\n/**\n * @author Buhuiming\n * @date :2022/10/11 14:14\n "
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/utils/NotificationUtil.kt",
    "chars": 8315,
    "preview": "package com.bhm.support.sdk.utils\n\nimport android.annotation.TargetApi\nimport android.app.Notification\nimport android.ap"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/utils/SPUtil.kt",
    "chars": 1431,
    "preview": "package com.bhm.support.sdk.utils\n\nimport android.annotation.SuppressLint\nimport android.content.Context\n\n/**\n * @author"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/utils/ViewUtil.kt",
    "chars": 10490,
    "preview": "@file:Suppress(\"UNCHECKED_CAST\")\n\npackage com.bhm.support.sdk.utils\n\nimport android.content.Context\nimport android.graph"
  },
  {
    "path": "support/src/main/java/com/bhm/support/sdk/widget/ChoseView.kt",
    "chars": 33762,
    "preview": "package com.bhm.support.sdk.widget\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android"
  },
  {
    "path": "support/src/main/res/values/attrs.xml",
    "chars": 1837,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <attr format=\"integer\" name=\"gravity\">\n        <flag name=\"start\""
  },
  {
    "path": "support/src/main/res/values/colors.xml",
    "chars": 224,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"white\">#FFFFFF</color><!--主色-->\n    <color name=\"col"
  },
  {
    "path": "support/src/main/res/values/ids.xml",
    "chars": 108,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<resources>\n    <item name=\"last_click_time\" type=\"id\"/>\n</resources>"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the buhuiming/BleCore GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 131 files (494.4 KB), approximately 121.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!