├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable
│ │ │ │ ├── ic_arrow_down.xml
│ │ │ │ ├── ic_rssi.xml
│ │ │ │ ├── ic_arrow_right.xml
│ │ │ │ ├── ic_ble.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── themes.xml
│ │ │ │ └── strings.xml
│ │ │ ├── menu
│ │ │ │ └── top_app_bar.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_operate.xml
│ │ │ │ ├── fragment_service.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── item_service.xml
│ │ │ │ ├── scan_filter.xml
│ │ │ │ ├── item_ble_device.xml
│ │ │ │ └── fragment_operate.xml
│ │ │ ├── navigation
│ │ │ │ └── operator_nav.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── values-zh
│ │ │ │ └── strings.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── huyuhui
│ │ │ │ └── blesample
│ │ │ │ ├── Uuid.kt
│ │ │ │ ├── MyApplication.kt
│ │ │ │ ├── adapter
│ │ │ │ ├── ScanFilterAdapter.kt
│ │ │ │ ├── SharedOperateAdapter.kt
│ │ │ │ ├── BleDeviceAdapter.kt
│ │ │ │ └── ServiceAdapter.kt
│ │ │ │ ├── AppUtils.kt
│ │ │ │ ├── operate
│ │ │ │ ├── OperateActivity.kt
│ │ │ │ ├── ServiceFragment.kt
│ │ │ │ └── SequenceNotifyOperator.kt
│ │ │ │ └── ScanRecyclerView.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── huyuhui
│ │ │ └── blesample
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── huyuhui
│ │ └── blesample
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── library
├── .gitignore
├── consumer-rules.pro
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── huyuhui
│ │ └── fastble
│ │ ├── queue
│ │ ├── TaskResult.kt
│ │ ├── operate
│ │ │ ├── SequenceBleOperator.kt
│ │ │ ├── BleOperatorQueue.kt
│ │ │ └── SequenceWriteOperator.kt
│ │ ├── Task.kt
│ │ └── Queue.kt
│ │ ├── data
│ │ ├── BleScanState.kt
│ │ ├── BleWriteState.kt
│ │ ├── BleOperatorKey.kt
│ │ └── BleDevice.kt
│ │ ├── common
│ │ ├── BleFactory.kt
│ │ ├── BluetoothChangedObserver.kt
│ │ ├── TimeoutTask.kt
│ │ └── BleConnectStrategy.kt
│ │ ├── callback
│ │ ├── BleOperateCallback.kt
│ │ ├── BleScanCallback.kt
│ │ ├── BleRssiCallback.kt
│ │ ├── BleMtuChangedCallback.kt
│ │ ├── BleReadCallback.kt
│ │ ├── BleNotifyCallback.kt
│ │ ├── BleIndicateCallback.kt
│ │ ├── BleWriteCallback.kt
│ │ └── BleGattCallback.kt
│ │ ├── utils
│ │ ├── BleLog.kt
│ │ ├── DataUtil.kt
│ │ ├── UuidUtils.kt
│ │ ├── BleLruHashMap.kt
│ │ └── HexUtil.kt
│ │ ├── bluetooth
│ │ ├── BleCharacteristicOperator.kt
│ │ ├── BleReadRssiOperator.kt
│ │ ├── BleMtuOperator.kt
│ │ ├── BleReadOperator.kt
│ │ ├── BleOperator.kt
│ │ ├── BleWriteOperator.kt
│ │ ├── MultipleBluetoothController.kt
│ │ ├── BleIndicateOperator.kt
│ │ ├── BleNotifyOperator.kt
│ │ └── SplitWriter.kt
│ │ ├── exception
│ │ ├── BleCoroutineExceptionHandler.kt
│ │ └── BleException.kt
│ │ └── scan
│ │ ├── BleScanRuleConfig.kt
│ │ └── BleScanner.kt
├── proguard-rules.pro
└── build.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── jitpack.yml
├── .gitignore
├── settings.gradle
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/library/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | before_install:
2 | - sdk install java 17.0.10-open
3 | - sdk use java 17.0.10-open
4 |
5 | jdk:
6 | - openjdk17
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/queue/TaskResult.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.queue
2 |
3 | data class TaskResult(val task: Task, val success: Boolean)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kanshenmekan/FastBle/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/data/BleScanState.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.data
2 |
3 | internal enum class BleScanState {
4 | STATE_IDLE, STATE_SCANNING;
5 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/data/BleWriteState.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.data
2 |
3 | internal class BleWriteState {
4 | companion object {
5 | @JvmStatic
6 | val DATA_WRITE_SINGLE = 1
7 | }
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/Uuid.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample
2 |
3 | object Uuid {
4 | const val Service = "0000ffe0-0000-1000-8000-00805F9B34FB"
5 | const val Characteristic = "0000ffe1-0000-1000-8000-00805F9B34FB"
6 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/common/BleFactory.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.common
2 |
3 | import com.huyuhui.fastble.data.BleDevice
4 |
5 | interface BleFactory {
6 | fun generateUniqueKey(bleDevice: BleDevice): String
7 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Oct 24 10:46:31 CST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/callback/BleOperateCallback.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.callback
2 |
3 | import android.bluetooth.BluetoothGattCharacteristic
4 | import com.huyuhui.fastble.data.BleDevice
5 | import com.huyuhui.fastble.exception.BleException
6 |
7 | abstract class BleOperateCallback {
8 |
9 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/callback/BleScanCallback.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.callback
2 |
3 | import com.huyuhui.fastble.data.BleDevice
4 |
5 | interface BleScanCallback {
6 | fun onScanStarted(success: Boolean)
7 | fun onLeScan(oldDevice: BleDevice, newDevice: BleDevice, scannedBefore: Boolean)
8 | fun onScanFinished(scanResultList: List)
9 | fun onFilter(bleDevice: BleDevice): Boolean
10 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/huyuhui/blesample/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/queue/operate/SequenceBleOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.queue.operate
2 |
3 | import com.huyuhui.fastble.data.BleDevice
4 | import com.huyuhui.fastble.queue.Task
5 | import com.huyuhui.fastble.queue.TaskResult
6 | import kotlinx.coroutines.channels.Channel
7 |
8 | abstract class SequenceBleOperator(priority: Int, delay: Long) : Task(priority, delay) {
9 | abstract fun execute(bleDevice: BleDevice, channel: Channel)
10 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_down.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/callback/BleRssiCallback.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.callback
2 |
3 | import android.bluetooth.BluetoothGattCharacteristic
4 | import com.huyuhui.fastble.data.BleDevice
5 | import com.huyuhui.fastble.exception.BleException
6 |
7 | abstract class BleRssiCallback : BleOperateCallback() {
8 |
9 | abstract fun onRssiFailure(bleDevice: BleDevice, exception: BleException)
10 |
11 | abstract fun onRssiSuccess(bleDevice: BleDevice, rssi: Int)
12 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/callback/BleMtuChangedCallback.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.callback
2 |
3 | import android.bluetooth.BluetoothGattCharacteristic
4 | import com.huyuhui.fastble.data.BleDevice
5 | import com.huyuhui.fastble.exception.BleException
6 |
7 | abstract class BleMtuChangedCallback : BleOperateCallback() {
8 |
9 | abstract fun onSetMTUFailure(bleDevice: BleDevice, exception: BleException)
10 |
11 | abstract fun onMtuChanged(bleDevice: BleDevice, mtu: Int)
12 |
13 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | maven { url = 'https://jitpack.io' }
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | maven { url = 'https://jitpack.io' }
15 | }
16 | }
17 | rootProject.name = "FastBle"
18 | include ':app'
19 | include ':library'
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rssi.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_right.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 | #eeeeee
11 |
12 | #1DE9B6
13 | #1DE9B6
14 | #FF5722
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/callback/BleReadCallback.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.callback
2 |
3 | import android.bluetooth.BluetoothGattCharacteristic
4 | import com.huyuhui.fastble.data.BleDevice
5 | import com.huyuhui.fastble.exception.BleException
6 |
7 | abstract class BleReadCallback : BleOperateCallback() {
8 | abstract fun onReadSuccess(
9 | bleDevice: BleDevice,
10 | characteristic: BluetoothGattCharacteristic,
11 | data: ByteArray
12 | )
13 |
14 | abstract fun onReadFailure(
15 | bleDevice: BleDevice,
16 | characteristic: BluetoothGattCharacteristic?,
17 | exception: BleException
18 | )
19 | }
--------------------------------------------------------------------------------
/app/src/main/res/menu/top_app_bar.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/huyuhui/blesample/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.gtpower.myble", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/utils/BleLog.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.utils
2 |
3 | import android.util.Log
4 |
5 | @Suppress("unused")
6 | object BleLog {
7 | var isPrint = true
8 |
9 | @JvmStatic
10 | private val defaultTag = "FastBle"
11 |
12 | fun d(msg: String?) {
13 | if (isPrint && msg != null) Log.d(defaultTag, msg)
14 | }
15 |
16 | fun i(msg: String?) {
17 | if (isPrint && msg != null) Log.i(defaultTag, msg)
18 | }
19 |
20 | fun w(msg: String?) {
21 | if (isPrint && msg != null) Log.w(defaultTag, msg)
22 | }
23 |
24 | fun e(msg: String?) {
25 | if (isPrint && msg != null) Log.e(defaultTag, msg)
26 | }
27 |
28 | fun e(msg: String?, e: Throwable) {
29 | if (isPrint && msg != null) Log.e(defaultTag, msg, e)
30 | }
31 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/bluetooth/BleCharacteristicOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.bluetooth
2 |
3 | import android.bluetooth.BluetoothGattCharacteristic
4 | import android.bluetooth.BluetoothGattService
5 | import com.huyuhui.fastble.data.BleOperatorKey
6 |
7 | internal abstract class BleCharacteristicOperator(
8 | bleBluetooth: BleBluetooth,
9 | timeout: Long,
10 | uuidService: String,
11 | uuidCharacteristic: String
12 | ) : BleOperator(bleBluetooth, timeout) {
13 |
14 | val mGattService: BluetoothGattService? =
15 | fromUUID(uuidService)?.let { mBluetoothGatt?.getService(it) }
16 |
17 | val mCharacteristic: BluetoothGattCharacteristic? = fromUUID(uuidCharacteristic)?.let {
18 | mGattService?.getCharacteristic(it)
19 | }
20 |
21 | val key = BleOperatorKey(uuidService, uuidCharacteristic)
22 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/queue/Task.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.queue
2 |
3 | import java.util.concurrent.atomic.AtomicLong
4 |
5 | abstract class Task(val priority: Int, var delay: Long) {
6 | companion object {
7 | private val atomic = AtomicLong(0)
8 | }
9 |
10 | var sequenceNum: Long = 0
11 | private set
12 |
13 | init {
14 | sequenceNum = atomic.getAndIncrement()
15 | }
16 |
17 | /**
18 | * 当continuous 为true的时候,等待任务完成之后(触发回调或者超时),才会进行delay任务,之后获取下一个任务
19 | * 为false的时候,会直接进行delay任务,之后获取下一个任务
20 | * @see timeout
21 | */
22 | abstract val continuous: Boolean
23 |
24 | /**
25 | * @see continuous
26 | * 当continuous为true之后,这个设置才有效果,如果任务在时间内没有回调,直接忽略掉,进行delay任务,之后获取下一个任务
27 | * 建议给一个适当的时长,以便任务有足够时间触发回调
28 | * 如果timeout为0,则会一直等待,直到任务回调触发
29 | */
30 | abstract val timeout: Long
31 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/callback/BleNotifyCallback.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.callback
2 |
3 | import android.bluetooth.BluetoothGattCharacteristic
4 | import com.huyuhui.fastble.data.BleDevice
5 | import com.huyuhui.fastble.exception.BleException
6 |
7 | abstract class BleNotifyCallback : BleOperateCallback() {
8 | abstract fun onNotifySuccess(bleDevice: BleDevice, characteristic: BluetoothGattCharacteristic)
9 |
10 | abstract fun onNotifyFailure(
11 | bleDevice: BleDevice,
12 | characteristic: BluetoothGattCharacteristic?,
13 | exception: BleException
14 | )
15 |
16 | open fun onNotifyCancel(bleDevice: BleDevice, characteristic: BluetoothGattCharacteristic){}
17 | abstract fun onCharacteristicChanged(
18 | bleDevice: BleDevice,
19 | characteristic: BluetoothGattCharacteristic,
20 | data: ByteArray
21 | )
22 |
23 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/data/BleOperatorKey.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.data
2 |
3 | class BleOperatorKey(uuidService: String, uuidCharacteristic: String) {
4 | private val normalizedServiceUuid = uuidService.lowercase()
5 | private val normalizedCharacteristicUuid = uuidCharacteristic.lowercase()
6 | override fun equals(other: Any?): Boolean {
7 | if (this === other) return true
8 | if (javaClass != other?.javaClass) return false
9 |
10 | other as BleOperatorKey
11 |
12 | if (normalizedServiceUuid != other.normalizedServiceUuid) return false
13 | if (normalizedCharacteristicUuid != other.normalizedCharacteristicUuid) return false
14 |
15 | return true
16 | }
17 |
18 | override fun hashCode(): Int {
19 | var result = normalizedServiceUuid.hashCode()
20 | result = 31 * result + normalizedCharacteristicUuid.hashCode()
21 | return result
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/callback/BleIndicateCallback.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.callback
2 |
3 | import android.bluetooth.BluetoothGattCharacteristic
4 | import com.huyuhui.fastble.data.BleDevice
5 | import com.huyuhui.fastble.exception.BleException
6 |
7 | abstract class BleIndicateCallback : BleOperateCallback() {
8 | abstract fun onIndicateSuccess(
9 | bleDevice: BleDevice,
10 | characteristic: BluetoothGattCharacteristic
11 | )
12 |
13 | abstract fun onIndicateFailure(
14 | bleDevice: BleDevice,
15 | characteristic: BluetoothGattCharacteristic?,
16 | exception: BleException
17 | )
18 |
19 | open fun onIndicateCancel(
20 | bleDevice: BleDevice,
21 | characteristic: BluetoothGattCharacteristic
22 | ){}
23 |
24 | abstract fun onCharacteristicChanged(
25 | bleDevice: BleDevice,
26 | characteristic: BluetoothGattCharacteristic,
27 | data: ByteArray?
28 | )
29 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_ble.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_operate.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/queue/operate/BleOperatorQueue.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.queue.operate
2 |
3 | import com.huyuhui.fastble.bluetooth.BleBluetooth
4 | import com.huyuhui.fastble.exception.BleCoroutineExceptionHandler
5 | import com.huyuhui.fastble.queue.Queue
6 | import com.huyuhui.fastble.queue.TaskResult
7 | import com.huyuhui.fastble.utils.BleLog
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.SupervisorJob
10 | import kotlinx.coroutines.channels.Channel
11 | import kotlinx.coroutines.job
12 |
13 | internal class BleOperatorQueue(private val bleBluetooth: BleBluetooth) :
14 | Queue(),
15 | CoroutineScope by CoroutineScope(
16 | SupervisorJob(bleBluetooth.coroutineContext.job) + BleCoroutineExceptionHandler({ _, throwable ->
17 | BleLog.e("Ble operator queue:a coroutine error has occurred ${throwable.message}")
18 | })
19 | ) {
20 | override fun execute(task: SequenceBleOperator, channel: Channel) {
21 | task.execute(bleBluetooth.bleDevice, channel)
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/MyApplication.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample
2 |
3 | import android.app.Application
4 | import com.huyuhui.fastble.BleManager
5 | import com.huyuhui.fastble.common.BleConnectStrategy
6 | import com.huyuhui.fastble.common.BleFactory
7 | import com.huyuhui.fastble.data.BleDevice
8 |
9 | class MyApplication : Application() {
10 | override fun onCreate() {
11 | super.onCreate()
12 | BleManager.apply {
13 | enableLog(true)
14 | maxConnectCount = 5
15 | operateTimeout = 2000
16 | splitWriteNum = 20
17 | bleConnectStrategy = BleConnectStrategy.Builder().setConnectOverTime(10000)
18 | .setConnectBackpressureStrategy(BleConnectStrategy.CONNECT_BACKPRESSURE_DROP)
19 | .setReConnectCount(1).setReConnectInterval(2000).build()
20 | bleFactory = object :BleFactory{
21 | override fun generateUniqueKey(bleDevice: BleDevice): String {
22 | return bleDevice.mac
23 | }
24 | }
25 | }.init(this)
26 | }
27 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/callback/BleWriteCallback.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.callback
2 |
3 | import android.bluetooth.BluetoothGattCharacteristic
4 | import com.huyuhui.fastble.data.BleDevice
5 | import com.huyuhui.fastble.data.BleWriteState
6 | import com.huyuhui.fastble.exception.BleException
7 |
8 | abstract class BleWriteCallback : BleOperateCallback() {
9 |
10 | abstract fun onWriteSuccess(
11 | bleDevice: BleDevice,
12 | characteristic: BluetoothGattCharacteristic,
13 | current: Int = BleWriteState.DATA_WRITE_SINGLE,
14 | total: Int = BleWriteState.DATA_WRITE_SINGLE,
15 | justWrite: ByteArray,
16 | data: ByteArray = justWrite
17 | )
18 |
19 | abstract fun onWriteFailure(
20 | bleDevice: BleDevice,
21 | characteristic: BluetoothGattCharacteristic?,
22 | exception: BleException,
23 | current: Int = BleWriteState.DATA_WRITE_SINGLE,
24 | total: Int = BleWriteState.DATA_WRITE_SINGLE,
25 | justWrite: ByteArray?,
26 | data: ByteArray? = justWrite,
27 | isTotalFail: Boolean = true
28 | )
29 |
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/adapter/ScanFilterAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.huyuhui.blesample.databinding.ScanFilterBinding
7 |
8 | class ScanFilterAdapter : RecyclerView.Adapter() {
9 | var scanFilterBinding: ScanFilterBinding? = null
10 | private set
11 |
12 | class ScanViewHolder(binding: ScanFilterBinding) :
13 | RecyclerView.ViewHolder(binding.root)
14 |
15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScanViewHolder {
16 | return ScanViewHolder(
17 | ScanFilterBinding.inflate(
18 | LayoutInflater.from(parent.context),
19 | parent,
20 | false
21 | ).apply {
22 | scanFilterBinding = this
23 | })
24 | }
25 |
26 | override fun getItemCount(): Int {
27 | return 1
28 | }
29 |
30 | override fun onBindViewHolder(holder: ScanViewHolder, position: Int) {
31 |
32 | }
33 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/utils/DataUtil.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.utils
2 |
3 | import java.util.LinkedList
4 | import java.util.Queue
5 |
6 | object DataUtil {
7 | fun splitPacketForByte(data: ByteArray?, length: Int): Queue {
8 | val dataInfoQueue: Queue = LinkedList()
9 | if (data != null) {
10 | var index = 0
11 | do {
12 | val surplusData = ByteArray(data.size - index)
13 | var currentData: ByteArray
14 | System.arraycopy(data, index, surplusData, 0, data.size - index)
15 | if (surplusData.size <= length) {
16 | currentData = ByteArray(surplusData.size)
17 | System.arraycopy(surplusData, 0, currentData, 0, surplusData.size)
18 | index += surplusData.size
19 | } else {
20 | currentData = ByteArray(length)
21 | System.arraycopy(data, index, currentData, 0, length)
22 | index += length
23 | }
24 | dataInfoQueue.offer(currentData)
25 | } while (index < data.size)
26 | }
27 | return dataInfoQueue
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_service.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
24 |
25 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/operator_nav.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
16 |
19 |
22 |
23 |
24 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/adapter/SharedOperateAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample.adapter
2 |
3 | import android.view.View
4 | import android.view.ViewGroup
5 | import android.view.ViewGroup.LayoutParams
6 | import android.widget.TextView
7 | import androidx.recyclerview.widget.LinearLayoutManager
8 | import androidx.recyclerview.widget.RecyclerView
9 |
10 | class SharedOperateAdapter(private val data: List) :
11 | RecyclerView.Adapter() {
12 |
13 | override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
14 | super.onAttachedToRecyclerView(recyclerView)
15 | recyclerView.layoutManager = LinearLayoutManager(recyclerView.context)
16 | }
17 | class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
18 |
19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
20 | return ItemViewHolder(TextView(parent.context).apply {
21 | layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
22 | })
23 | }
24 |
25 | override fun getItemCount(): Int {
26 | return data.size
27 | }
28 |
29 | override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
30 | (holder.itemView as TextView).text = data[position]
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
17 |
18 |
27 |
28 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/exception/BleCoroutineExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.exception
2 |
3 | import kotlinx.coroutines.CoroutineExceptionHandler
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.MainScope
6 | import kotlin.coroutines.CoroutineContext
7 |
8 | // 扩展 MainScope,返回一个包含 exceptionHandler 的作用域
9 | fun CoroutineScope.withExceptionHandler(handler: CoroutineExceptionHandler): CoroutineScope {
10 | return CoroutineScope(coroutineContext + handler)
11 | }
12 |
13 | @Suppress("FunctionName")
14 | fun BleMainScope(handler: (CoroutineContext, Throwable) -> Unit): CoroutineScope =
15 | MainScope().withExceptionHandler(BleCoroutineExceptionHandler(handler))
16 |
17 | @Suppress("FunctionName")
18 | inline fun BleCoroutineExceptionHandler(crossinline handler: (CoroutineContext, Throwable) -> Unit): CoroutineExceptionHandler =
19 | CoroutineExceptionHandler { coroutineContext, exception ->
20 | println("Caught exception: ${exception.message}")
21 | println("Stack trace:")
22 | exception.stackTrace.filter { it.className.startsWith("com.huyuhui.fastble") }
23 | .forEach { stackTraceElement ->
24 | println(" at $stackTraceElement")
25 | }
26 | // 打印完整的堆栈跟踪
27 | exception.stackTrace.forEach { stackTraceElement ->
28 | println(" at $stackTraceElement")
29 | }
30 | handler(coroutineContext, exception)
31 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 |
25 | android.enableJetifier = true
--------------------------------------------------------------------------------
/library/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2 |
3 | plugins {
4 | id("com.android.library")
5 | id("org.jetbrains.kotlin.android")
6 | id("maven-publish")
7 | }
8 | val VERSION_NAME = "1.0"
9 | val GROUP_ID = "com.github.kanshenmekan"
10 | val ARTIFACT_ID = "FastBle"
11 | android {
12 | namespace = "com.huyuhui.fastble"
13 | compileSdk = 35
14 |
15 | defaultConfig {
16 | minSdk = 21
17 | }
18 | compileOptions {
19 | sourceCompatibility = JavaVersion.VERSION_1_8
20 | targetCompatibility = JavaVersion.VERSION_1_8
21 | }
22 | kotlin {
23 | compilerOptions {
24 | jvmTarget.set(JvmTarget.JVM_1_8)
25 | }
26 | }
27 | packaging {
28 | // 剔除这个包下的所有文件(不会移除签名信息)
29 | resources.excludes.add("META-INF/*******")
30 | }
31 | publishing {
32 | singleVariant("release") {
33 | withSourcesJar()
34 | withJavadocJar()
35 | }
36 | }
37 | }
38 |
39 | afterEvaluate {
40 | publishing {
41 | publications {
42 | create("release") {
43 | groupId = GROUP_ID
44 | artifactId = ARTIFACT_ID
45 | version = VERSION_NAME
46 | from(components["release"])
47 | }
48 | }
49 | }
50 | }
51 |
52 | dependencies {
53 | implementation("androidx.core:core-ktx:1.16.0")
54 | implementation("androidx.appcompat:appcompat:1.7.1")
55 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/callback/BleGattCallback.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.callback
2 |
3 | import android.bluetooth.BluetoothGatt
4 | import android.bluetooth.BluetoothGattCallback
5 | import com.huyuhui.fastble.data.BleDevice
6 | import com.huyuhui.fastble.exception.BleException
7 |
8 | abstract class BleGattCallback : BluetoothGattCallback() {
9 |
10 | abstract fun onStartConnect(bleDevice: BleDevice)
11 |
12 | abstract fun onConnectFail(bleDevice: BleDevice, exception: BleException)
13 |
14 | /**
15 | * @param skip true表示 当前发起的连接,设备已经连接,或者该连接被新发起的连接覆盖 false 则表示手动取消了该次连接
16 | */
17 | open fun onConnectCancel(bleDevice: BleDevice, skip: Boolean) {}
18 | abstract fun onConnectSuccess(bleDevice: BleDevice, gatt: BluetoothGatt?, status: Int)
19 |
20 | abstract fun onDisConnected(
21 | isActiveDisConnected: Boolean,
22 | device: BleDevice,
23 | gatt: BluetoothGatt?,
24 | status: Int
25 | )
26 |
27 | open fun onServicesDiscovered(bleDevice: BleDevice, gatt: BluetoothGatt?, status: Int) {
28 |
29 | }
30 |
31 | open fun onPhyUpdate(
32 | bleDevice: BleDevice,
33 | gatt: BluetoothGatt?,
34 | txPhy: Int,
35 | rxPhy: Int,
36 | status: Int
37 | ) {
38 |
39 | }
40 |
41 | open fun onPhyRead(
42 | bleDevice: BleDevice,
43 | gatt: BluetoothGatt?,
44 | txPhy: Int,
45 | rxPhy: Int,
46 | status: Int
47 | ) {
48 |
49 | }
50 |
51 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/utils/UuidUtils.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.utils
2 |
3 | import android.annotation.SuppressLint
4 |
5 | @Suppress("unused")
6 | object UuidUtils {
7 | private const val BASE_UUID_REGEX =
8 | "0000([0-9a-f][0-9a-f][0-9a-f][0-9a-f])-0000-1000-8000-00805f9b34fb"
9 | private const val BASE_UUID = "0000xxxx-0000-1000-8000-00805F9B34FB"
10 |
11 | @SuppressLint("PrivateApi")
12 | fun isBaseUUID(uuid: String): Boolean {
13 | return uuid.lowercase()
14 | .matches(Regex("0000([0-9a-f][0-9a-f][0-9a-f][0-9a-f])-0000-1000-8000-00805f9b34fb"))
15 | }
16 |
17 | fun is16UUID(uuid: String): Boolean {
18 | return uuid.matches(Regex("""^[0-9a-fA-F]{4}$"""))
19 | }
20 |
21 | fun isUUID(str: String): Boolean {
22 | val uuidPattern = Regex(
23 | """^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"""
24 | )
25 | return uuidPattern.matches(str)
26 | }
27 |
28 | fun uuid128To16(uuid: String, lowerCase: Boolean = true): String? {
29 | return if (isBaseUUID(uuid)) {
30 | if (lowerCase) uuid.substring(4, 8).lowercase()
31 | else uuid.substring(4, 8).uppercase()
32 | } else null
33 | }
34 |
35 | fun uuid16To128(uuid: String, lowerCase: Boolean = true): String? {
36 | return if (is16UUID(uuid)) {
37 | if (lowerCase) BASE_UUID.replaceRange(4, 8, uuid)
38 | .lowercase() else BASE_UUID.replaceRange(4, 8, uuid).uppercase()
39 | } else null
40 | }
41 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/bluetooth/BleReadRssiOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.bluetooth
2 |
3 | import android.annotation.SuppressLint
4 | import com.huyuhui.fastble.callback.BleRssiCallback
5 | import com.huyuhui.fastble.common.TimeoutTask
6 | import com.huyuhui.fastble.exception.BleException
7 |
8 | @SuppressLint("MissingPermission")
9 | internal class BleReadRssiOperator(
10 | bleBluetooth: BleBluetooth,
11 | timeout: Long
12 | ) : BleOperator(bleBluetooth, timeout) {
13 | var bleRssiCallback: BleRssiCallback? = null
14 | private set
15 |
16 | /**
17 | * rssi
18 | */
19 | fun readRemoteRssi(bleRssiCallback: BleRssiCallback?) {
20 | if (mBluetoothGatt == null) {
21 | bleRssiCallback?.onRssiFailure(
22 | bleDevice,
23 | BleException.OtherException(BleException.GATT_NULL, "gatt is null")
24 | )
25 | } else {
26 | timeOutTask.start(this)
27 | this.bleRssiCallback = bleRssiCallback
28 | bleBluetooth.setRssiOperator(this)
29 | if (!mBluetoothGatt!!.readRemoteRssi()) {
30 | removeTimeOut()
31 | bleRssiCallback?.onRssiFailure(
32 | bleDevice,
33 | BleException.OtherException(message = "gatt readRemoteRssi fail")
34 | )
35 | }
36 | }
37 | }
38 |
39 | override fun onTimeout(
40 | task: TimeoutTask,
41 | e: Throwable?,
42 | isActive: Boolean
43 | ) {
44 | bleRssiCallback?.onRssiFailure(bleDevice, BleException.TimeoutException())
45 | }
46 |
47 | override fun destroy() {
48 | super.destroy()
49 | bleRssiCallback = null
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/bluetooth/BleMtuOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.bluetooth
2 |
3 | import android.annotation.SuppressLint
4 | import com.huyuhui.fastble.callback.BleMtuChangedCallback
5 | import com.huyuhui.fastble.common.TimeoutTask
6 | import com.huyuhui.fastble.exception.BleException
7 |
8 | @SuppressLint("MissingPermission")
9 | internal class BleMtuOperator(
10 | bleBluetooth: BleBluetooth,
11 | timeout: Long
12 | ) : BleOperator(bleBluetooth, timeout) {
13 |
14 | var bleMtuChangedCallback: BleMtuChangedCallback? = null
15 | private set
16 |
17 | /**
18 | * set mtu
19 | */
20 | fun setMtu(requiredMtu: Int, bleMtuChangedCallback: BleMtuChangedCallback?) {
21 | if (mBluetoothGatt == null) {
22 | bleMtuChangedCallback?.onSetMTUFailure(
23 | bleDevice,
24 | BleException.OtherException(message = "gatt requestMtu fail")
25 | )
26 | } else {
27 | timeOutTask.start(this)
28 | this.bleMtuChangedCallback = bleMtuChangedCallback
29 | bleBluetooth.setMtuOperator(this)
30 | if (!mBluetoothGatt!!.requestMtu(requiredMtu)) {
31 | removeTimeOut()
32 | bleMtuChangedCallback?.onSetMTUFailure(
33 | bleDevice,
34 | BleException.OtherException(message = "gatt requestMtu fail")
35 | )
36 | }
37 | }
38 | }
39 |
40 | override fun onTimeout(
41 | task: TimeoutTask,
42 | e: Throwable?,
43 | isActive: Boolean
44 | ) {
45 | bleMtuChangedCallback?.onSetMTUFailure(bleDevice, BleException.TimeoutException())
46 | }
47 |
48 | override fun destroy() {
49 | super.destroy()
50 | bleMtuChangedCallback = null
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_service.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
16 |
17 |
25 |
26 |
36 |
37 |
45 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/utils/BleLruHashMap.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.utils
2 |
3 | import com.huyuhui.fastble.bluetooth.BleBluetooth
4 | import kotlin.math.ceil
5 |
6 | internal class BleLruHashMap(private val maxSize: Int) : LinkedHashMap(
7 | ceil(maxSize / 0.75).toInt() + 1, 0.75f, true
8 | ) {
9 |
10 | // 内部锁对象,保护所有操作的线程安全
11 | private val lock = Any()
12 |
13 | override fun removeEldestEntry(eldest: MutableMap.MutableEntry): Boolean {
14 | if (size > maxSize && eldest.value is BleBluetooth) {
15 | BleLog.w("The number of connections has surpassed the maximum limit.")
16 | (eldest.value as BleBluetooth).disconnect()
17 | }
18 | return size > maxSize
19 | }
20 |
21 | override val size: Int
22 | get() = synchronized(lock) { super.size }
23 |
24 | override fun isEmpty(): Boolean = synchronized(lock) { super.isEmpty() }
25 |
26 | override fun containsKey(key: K): Boolean = synchronized(lock) { super.containsKey(key) }
27 |
28 | override fun containsValue(value: V): Boolean = synchronized(lock) { super.containsValue(value) }
29 |
30 | override fun get(key: K): V? = synchronized(lock) { super.get(key) }
31 |
32 | override fun put(key: K, value: V): V? = synchronized(lock) { super.put(key, value) }
33 |
34 | override fun remove(key: K): V? = synchronized(lock) { super.remove(key) }
35 |
36 | override fun putAll(from: Map) = synchronized(lock) { super.putAll(from) }
37 |
38 | override fun clear() = synchronized(lock) { super.clear() }
39 |
40 | override fun toString(): String = synchronized(lock) {
41 | val sb = StringBuilder()
42 | for ((key, value) in entries) {
43 | sb.append(String.format("%s:%s ", key, value))
44 | }
45 | sb.toString()
46 | }
47 |
48 | override val keys: MutableSet
49 | get() = synchronized(lock) { super.keys }
50 | // 新增:获取不可变的键集合(避免外部迭代修改)
51 |
52 | override val values: MutableCollection
53 | get() = synchronized(lock) { super.values }
54 |
55 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
12 |
15 |
19 |
23 |
27 |
28 |
29 |
30 |
41 |
44 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/bluetooth/BleReadOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.bluetooth
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.BluetoothGattCharacteristic
5 | import com.huyuhui.fastble.callback.BleReadCallback
6 | import com.huyuhui.fastble.common.TimeoutTask
7 | import com.huyuhui.fastble.exception.BleException
8 |
9 | @SuppressLint("MissingPermission")
10 | internal class BleReadOperator(
11 | bleBluetooth: BleBluetooth,
12 | timeout: Long,
13 | uuidService: String,
14 | uuidCharacteristic: String
15 | ) : BleCharacteristicOperator(bleBluetooth, timeout, uuidService, uuidCharacteristic) {
16 | var bleReadCallback: BleReadCallback? = null
17 | private set
18 |
19 | /**
20 | * read
21 | */
22 | fun readCharacteristic(bleReadCallback: BleReadCallback?) {
23 | if (mCharacteristic != null
24 | && mCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_READ > 0
25 | ) {
26 | this.bleReadCallback = bleReadCallback
27 | timeOutTask.start(this)
28 | bleBluetooth.addReadOperator(key, this)
29 | if (!mBluetoothGatt!!.readCharacteristic(mCharacteristic)) {
30 | removeTimeOut()
31 | bleReadCallback?.onReadFailure(
32 | bleDevice,
33 | mCharacteristic,
34 | BleException.OtherException(
35 | BleException.CHARACTERISTIC_ERROR,
36 | "gatt readCharacteristic fail"
37 | )
38 | )
39 | }
40 | } else {
41 | bleReadCallback?.onReadFailure(
42 | bleDevice,
43 | mCharacteristic,
44 | BleException.OtherException(
45 | BleException.CHARACTERISTIC_NOT_SUPPORT,
46 | "this characteristic not support read!"
47 | )
48 | )
49 | }
50 | }
51 |
52 | override fun onTimeout(
53 | task: TimeoutTask,
54 | e: Throwable?,
55 | isActive: Boolean
56 | ) {
57 | bleReadCallback?.onReadFailure(bleDevice, mCharacteristic, BleException.TimeoutException())
58 | }
59 |
60 | override fun destroy() {
61 | super.destroy()
62 | bleReadCallback = null
63 | }
64 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-zh/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FastBle
3 | 搜索
4 | 停止
5 |
6 | 提示
7 | 当前手机需要打开定位服务才能搜索蓝牙
8 | 去设置
9 | 取消
10 |
11 | 展开搜索设置
12 | 收起搜索设置
13 | 连接失败
14 | 连接断开
15 | 连接中断
16 | 请先打开蓝牙
17 | 选择操作
18 | Characteristic
19 | 数据:
20 | Read
21 | Write
22 | Write No Response
23 | Write Signed
24 | indicate
25 | notification
26 | 打开通知
27 | 关闭通知
28 | Service列表
29 | Characteristic列表
30 | 控制
31 | 设置广播名:
32 | MAC:
33 | Service
34 | Service type (Primary service)
35 | Secondary type (Secondary service)
36 | Service type (main service)
37 | 在下面设置过滤条件, 可以为空,多个数据用英文逗号隔开
38 | 输入广播名
39 | 输入设备MAC
40 | 输入设备UUID,16位或者128位
41 | 输入16进制格式的数据
42 | 连接
43 | 已连接
44 | 断开
45 | Enter
46 | 操作
47 | 蓝牙权限被禁止
48 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace = 'com.huyuhui.blesample'
8 | compileSdk = 35
9 |
10 | defaultConfig {
11 | applicationId "com.huyuhui.blesample"
12 | minSdk = 23
13 | targetSdk = 35
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary = true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_17
31 | targetCompatibility JavaVersion.VERSION_17
32 | }
33 | kotlin {
34 | jvmToolchain(17) // 设置 JVM 17
35 | }
36 | buildFeatures {
37 | viewBinding = true
38 | }
39 | packagingOptions {
40 | resources {
41 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
42 | }
43 | }
44 | }
45 |
46 | dependencies {
47 |
48 | implementation 'androidx.core:core-ktx:1.13.1'
49 | implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.22')
50 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
51 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
52 | testImplementation 'junit:junit:4.13.2'
53 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
55 | implementation "androidx.activity:activity-ktx:1.9.3"
56 | implementation 'androidx.appcompat:appcompat:1.7.0'
57 | implementation 'com.google.android.material:material:1.12.0'
58 | implementation 'com.github.getActivity:XXPermissions:18.3'
59 | implementation project(':library')
60 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
61 | implementation 'androidx.recyclerview:recyclerview:1.3.2'
62 | def nav_version = "2.8.3"
63 | implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
64 | implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
65 | implementation 'com.github.li-xiaojun:XPopup:2.10.0'
66 | // implementation 'com.google.guava:guava:30.1-jre'
67 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/bluetooth/BleOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.bluetooth
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.BluetoothGatt
5 | import com.huyuhui.fastble.common.TimeoutTask
6 | import com.huyuhui.fastble.data.BleDevice
7 | import com.huyuhui.fastble.exception.BleCoroutineExceptionHandler
8 | import com.huyuhui.fastble.utils.BleLog
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.SupervisorJob
12 | import kotlinx.coroutines.cancel
13 | import kotlinx.coroutines.job
14 | import java.util.UUID
15 |
16 | @SuppressLint("MissingPermission")
17 | internal abstract class BleOperator(
18 | protected val bleBluetooth: BleBluetooth,
19 | val timeout: Long
20 | ) :
21 | CoroutineScope by CoroutineScope(
22 | SupervisorJob(bleBluetooth.coroutineContext.job) + Dispatchers.Main
23 | + BleCoroutineExceptionHandler { _, throwable ->
24 | BleLog.e(
25 | "Bluetooth operation: a coroutine error has occurred. ${throwable.message}\n " +
26 | "Device:${bleBluetooth.bleDevice} \n"
27 | )
28 | }) {
29 | companion object {
30 | @JvmStatic
31 | val UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR =
32 | "00002902-0000-1000-8000-00805f9b34fb"
33 |
34 |
35 | const val WRITE_TYPE_DEFAULT = -1
36 | }
37 |
38 | /**
39 | * 操作的数据
40 | */
41 | val bleDevice: BleDevice
42 | get() = bleBluetooth.bleDevice
43 | val mBluetoothGatt: BluetoothGatt?
44 | get() = bleBluetooth.bluetoothGatt
45 |
46 | abstract fun onTimeout(task: TimeoutTask, e: Throwable?, isActive: Boolean)
47 |
48 | protected val timeOutTask = TimeoutTask(
49 | timeout, object : TimeoutTask.OnResultCallBack {
50 | override fun onError(task: TimeoutTask, e: Throwable?, isActive: Boolean) {
51 | super.onError(task, e, isActive)
52 | onTimeout(task, e, isActive)
53 | }
54 | }
55 | )
56 |
57 | protected fun fromUUID(uuid: String): UUID? {
58 | return try {
59 | UUID.fromString(uuid)
60 | } catch (_: IllegalArgumentException) {
61 | null
62 | }
63 |
64 | }
65 |
66 | fun hasTask(): Boolean {
67 | return timeOutTask.hasTask()
68 | }
69 |
70 | fun removeTimeOut() {
71 | timeOutTask.success()
72 | }
73 |
74 | open fun destroy() {
75 | timeOutTask.onTimeoutResultCallBack = null
76 | cancel()
77 | }
78 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/common/BluetoothChangedObserver.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.common
2 |
3 | import android.bluetooth.BluetoothAdapter
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.IntentFilter
8 | import com.huyuhui.fastble.BleManager
9 | import com.huyuhui.fastble.scan.BleScanner
10 | import java.lang.ref.WeakReference
11 |
12 |
13 | class BluetoothChangedObserver {
14 | private var mBleReceiver: BleReceiver? = null
15 | var bleStatusCallback: BleStatusCallback? = null
16 |
17 | interface BleStatusCallback {
18 | fun onStateOn()
19 | fun onStateTurningOn()
20 | fun onStateOff()
21 | fun onStateTurningOff()
22 | }
23 |
24 | fun registerReceiver(context: Context) {
25 | mBleReceiver = BleReceiver(this)
26 | val filter = IntentFilter()
27 | filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
28 | context.registerReceiver(mBleReceiver, filter)
29 | }
30 |
31 | fun unregisterReceiver(context: Context) {
32 | if (mBleReceiver != null) {
33 | try {
34 | context.unregisterReceiver(mBleReceiver)
35 | bleStatusCallback = null
36 | } catch (e: Exception) {
37 | e.printStackTrace()
38 | }
39 | }
40 | }
41 |
42 | class BleReceiver(bluetoothChangedObserver: BluetoothChangedObserver) : BroadcastReceiver() {
43 | private var mObserverWeakReference: WeakReference = WeakReference(bluetoothChangedObserver)
44 |
45 | override fun onReceive(context: Context?, intent: Intent?) {
46 | if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
47 | val observer = mObserverWeakReference.get()
48 | when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
49 | BluetoothAdapter.STATE_OFF -> {
50 | observer?.bleStatusCallback?.onStateOff()
51 | BleScanner.stopLeScan()
52 | BleManager.multipleBluetoothController.onBleOff()
53 | }
54 |
55 | BluetoothAdapter.STATE_TURNING_OFF -> {
56 | observer?.bleStatusCallback?.onStateTurningOff()
57 | }
58 |
59 | BluetoothAdapter.STATE_ON -> {
60 | observer?.bleStatusCallback?.onStateOn()
61 | }
62 |
63 | BluetoothAdapter.STATE_TURNING_ON -> {
64 | observer?.bleStatusCallback?.onStateTurningOn()
65 | }
66 | }
67 | }
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/AppUtils.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.*
5 | import android.bluetooth.BluetoothAdapter
6 | import android.bluetooth.BluetoothManager
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.content.pm.PackageManager
10 | import android.location.LocationManager
11 | import android.provider.Settings
12 | import androidx.activity.ComponentActivity
13 | import androidx.activity.result.ActivityResultLauncher
14 | import com.hjq.permissions.Permission
15 | import com.hjq.permissions.XXPermissions
16 |
17 |
18 | object AppUtils {
19 | @SuppressLint("MissingPermission")
20 | fun enableBluetooth(
21 | activity: ComponentActivity,
22 | launcher: ActivityResultLauncher
23 | ) {
24 | val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
25 | if (XXPermissions.isGranted(activity, *Permission.Group.BLUETOOTH)) {
26 | launcher.launch(intent)
27 | }
28 | }
29 |
30 | fun isSupportBle(context: Context?): Boolean {
31 | if (context == null || !context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
32 | return false
33 | }
34 | context.getSystemService(Context.BLUETOOTH_SERVICE)?.let {
35 | return (it as BluetoothManager).adapter != null
36 | }
37 | return false
38 | }
39 |
40 | fun isBleEnable(context: Context): Boolean {
41 | if (!isSupportBle(context)) {
42 | return false
43 | }
44 | val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
45 | return manager.adapter.isEnabled
46 | }
47 |
48 | fun isSupportGPS(context: Context): Boolean {
49 | return context.getSystemService(Context.LOCATION_SERVICE) != null
50 | }
51 |
52 | /**
53 | * 判断GPS是否开启,GPS或者AGPS开启一个就认为是开启的
54 | *
55 | * @param context
56 | * @return true 表示开启
57 | */
58 | fun isOPenGPS(context: Context): Boolean {
59 | if (!isSupportGPS(context)) return false
60 | val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
61 | // 通过GPS卫星定位,定位级别可以精确到街(通过24颗卫星定位,在室外和空旷的地方定位准确、速度快)
62 | val gps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
63 | // 通过WLAN或移动网络(3G/2G)确定的位置(也称作AGPS,辅助GPS定位。主要用于在室内或遮盖物(建筑群或茂密的深林等)密集的地方定位)
64 | val network = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
65 | return gps || network
66 | }
67 |
68 | fun openGPS(activity: Activity) {
69 | val intent =
70 | Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
71 | activity.startActivity(intent)
72 | }
73 |
74 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/scan_filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
20 |
21 |
26 |
27 |
32 |
33 |
34 |
39 |
40 |
45 |
46 |
47 |
53 |
54 |
59 |
60 |
67 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FastBle
3 | Scan
4 | Stop
5 |
6 | Prompt
7 | Current mobile phone scanning Bluetooth needs to open the positioning function
8 | Go to settings
9 | Cancel
10 |
11 | Expand search settings
12 | Retrieve search settings
13 | connection failed
14 | disconnect
15 | Connection Broken
16 | Please turn on Bluetooth first
17 | Select operation type
18 | Characteristic
19 | Data changes:
20 | Read
21 | Write
22 | Write No Response
23 | Write Signed
24 | indicate
25 | notification
26 | Open notification
27 | Close notification
28 | Service list
29 | Characteristic list
30 | Console
31 | Device broadcast name:
32 | MAC:
33 | Service
34 | Service type (Primary service)
35 | Secondary type (Secondary service)
36 | Service type (main service)
37 | Below, you can configure the conditions that you need to scan the device, which can be empty,separated by English commas
38 | Enter a broadcast name for Bluetooth devices
39 | Enter Bluetooth device MAC
40 | Enter Bluetooth device UUID,16 bits or 128 bits
41 | Please enter the HEX format command
42 | Connect
43 | Connected
44 | Disconnect
45 | Enter
46 | Detail
47 | The Bluetooth permission is not obtained
48 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/exception/BleException.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.exception
2 |
3 | import android.bluetooth.BluetoothGatt
4 |
5 | sealed class BleException(open val code: Int, message: String) : Throwable(message) {
6 | companion object {
7 | @JvmStatic
8 | val ERROR_CODE_TIMEOUT = 100
9 |
10 | @JvmStatic
11 | val ERROR_CODE_GATT = 101
12 |
13 | @JvmStatic
14 | val ERROR_CODE_OTHER = 102
15 |
16 | @JvmStatic
17 | val NOT_SUPPORT_BLE = 2005
18 |
19 | @JvmStatic
20 | val BLUETOOTH_NOT_ENABLED = 2006
21 |
22 | @JvmStatic
23 | val DEVICE_NULL = 2007
24 |
25 | @JvmStatic
26 | val DEVICE_NOT_CONNECT = 2008
27 |
28 | @JvmStatic
29 | val DATA_NULL = 2009
30 |
31 | @JvmStatic
32 | val GATT_NULL = 2010
33 |
34 | @JvmStatic
35 | val CHARACTERISTIC_NOT_SUPPORT = 2011
36 |
37 | @JvmStatic
38 | val CHARACTERISTIC_ERROR = 2012
39 |
40 | @JvmStatic
41 | val DESCRIPTOR_NULL = 2013
42 |
43 | @JvmStatic
44 | val DESCRIPTOR_ERROR = 2014
45 |
46 | @JvmStatic
47 | val COROUTINE_SCOPE_CANCELLED = 2015
48 |
49 | // @JvmStatic
50 | // val DEVICE_HAS_CONNECTED = 2016
51 | }
52 |
53 | override fun toString(): String {
54 | return "BleException(code=$code, message='$message')"
55 | }
56 |
57 | class OtherException(
58 | override val code: Int = ERROR_CODE_OTHER,
59 | message: String,
60 | ) : BleException(code, message) {
61 | override fun toString(): String {
62 | return "OtherException(code=$code, message='$message')"
63 | }
64 | }
65 |
66 | class TimeoutException(message: String = "Timeout Exception Occurred!") :
67 | BleException(ERROR_CODE_TIMEOUT, message) {
68 | override fun toString(): String {
69 | return "TimeoutException(code=$code, message='$message')"
70 | }
71 | }
72 |
73 | class DiscoverException(
74 | override val code: Int = ERROR_CODE_GATT,
75 | message: String = "GATT discover services exception occurred!"
76 | ) : BleException(code, message) {
77 | override fun toString(): String {
78 | return "DiscoverException(code=$code, message='$message')"
79 | }
80 | }
81 |
82 | class ConnectException(
83 | val bluetoothGatt: BluetoothGatt?,
84 | val gattStatus: Int,
85 | ) : BleException(ERROR_CODE_GATT, "Gatt Exception Occurred!") {
86 | override fun toString(): String {
87 | return "ConnectException(bluetoothGatt=$bluetoothGatt, gattStatus=$gattStatus) code=$code, message='$message'"
88 | }
89 | }
90 |
91 | class GattException(
92 | val bluetoothGatt: BluetoothGatt?,
93 | val gattStatus: Int,
94 | ) : BleException(ERROR_CODE_GATT, "Gatt Exception Occurred!") {
95 | override fun toString(): String {
96 | return "GattException(bluetoothGatt=$bluetoothGatt, gattStatus=$gattStatus) code=$code, message='$message'"
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/operate/OperateActivity.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample.operate
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.activity.addCallback
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.core.view.ViewCompat
9 | import androidx.core.view.WindowInsetsCompat
10 | import androidx.core.view.updatePadding
11 | import androidx.navigation.fragment.NavHostFragment
12 | import androidx.navigation.ui.AppBarConfiguration
13 | import androidx.navigation.ui.setupWithNavController
14 | import com.huyuhui.blesample.R
15 | import com.huyuhui.blesample.databinding.ActivityOperateBinding
16 | import com.huyuhui.fastble.BleManager
17 | import com.huyuhui.fastble.data.BleDevice
18 |
19 | class OperateActivity : AppCompatActivity() {
20 | private var bleDevice: BleDevice? = null
21 | private lateinit var binding: ActivityOperateBinding
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | binding = ActivityOperateBinding.inflate(layoutInflater)
25 | setContentView(binding.root)
26 | bleDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
27 | intent.getParcelableExtra("device", BleDevice::class.java)
28 | } else {
29 | @Suppress("DEPRECATION")
30 | intent.getParcelableExtra("device")
31 | }
32 | if (bleDevice == null) {
33 | finish()
34 | }
35 | ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { v: View, insets: WindowInsetsCompat ->
36 | insets.getInsets(WindowInsetsCompat.Type.statusBars()).run {
37 | v.updatePadding(left, top, right, bottom)
38 | }
39 | return@setOnApplyWindowInsetsListener insets
40 | }
41 | ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v: View, insets: WindowInsetsCompat ->
42 | insets.getInsets(WindowInsetsCompat.Type.navigationBars()).run {
43 | v.updatePadding(left, top, right, bottom)
44 | }
45 | return@setOnApplyWindowInsetsListener insets
46 | }
47 | setSupportActionBar(binding.toolbar)
48 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
49 | val navHostFragment =
50 | supportFragmentManager.findFragmentById(R.id.nav_operate_fragment) as NavHostFragment
51 | val navController = navHostFragment.navController
52 | val bundle = Bundle().apply {
53 | putParcelable("device", bleDevice)
54 | }
55 | navController.setGraph(R.navigation.operator_nav, bundle)
56 |
57 | val appBarConfiguration = AppBarConfiguration(topLevelDestinationIds = setOf()) {
58 | finish()
59 | false
60 | }
61 | binding.toolbar.setupWithNavController(navController, appBarConfiguration)
62 |
63 | onBackPressedDispatcher.addCallback(this, true) {
64 | if (!navController.popBackStack()) {
65 | remove()
66 | finish()
67 | }
68 | }
69 | }
70 |
71 | override fun onDestroy() {
72 | super.onDestroy()
73 | BleManager.clearCharacterCallback(bleDevice)
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/operate/ServiceFragment.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample.operate
2 |
3 | import android.os.Build
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.Toast
9 | import androidx.fragment.app.Fragment
10 | import androidx.navigation.fragment.findNavController
11 | import com.huyuhui.fastble.BleManager
12 | import com.huyuhui.fastble.data.BleDevice
13 | import com.huyuhui.blesample.R
14 | import com.huyuhui.blesample.adapter.ServiceAdapter
15 | import com.huyuhui.blesample.databinding.FragmentServiceBinding
16 |
17 |
18 | class ServiceFragment : Fragment() {
19 | private var _binding: FragmentServiceBinding? = null
20 | private val binding
21 | get() = _binding!!
22 | private var bleDevice: BleDevice? = null
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | bleDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
26 | arguments?.getParcelable("device", BleDevice::class.java)
27 | } else {
28 | @Suppress("DEPRECATION")
29 | arguments?.getParcelable("device")
30 | }
31 | }
32 |
33 | override fun onCreateView(
34 | inflater: LayoutInflater, container: ViewGroup?,
35 | savedInstanceState: Bundle?
36 | ): View {
37 | _binding = FragmentServiceBinding.inflate(layoutInflater, container, false)
38 | return binding.root
39 | }
40 |
41 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
42 | super.onViewCreated(view, savedInstanceState)
43 | binding.tvName.text = bleDevice?.name
44 | binding.tvMac.text = bleDevice?.mac
45 | BleManager.getBluetoothGattServices(bleDevice)?.let {
46 | val adapter = ServiceAdapter(requireContext(), it)
47 | binding.lv.setAdapter(adapter)
48 | binding.lv.setOnChildClickListener { _, _, groupPosition, childPosition, _ ->
49 | val bundle = Bundle().apply {
50 | putParcelable("device", bleDevice)
51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
52 | putParcelable(
53 | "characteristic",
54 | it[groupPosition].characteristics[childPosition]
55 | )
56 | } else {
57 | putString(
58 | "serviceUUID",
59 | it[groupPosition].uuid.toString()
60 | )
61 | putString(
62 | "characteristicUUID",
63 | it[groupPosition].characteristics[childPosition].uuid.toString()
64 | )
65 | }
66 | }
67 | findNavController().navigate(R.id.action_serviceFragment_to_operateFragment, bundle)
68 | true
69 | }
70 | } ?: Toast.makeText(requireContext(), R.string.connection_broken, Toast.LENGTH_SHORT).show()
71 | }
72 |
73 | override fun onDestroyView() {
74 | super.onDestroyView()
75 | _binding = null
76 | }
77 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/queue/Queue.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.queue
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.Job
6 | import kotlinx.coroutines.cancel
7 | import kotlinx.coroutines.channels.Channel
8 | import kotlinx.coroutines.delay
9 | import kotlinx.coroutines.isActive
10 | import kotlinx.coroutines.job
11 | import kotlinx.coroutines.launch
12 | import kotlinx.coroutines.withContext
13 | import kotlinx.coroutines.withTimeoutOrNull
14 | import java.util.concurrent.PriorityBlockingQueue
15 |
16 | internal abstract class Queue : CoroutineScope {
17 | /**
18 | * 某些情况发送失败太快,导致还没有调用receive的情况就开始trySend一直失败,给一个缓冲
19 | */
20 | private val channel = Channel(1)
21 | private val taskComparator = Comparator { task1, task2 ->
22 | if (task2.priority != task1.priority) {
23 | task2.priority.compareTo(task1.priority) // 逆序排列,优先级高的排在前面
24 | } else {
25 | task1.sequenceNum.compareTo(task2.sequenceNum)
26 | }
27 | }
28 | private val priorityQueue: PriorityBlockingQueue =
29 | PriorityBlockingQueue(10, taskComparator)
30 | private var job: Job? = null
31 |
32 | val remainSize
33 | get() = priorityQueue.size
34 |
35 | fun offer(task: T): Boolean {
36 | return priorityQueue.offer(task)
37 | }
38 |
39 | fun startProcessingTasks() {
40 | resume()
41 | }
42 |
43 | abstract fun execute(task: T, channel: Channel)
44 | fun clear() {
45 | priorityQueue.clear()
46 | }
47 |
48 | fun remove(task: T): Boolean {
49 | return priorityQueue.remove(task)
50 | }
51 |
52 | fun pause() {
53 | job?.cancel()
54 | }
55 |
56 | /**
57 | * receive 可以收到在调用之前通过 trySend 发送的数据,前提是 trySend 成功发送了数据,并且 Channel 没有被关闭
58 | * 可能出现trySend比Receive调用更早的情况
59 | * 如果channel没有缓存,就会出现trySend失败,当continuous为true的时候,就会一直等待withTimeoutOrNull超时,然后取消receive
60 | * 等待priorityQueue.take()下一个任务来的时候,上一次receive已经被取消,又会卡在withTimeoutOrNull,如此循环。如果没有超时逻辑,会一直卡在receive
61 | *
62 | **/
63 | fun resume() {
64 | if (job?.isActive == true) {
65 | return
66 | }
67 | job = launch(Dispatchers.IO) {
68 | while (isActive) {
69 | val task = priorityQueue.take()
70 |
71 | withContext(Dispatchers.Main) {
72 | //这里会在回调里面trySend
73 | execute(task, channel)
74 |
75 | }
76 | if (task.continuous) {
77 | if (task.timeout > 0) {
78 | withTimeoutOrNull(task.timeout) {
79 | do {
80 | val result = channel.receive()
81 | } while (result.task != task)
82 | return@withTimeoutOrNull
83 | }
84 | } else {
85 | do {
86 | val result = channel.receive()
87 | } while (result.task != task)
88 | }
89 | }
90 | delay(task.delay)
91 | }
92 | }
93 | }
94 |
95 | fun destroy() {
96 | coroutineContext.job.invokeOnCompletion {
97 | channel.close()
98 | }
99 | clear()
100 | cancel()
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_ble_device.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
28 |
29 |
38 |
39 |
50 |
51 |
63 |
64 |
72 |
73 |
80 |
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/adapter/BleDeviceAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample.adapter
2 |
3 | import android.annotation.SuppressLint
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.huyuhui.fastble.BleManager
9 | import com.huyuhui.fastble.data.BleDevice
10 | import com.huyuhui.blesample.R
11 | import com.huyuhui.blesample.databinding.ItemBleDeviceBinding
12 |
13 | /**
14 | */
15 | class BleDeviceAdapter(
16 | private val values: List
17 | ) : RecyclerView.Adapter() {
18 | var onItemButtonClickListener: OnItemButtonClickListener? = null
19 | override fun onCreateViewHolder(
20 | parent: ViewGroup,
21 | viewType: Int
22 | ): ViewHolder {
23 | return ViewHolder(
24 | ItemBleDeviceBinding.inflate(
25 | LayoutInflater.from(parent.context),
26 | parent,
27 | false,
28 | ), onItemButtonClickListener
29 | )
30 |
31 | }
32 |
33 | @SuppressLint("MissingPermission")
34 | override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) {
35 | if (payloads.isNotEmpty()) {
36 | val map = payloads[0] as Map<*, *>
37 | val rssi = map["rssi"]
38 | holder.binding.tvRssi.text = rssi.toString()
39 | } else {
40 | super.onBindViewHolder(holder, position, payloads)
41 | }
42 |
43 | }
44 |
45 | @SuppressLint("MissingPermission")
46 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
47 | val item = values[position]
48 | holder.binding.tvName.text = item.name
49 | holder.binding.tvMac.text = item.mac
50 | holder.binding.tvRssi.text = item.rssi.toString()
51 | if (BleManager.isConnected(item)) {
52 | holder.binding.btnConnection.apply {
53 | text = context.getString(R.string.disconnect)
54 | setTextColor(context.getColor(R.color.colorPrimary))
55 | }
56 | holder.binding.btnDetail.visibility = View.VISIBLE
57 | holder.binding.tvRssi.visibility = View.GONE
58 | holder.binding.imgRssi.visibility = View.GONE
59 | } else {
60 | holder.binding.btnConnection.apply {
61 | text = context.getString(R.string.connect)
62 | setTextColor(context.getColor(R.color.black))
63 | }
64 | holder.binding.btnDetail.visibility = View.GONE
65 | holder.binding.tvRssi.visibility = View.VISIBLE
66 | holder.binding.imgRssi.visibility = View.VISIBLE
67 | }
68 | }
69 |
70 | override fun getItemCount(): Int = values.size
71 |
72 |
73 | class ViewHolder(
74 | val binding: ItemBleDeviceBinding,
75 | private val onItemButtonClickListener: OnItemButtonClickListener? = null
76 | ) :
77 | RecyclerView.ViewHolder(binding.root) {
78 |
79 | init {
80 | binding.btnConnection.setOnClickListener {
81 | onItemButtonClickListener?.onBtnConnectionClick(bindingAdapterPosition)
82 | }
83 | binding.btnDetail.setOnClickListener {
84 | onItemButtonClickListener?.onBtnDetailClick(bindingAdapterPosition)
85 | }
86 | }
87 | }
88 |
89 | interface OnItemButtonClickListener {
90 | fun onBtnConnectionClick(position: Int)
91 | fun onBtnDetailClick(position: Int)
92 | }
93 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/common/TimeoutTask.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.common
2 |
3 | import kotlinx.coroutines.CancellationException
4 | import kotlinx.coroutines.CoroutineExceptionHandler
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Job
7 | import kotlinx.coroutines.delay
8 | import kotlinx.coroutines.job
9 | import kotlinx.coroutines.launch
10 | import kotlinx.coroutines.supervisorScope
11 |
12 | @Suppress("unused")
13 | class TimeoutTask(
14 | private val delayTime: Long,
15 | var onTimeoutResultCallBack: OnResultCallBack? = null
16 | ) {
17 | private var job: Job? = null
18 | private var isSkip: Boolean = false
19 | private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
20 | when (throwable) {
21 | is TimeoutThrowable.SkipError -> {
22 | onTimeoutResultCallBack?.onSkip(this)
23 | }
24 |
25 | is TimeoutThrowable.Success -> {
26 | onTimeoutResultCallBack?.onSuccess(this)
27 | }
28 |
29 | is TimeoutThrowable.ActiveError -> {
30 | onTimeoutResultCallBack?.onError(this, throwable, true)
31 | }
32 |
33 | is TimeoutThrowable.TimeOutError -> {
34 | onTimeoutResultCallBack?.onError(this, throwable, false)
35 | }
36 |
37 | else -> {
38 |
39 | }
40 | }
41 | }
42 | fun start(scope: CoroutineScope){
43 | job?.apply {
44 | if (isActive) {
45 | isSkip = true
46 | cancel(
47 | CancellationException(
48 | TimeoutThrowable.SkipError.message,
49 | TimeoutThrowable.SkipError
50 | )
51 | )
52 | } else {
53 | isSkip = false
54 | }
55 | }
56 | job = scope.launch {
57 | supervisorScope {
58 | launch(exceptionHandler) {
59 | coroutineContext.job.invokeOnCompletion {
60 | onTimeoutResultCallBack?.onFinal(this@TimeoutTask, isSkip)
61 | }
62 | onTimeoutResultCallBack?.onStart(this@TimeoutTask)
63 | try {
64 | delay(delayTime)
65 | throw TimeoutThrowable.TimeOutError
66 | } catch (e: CancellationException) {
67 | throw e.cause ?: Throwable()
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 | fun fail() {
75 | job?.takeIf { it.isActive }?.cancel(
76 | CancellationException(
77 | TimeoutThrowable.ActiveError.message,
78 | TimeoutThrowable.ActiveError
79 | )
80 | )
81 | }
82 |
83 | fun success() {
84 | job?.takeIf { it.isActive }?.cancel(
85 | CancellationException(
86 | TimeoutThrowable.Success.message,
87 | TimeoutThrowable.Success
88 | )
89 | )
90 | }
91 |
92 | fun hasTask(): Boolean {
93 | return job?.isActive ?: false
94 | }
95 |
96 | /**
97 | * 不回调成功或者失败
98 | */
99 | fun cancel() {
100 | job?.takeIf { it.isActive }?.cancel()
101 | }
102 |
103 | interface OnResultCallBack {
104 | fun onStart(task: TimeoutTask) {}
105 | fun onError(task: TimeoutTask, e: Throwable?, isActive: Boolean) {}
106 | fun onSuccess(task: TimeoutTask) {}
107 | fun onSkip(task: TimeoutTask) {}
108 | fun onFinal(task: TimeoutTask, isSkip: Boolean) {}
109 | }
110 |
111 | sealed class TimeoutThrowable(message: String? = null) : Throwable(message) {
112 | object Success : TimeoutThrowable("success") {
113 | private fun readResolve(): Any = Success
114 | }
115 |
116 | object ActiveError : TimeoutThrowable("error") {
117 | private fun readResolve(): Any = ActiveError
118 | }
119 |
120 | object TimeOutError : TimeoutThrowable("time out") {
121 | private fun readResolve(): Any = TimeOutError
122 | }
123 |
124 | object SkipError : TimeoutThrowable("skip") {
125 | private fun readResolve(): Any = SkipError
126 | }
127 | }
128 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/utils/HexUtil.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.utils
2 |
3 | import java.util.Locale
4 | @Suppress("unused")
5 | object HexUtil {
6 | private val DIGITS_LOWER = charArrayOf(
7 | '0', '1', '2', '3', '4', '5',
8 | '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
9 | )
10 |
11 | private val DIGITS_UPPER = charArrayOf(
12 | '0', '1', '2', '3', '4', '5',
13 | '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
14 | )
15 |
16 | @JvmStatic
17 | fun encodeHex(data: ByteArray?): CharArray? {
18 | return encodeHex(data, true)
19 | }
20 |
21 | @JvmStatic
22 | fun encodeHex(data: ByteArray?, toLowerCase: Boolean): CharArray? {
23 | return encodeHex(data, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER)
24 | }
25 |
26 | private fun encodeHex(data: ByteArray?, toDigits: CharArray): CharArray? {
27 | if (data == null) return null
28 | val l = data.size
29 | val out = CharArray(l shl 1)
30 | var i = 0
31 | var j = 0
32 | while (i < l) {
33 | out[j++] = toDigits[(0xF0 and data[i].toInt()) ushr 4]
34 | out[j++] = toDigits[0x0F and data[i].toInt()]
35 | i++
36 | }
37 | return out
38 | }
39 |
40 | @JvmStatic
41 | fun encodeHexStr(data: ByteArray?): String? {
42 | return encodeHexStr(data, true)
43 | }
44 |
45 | @JvmStatic
46 | fun encodeHexStr(data: ByteArray?, toLowerCase: Boolean): String? {
47 | return encodeHexStr(data, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER)
48 | }
49 |
50 |
51 | private fun encodeHexStr(data: ByteArray?, toDigits: CharArray): String? {
52 | return encodeHex(data, toDigits)?.let { String(it) }
53 | }
54 |
55 | @JvmStatic
56 | fun formatHexString(data: ByteArray?): String? {
57 | return formatHexString(data, false)
58 | }
59 |
60 | @JvmStatic
61 | fun formatHexString(data: ByteArray?, addSpace: Boolean): String? {
62 | if (data == null || data.isEmpty()) return null
63 | val sb = StringBuilder()
64 | for (i in data.indices) {
65 | var hex = Integer.toHexString(data[i].toInt() and 0xFF)
66 | if (hex.length == 1) {
67 | hex = "0$hex"
68 | }
69 | sb.append(hex)
70 | if (addSpace) sb.append(" ")
71 | }
72 | return sb.toString().trim { it <= ' ' }
73 | }
74 |
75 | fun decodeHex(data: CharArray): ByteArray {
76 | val len = data.size
77 | if (len and 0x01 != 0) {
78 | throw RuntimeException("Odd number of characters.")
79 | }
80 | val out = ByteArray(len shr 1)
81 |
82 | // two characters form the hex value.
83 | var i = 0
84 | var j = 0
85 | while (j < len) {
86 | var f = toDigit(data[j], j) shl 4
87 | j++
88 | f = f or toDigit(data[j], j)
89 | j++
90 | out[i] = (f and 0xFF).toByte()
91 | i++
92 | }
93 | return out
94 | }
95 |
96 |
97 | private fun toDigit(ch: Char, index: Int): Int {
98 | val digit = ch.digitToIntOrNull(16) ?: -1
99 | if (digit == -1) {
100 | throw RuntimeException(
101 | "Illegal hexadecimal character " + ch
102 | + " at index " + index
103 | )
104 | }
105 | return digit
106 | }
107 |
108 | @JvmStatic
109 | fun hexStringToBytes(hexString: String?): ByteArray? {
110 | if (hexString.isNullOrEmpty()) {
111 | return null
112 | }
113 | var string = hexString.trim { it <= ' ' }
114 | string = string.uppercase(Locale.getDefault())
115 | val length = string.length / 2
116 | val hexChars = string.toCharArray()
117 | hexChars.forEach {
118 | if ("0123456789ABCDEF".indexOf(it) == -1) return null
119 | }
120 | val d = ByteArray(length)
121 | for (i in 0 until length) {
122 | val pos = i * 2
123 | d[i] =
124 | ((charToByte(hexChars[pos]).toInt() shl 4) or charToByte(hexChars[pos + 1]).toInt()).toByte()
125 | }
126 | return d
127 | }
128 |
129 | @JvmStatic
130 | fun charToByte(c: Char): Byte {
131 | return "0123456789ABCDEF".indexOf(c).toByte()
132 | }
133 |
134 | @JvmStatic
135 | fun extractData(data: ByteArray, position: Int): String? {
136 | return formatHexString(byteArrayOf(data[position]))
137 | }
138 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/scan/BleScanRuleConfig.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.scan
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.le.ScanFilter
5 | import android.bluetooth.le.ScanSettings
6 | import android.os.ParcelUuid
7 | import com.huyuhui.fastble.BleManager
8 | import java.util.UUID
9 |
10 | @Suppress("unused")
11 | class BleScanRuleConfig private constructor() {
12 |
13 | var mServiceUuids: List? = null
14 | private set
15 | var mDeviceMacs: List? = null
16 | private set
17 | var mDeviceNames: List? = null
18 | private set
19 | var mScanTimeOut = BleManager.DEFAULT_SCAN_TIME
20 | private set
21 | var mFuzzyName: Boolean = false
22 | private set
23 |
24 | private var scanSettings: ScanSettings? = null
25 |
26 | fun generateScanFilter(): List {
27 | val scanFilters = mutableListOf()
28 | mServiceUuids?.forEach {
29 | val build = ScanFilter.Builder().setServiceUuid(ParcelUuid(it))
30 | scanFilters.add(build.build())
31 | }
32 | if (!mFuzzyName) {
33 | mDeviceNames?.forEach {
34 | val build = ScanFilter.Builder().setDeviceName(it)
35 | scanFilters.add(build.build())
36 | }
37 | }
38 | mDeviceMacs?.forEach {
39 | val build = ScanFilter.Builder().setDeviceAddress(it)
40 | scanFilters.add(build.build())
41 | }
42 | return scanFilters
43 | }
44 |
45 | /**
46 | * public static final int CALLBACK_TYPE_ALL_MATCHES = 1; //寻找符合过滤条件的蓝牙广播,如果没有设置过滤条件,则返回全部广播包
47 | public static final int CALLBACK_TYPE_FIRST_MATCH = 2; //要设置过滤条件,不设置就不返回,首次匹配的设备的时候才会回调。
48 | public static final int CALLBACK_TYPE_MATCH_LOST = 4; // 要设置过滤条件,之前搜索过滤完符合条件,后面搜索的时候,没有找到了,当设备不再匹配过滤条件时,您可以触发警报或采取其他适当的措施。
49 | @NonNull
50 | public static final int MATCH_MODE_AGGRESSIVE = 1; //激进模式,即使信号强度微弱且持续时间内匹配的次数很少,硬件也会更快地确定匹配
51 | public static final int MATCH_MODE_STICKY = 2; //粘性模式,需要更高的信号强度和目击阈值,才能被确定匹配
52 | public static final int MATCH_NUM_FEW_ADVERTISEMENT = 2; //每个过滤器过滤少量的设备,取决于系统资源
53 | public static final int MATCH_NUM_MAX_ADVERTISEMENT = 3; //每个过滤器尽可能匹配更多的广播,取决于系统资源
54 | public static final int MATCH_NUM_ONE_ADVERTISEMENT = 1; //确定每个筛选条件要匹配的播发数量,因为这是稀缺的硬件资源。为每个筛选条件匹配一个广播
55 | public static final int PHY_LE_ALL_SUPPORTED = 255;
56 | public static final int SCAN_MODE_BALANCED = 1; //平衡模式
57 | public static final int SCAN_MODE_LOW_LATENCY = 2; //低延时扫描,高功耗模式(建议仅在应用程序在前台运行时才使用此模式。)
58 | public static final int SCAN_MODE_LOW_POWER = 0; //低功耗模式(默认扫描模式,如果扫描应用程序不在前台,则强制使用此模式。)
59 | public static final int SCAN_MODE_OPPORTUNISTIC = -1; //一种特殊的蓝牙 LE 扫描模式。使用此扫描模式的应用程序将被动监听其他扫描结果,而不会自行启动 BLE 扫描。
60 | */
61 | fun generateScanSettings(): ScanSettings {
62 | return if (scanSettings == null) {
63 | ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_BALANCED).build()
64 | } else {
65 | scanSettings!!
66 | }
67 | }
68 |
69 | class Builder {
70 | private var mServiceUuids: List? = null
71 | private var mDeviceNames: List? = null
72 | private var mDeviceMacs: List? = null
73 | private var mTimeOut = BleManager.DEFAULT_SCAN_TIME
74 | private var scanSettings: ScanSettings? = null
75 | private var mFuzzyName: Boolean = false
76 | fun setServiceUuids(uuids: List?): Builder {
77 | mServiceUuids = uuids
78 | return this
79 | }
80 |
81 | fun setDeviceName(vararg names: String, isFuzzy: Boolean = false): Builder {
82 | return setDeviceName(names.toList(), isFuzzy)
83 | }
84 |
85 | fun setDeviceName(names: List?, isFuzzy: Boolean = false): Builder {
86 | mDeviceNames = names
87 | mFuzzyName = isFuzzy
88 | return this
89 | }
90 |
91 | fun setDeviceMac(vararg macs: String): Builder {
92 | mDeviceMacs = macs.toList()
93 | return this
94 | }
95 |
96 |
97 | fun setScanTimeOut(timeOut: Long): Builder {
98 | mTimeOut = timeOut
99 | return this
100 | }
101 |
102 | fun setScanSettings(scanSettings: ScanSettings) {
103 | this.scanSettings = scanSettings
104 | }
105 | @SuppressLint("PrivateApi")
106 | fun applyConfig(config: BleScanRuleConfig) {
107 | config.mServiceUuids = mServiceUuids
108 | config.mDeviceNames = mDeviceNames
109 | config.mDeviceMacs = mDeviceMacs
110 | config.mScanTimeOut = mTimeOut
111 | config.scanSettings = scanSettings
112 | config.mFuzzyName = mFuzzyName
113 | }
114 |
115 | fun build(): BleScanRuleConfig {
116 | val config = BleScanRuleConfig()
117 | applyConfig(config)
118 | return config
119 | }
120 | }
121 |
122 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/bluetooth/BleWriteOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.bluetooth
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.BluetoothGattCharacteristic
5 | import android.os.Build
6 | import com.huyuhui.fastble.callback.BleWriteCallback
7 | import com.huyuhui.fastble.common.TimeoutTask
8 | import com.huyuhui.fastble.exception.BleException
9 |
10 | @SuppressLint("MissingPermission")
11 | internal class BleWriteOperator(
12 | bleBluetooth: BleBluetooth,
13 | timeout: Long,
14 | uuidService: String,
15 | uuidCharacteristic: String
16 | ) : BleCharacteristicOperator(bleBluetooth, timeout, uuidService, uuidCharacteristic) {
17 | var bleWriteCallback: BleWriteCallback? = null
18 | private set
19 |
20 | var data: ByteArray? = null
21 | private set
22 |
23 | @Suppress("DEPRECATION")
24 | fun writeCharacteristic(
25 | data: ByteArray?,
26 | bleWriteCallback: BleWriteCallback?,
27 | writeType: Int,
28 | ) {
29 | this.data = data
30 | if (data == null || data.isEmpty()) {
31 | bleWriteCallback?.onWriteFailure(
32 | bleDevice, mCharacteristic,
33 | BleException.OtherException(
34 | BleException.DATA_NULL,
35 | "the data to be written is empty"
36 | ),
37 | justWrite = data
38 | )
39 | return
40 | }
41 | if (mCharacteristic == null
42 | || mCharacteristic.properties and (BluetoothGattCharacteristic.PROPERTY_WRITE or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE) == 0
43 | ) {
44 | bleWriteCallback?.onWriteFailure(
45 | bleDevice, mCharacteristic,
46 | BleException.OtherException(
47 | BleException.CHARACTERISTIC_NOT_SUPPORT,
48 | "this characteristic not support write!"
49 | ),
50 | justWrite = data
51 | )
52 | return
53 | }
54 | val finalWriteType = if (writeType == WRITE_TYPE_DEFAULT) {
55 | if (mCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0) {
56 | BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
57 | } else if (mCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0) {
58 | BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
59 | } else {
60 | BluetoothGattCharacteristic.WRITE_TYPE_SIGNED
61 | }
62 | } else {
63 | writeType
64 | }
65 | mCharacteristic.writeType = finalWriteType
66 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
67 | this.bleWriteCallback = bleWriteCallback
68 | timeOutTask.start(this)
69 | bleBluetooth.addWriteOperator(key, this)
70 | mBluetoothGatt!!.writeCharacteristic(mCharacteristic, data, finalWriteType)
71 | //这里会触发一次,onCharacteristicWrite里面还会触发一次
72 | // val status = mBluetoothGatt!!.writeCharacteristic(mCharacteristic!!, data, writeType)
73 | // if (status != BluetoothStatusCodes.SUCCESS) {
74 | // removeTimeOut()
75 | // bleWriteCallback?.onWriteFailure(
76 | // BleException.OtherException("Updates the locally stored value of this characteristic fail"),
77 | // justWrite = data
78 | // )
79 | // }
80 | } else {
81 | if (mCharacteristic.setValue(data)) {
82 | this.bleWriteCallback = bleWriteCallback
83 | timeOutTask.start(this)
84 | bleBluetooth.addWriteOperator(key, this)
85 | mBluetoothGatt!!.writeCharacteristic(mCharacteristic)
86 | // if (!mBluetoothGatt!!.writeCharacteristic(mCharacteristic)) {
87 | // removeTimeOut()
88 | // bleWriteCallback?.onWriteFailure(
89 | // BleException.OtherException("gatt writeCharacteristic fail"),
90 | // justWrite = data
91 | // )
92 | // }
93 | } else {
94 | bleWriteCallback?.onWriteFailure(
95 | bleDevice, mCharacteristic,
96 | BleException.OtherException(
97 | BleException.CHARACTERISTIC_ERROR,
98 | "Updates the locally stored value of this characteristic fail"
99 | ),
100 | justWrite = data
101 | )
102 | }
103 | }
104 | }
105 |
106 | override fun onTimeout(
107 | task: TimeoutTask,
108 | e: Throwable?,
109 | isActive: Boolean
110 | ) {
111 | bleWriteCallback?.onWriteFailure(
112 | bleDevice,
113 | mCharacteristic,
114 | BleException.TimeoutException(),
115 | justWrite = data
116 | )
117 | }
118 |
119 | override fun destroy() {
120 | super.destroy()
121 | data = null
122 | bleWriteCallback = null
123 | }
124 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/scan/BleScanner.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.scan
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.le.ScanCallback
5 | import android.bluetooth.le.ScanResult
6 | import com.huyuhui.fastble.BleManager
7 | import com.huyuhui.fastble.callback.BleScanCallback
8 | import com.huyuhui.fastble.data.BleDevice
9 | import com.huyuhui.fastble.data.BleScanState
10 | import com.huyuhui.fastble.exception.BleMainScope
11 | import com.huyuhui.fastble.utils.BleLog
12 | import com.huyuhui.fastble.utils.HexUtil
13 | import kotlinx.coroutines.CoroutineScope
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.cancelChildren
16 | import kotlinx.coroutines.delay
17 | import kotlinx.coroutines.ensureActive
18 | import kotlinx.coroutines.launch
19 | import kotlinx.coroutines.sync.Mutex
20 | import kotlinx.coroutines.sync.withLock
21 | import kotlinx.coroutines.withContext
22 |
23 | @SuppressLint("MissingPermission")
24 | internal object BleScanner : ScanCallback(), CoroutineScope by BleMainScope({ _, throwable ->
25 | BleLog.e("BleScanner: a coroutine error has occurred ${throwable.message}")
26 | }) {
27 | var mBleScanState = BleScanState.STATE_IDLE
28 | private set
29 | private val bleScanRuleConfig
30 | get() = BleManager.bleScanRuleConfig
31 | var bleScanCallback: BleScanCallback? = null
32 | private val map = linkedMapOf()
33 | override fun onScanFailed(errorCode: Int) {
34 | super.onScanFailed(errorCode)
35 | BleLog.e("scan failed,errorCode = $errorCode")
36 | mBleScanState = BleScanState.STATE_IDLE
37 | bleScanCallback?.onScanStarted(false)
38 | }
39 |
40 | override fun onScanResult(callbackType: Int, result: ScanResult?) {
41 | super.onScanResult(callbackType, result)
42 | launch(Dispatchers.IO) {
43 | if (result == null) return@launch
44 | val bleDevice = BleDevice(result)
45 | if (bleScanRuleConfig.mFuzzyName && !bleScanRuleConfig.mDeviceNames.isNullOrEmpty()) {
46 | if (bleDevice.name == null) return@launch
47 | var hasFound = false
48 | bleScanRuleConfig.mDeviceNames?.forEach forEach@{
49 | if (bleDevice.name!!.contains(it, true)) {
50 | hasFound = true
51 | return@forEach
52 | }
53 | }
54 | if (!hasFound) return@launch
55 | }
56 | if (bleScanCallback?.onFilter(bleDevice) != false) {
57 | correctDeviceAndNextStep(bleDevice)
58 | }
59 | }
60 | }
61 |
62 | private val mutex = Mutex()
63 | private suspend fun correctDeviceAndNextStep(bleDevice: BleDevice) {
64 | mutex.withLock {
65 | ensureActive()
66 | if (!map.contains(bleDevice.key)) {
67 | BleLog.i(
68 | "device detected ------ name: ${bleDevice.name} mac: ${bleDevice.mac} " +
69 | " Rssi: ${bleDevice.rssi} scanRecord: ${
70 | HexUtil.formatHexString(
71 | bleDevice.scanRecord,
72 | true
73 | )
74 | }"
75 | )
76 | map[bleDevice.key] = bleDevice
77 | withContext(Dispatchers.Main) {
78 | bleScanCallback?.onLeScan(bleDevice, bleDevice, false)
79 | }
80 | } else {
81 | val oldDevice = map[bleDevice.key]
82 | map[bleDevice.key] = bleDevice
83 | withContext(Dispatchers.Main) {
84 | bleScanCallback?.onLeScan(oldDevice!!, bleDevice, true)
85 | }
86 | }
87 | }
88 |
89 | }
90 |
91 | @Synchronized
92 | fun startLeScan(scanTimeout: Long) {
93 | if (mBleScanState != BleScanState.STATE_IDLE) {
94 | BleLog.w("scan action already exists, complete the previous scan action first")
95 | bleScanCallback?.onScanStarted(false)
96 | return
97 | }
98 | map.clear()
99 | bleScanCallback?.onScanStarted(true)
100 | BleLog.i("scan start")
101 | mBleScanState = BleScanState.STATE_SCANNING
102 | launch {
103 | BleManager.bluetoothAdapter?.bluetoothLeScanner?.startScan(
104 | bleScanRuleConfig.generateScanFilter(),
105 | bleScanRuleConfig.generateScanSettings(), this@BleScanner
106 | )
107 | if (scanTimeout > 0) {
108 | delay(scanTimeout)
109 | stopLeScan()
110 | }
111 | }
112 |
113 | }
114 |
115 | @Synchronized
116 | fun stopLeScan() {
117 | if (mBleScanState == BleScanState.STATE_SCANNING) {
118 | BleManager.bluetoothAdapter?.bluetoothLeScanner?.stopScan(this@BleScanner)
119 | mBleScanState = BleScanState.STATE_IDLE
120 | bleScanCallback?.onScanFinished(map.values.toList())
121 | BleLog.i("scan finished")
122 | coroutineContext.cancelChildren()
123 | }
124 | }
125 |
126 | fun destroy() {
127 | bleScanCallback = null
128 | map.clear()
129 | stopLeScan()
130 | }
131 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/data/BleDevice.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.data
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.BluetoothDevice
5 | import android.bluetooth.le.ScanResult
6 | import android.os.Build
7 | import android.os.Parcel
8 | import android.os.Parcelable
9 | import com.huyuhui.fastble.BleManager
10 | import com.huyuhui.fastble.utils.BleLog
11 |
12 | @Suppress("unused")
13 | data class BleDevice(
14 | val device: BluetoothDevice
15 | ) : Parcelable {
16 | var scanResult: ScanResult? = null
17 |
18 | constructor(scanResult: ScanResult) : this(scanResult.device) {
19 | this.scanResult = scanResult
20 | }
21 |
22 | //获取名称需要权限,没有权限的时候返回null
23 | val name: String?
24 | @SuppressLint("MissingPermission")
25 | get() {
26 | return try {
27 | device.name
28 | } catch (e: SecurityException) {
29 | null
30 | }
31 | }
32 | val mac: String
33 | get() = device.address
34 |
35 | val key = BleManager.bleFactory?.generateUniqueKey(this) ?: mac
36 |
37 | val scanRecord
38 | get() = scanResult?.scanRecord?.bytes
39 | val rssi
40 | get() = scanResult?.rssi ?: Int.MIN_VALUE
41 | val isConnected
42 | get() = BleManager.isConnected(this)
43 | val isConnecting
44 | get() = BleManager.isConnecting(this)
45 |
46 | var bleAlias: String? = null
47 |
48 | /**
49 | * 获取类型的时候需要权限,没有权限的时候返回null
50 | * DEVICE_TYPE_UNKNOWN = 0
51 | * DEVICE_TYPE_CLASSIC = 1
52 | * DEVICE_TYPE_LE = 2
53 | * DEVICE_TYPE_DUAL = 3
54 | */
55 | val deviceType: Int
56 | @SuppressLint("MissingPermission")
57 | get() {
58 | return try {
59 | device.type
60 | } catch (e: SecurityException) {
61 | 0
62 | }
63 | }
64 |
65 | /**
66 | * 自定义属性值:仅支持以下类型,其他类型会导致序列化失败
67 | * - 基础类型:String、Int、Long、Float、Double、Boolean、ByteArray
68 | * - 自定义类型:实现 Parcelable 接口的类
69 | */
70 | private val propertyMap: HashMap by lazy {
71 | hashMapOf()
72 | }
73 |
74 | @Suppress("DEPRECATION")
75 | constructor(parcel: Parcel) : this(
76 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
77 | parcel.readParcelable(
78 | BluetoothDevice::class.java.classLoader,
79 | BluetoothDevice::class.java
80 | )!!
81 | } else {
82 | parcel.readParcelable(BluetoothDevice::class.java.classLoader)!!
83 | }
84 | ) {
85 | scanResult = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
86 | parcel.readParcelable(
87 | ScanResult::class.java.classLoader,
88 | ScanResult::class.java
89 | )
90 | } else {
91 | parcel.readParcelable(ScanResult::class.java.classLoader)
92 | }
93 | bleAlias = parcel.readString()
94 | parcel.readMap(propertyMap, HashMap::class.java.classLoader)
95 |
96 | }
97 |
98 | /**
99 | * 存入自定义属性(仅支持指定合法类型,非法类型会打警告并拒绝存入)
100 | * @param key 属性键
101 | * @param value 属性值:支持基础类型(String/Int/Long等)或 Parcelable 类型
102 | */
103 | fun put(key: String, value: Any) {
104 | val isLegalType = when (value) {
105 | is String, is Int, is Long, is Float, is Double, is Boolean, is ByteArray -> true
106 | is Parcelable -> true
107 | else -> false
108 | }
109 | if (!isLegalType) {
110 | BleLog.e("put failed: Only basic types (String/Int/Long/Boolean/ByteArray) or Parcelable are supported! Illegal type: ${value.javaClass.name}")
111 | return
112 | }
113 | propertyMap[key] = value
114 | }
115 |
116 | operator fun get(key: String?): Any? {
117 | return propertyMap[key]
118 | }
119 |
120 | override fun writeToParcel(parcel: Parcel, flags: Int) {
121 | parcel.writeParcelable(device, flags)
122 | parcel.writeParcelable(scanResult, flags)
123 | parcel.writeString(bleAlias)
124 | try {
125 | parcel.writeMap(propertyMap)
126 | } catch (e: Exception) {
127 | BleLog.e("Only basic data types or Parcelable can be written.", e)
128 | parcel.writeMap(hashMapOf()) // 写入空 Map 避免后续反序列化异常
129 | }
130 | }
131 |
132 | override fun describeContents(): Int {
133 | return 0
134 | }
135 |
136 | override fun toString(): String {
137 | return "BleDevice(scanResult=$scanResult, device=$device, bleAlias=$bleAlias, deviceType=$deviceType, propertyMap=$propertyMap)"
138 | }
139 |
140 | override fun equals(other: Any?): Boolean {
141 | if (this === other) return true
142 | if (javaClass != other?.javaClass) return false
143 |
144 | other as BleDevice
145 |
146 | return key == other.key
147 | }
148 |
149 | override fun hashCode(): Int {
150 | return key.hashCode()
151 | }
152 |
153 | companion object CREATOR : Parcelable.Creator {
154 | override fun createFromParcel(parcel: Parcel): BleDevice {
155 | return BleDevice(parcel)
156 | }
157 |
158 | override fun newArray(size: Int): Array {
159 | return arrayOfNulls(size)
160 | }
161 | }
162 |
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/adapter/ServiceAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample.adapter
2 |
3 | import android.bluetooth.BluetoothGattCharacteristic
4 | import android.bluetooth.BluetoothGattService
5 | import android.content.Context
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.BaseExpandableListAdapter
10 | import androidx.core.content.ContextCompat
11 | import com.huyuhui.blesample.R
12 | import com.huyuhui.blesample.databinding.ItemServiceBinding
13 |
14 |
15 | class ServiceAdapter(val context: Context, private val services: List) :
16 | BaseExpandableListAdapter() {
17 | override fun getGroupCount(): Int {
18 | return services.count()
19 | }
20 |
21 | override fun getChildrenCount(groupPosition: Int): Int {
22 | return services[groupPosition].characteristics.size
23 | }
24 |
25 | override fun getGroup(groupPosition: Int): Any {
26 | return services[groupPosition]
27 | }
28 |
29 | override fun getChild(groupPosition: Int, childPosition: Int): Any {
30 | return (services[groupPosition].characteristics[childPosition])
31 | }
32 |
33 | override fun getGroupId(groupPosition: Int): Long {
34 | return groupPosition.toLong()
35 | }
36 |
37 | override fun getChildId(groupPosition: Int, childPosition: Int): Long {
38 | return childPosition.toLong()
39 | }
40 |
41 | override fun hasStableIds(): Boolean {
42 | return true
43 | }
44 |
45 | override fun getGroupView(
46 | groupPosition: Int,
47 | isExpanded: Boolean,
48 | convertView: View?,
49 | parent: ViewGroup?
50 | ): View {
51 | val serviceHolder = if (convertView == null) {
52 | val binding =
53 | ItemServiceBinding.inflate(LayoutInflater.from(context), parent, false)
54 | ViewHolder(binding).apply {
55 | binding.root.tag = this
56 | }
57 |
58 | } else {
59 | convertView.tag as ViewHolder
60 | }
61 | serviceHolder.binding.tvTitle.text =
62 | "${context.getString(R.string.service)}($groupPosition)"
63 | serviceHolder.binding.tvUuid.text = services[groupPosition].uuid.toString()
64 | serviceHolder.binding.tvType.text =
65 | if (services[groupPosition].type == BluetoothGattService.SERVICE_TYPE_PRIMARY) {
66 | context.getString(R.string.primary_service)
67 | } else {
68 | context.getString(R.string.secondary_service)
69 | }
70 | if (isExpanded) {
71 | serviceHolder.binding.iv.setImageDrawable(
72 | ContextCompat.getDrawable(
73 | context,
74 | R.drawable.ic_arrow_down
75 | )
76 | )
77 | } else {
78 | serviceHolder.binding.iv.setImageDrawable(
79 | ContextCompat.getDrawable(
80 | context,
81 | R.drawable.ic_arrow_right
82 | )
83 | )
84 | }
85 | serviceHolder.binding.root.setBackgroundColor(context.getColor(R.color.gray))
86 | return serviceHolder.binding.root
87 | }
88 |
89 | override fun getChildView(
90 | groupPosition: Int,
91 | childPosition: Int,
92 | isLastChild: Boolean,
93 | convertView: View?,
94 | parent: ViewGroup?
95 | ): View {
96 | val characteristicHolder = if (convertView == null) {
97 | val binding =
98 | ItemServiceBinding.inflate(LayoutInflater.from(context), parent, false)
99 | ViewHolder(binding).apply {
100 | binding.root.tag = this
101 | }
102 |
103 | } else {
104 | convertView.tag as ViewHolder
105 | }
106 | val characteristic = services[groupPosition].characteristics[childPosition]
107 | characteristicHolder.binding.tvTitle.text =
108 | "${context.getString(R.string.characteristic)}($childPosition)"
109 | characteristicHolder.binding.tvUuid.text = characteristic.uuid.toString()
110 | val property = StringBuilder()
111 | val charaProp = characteristic.properties
112 | if (charaProp and BluetoothGattCharacteristic.PROPERTY_READ > 0) {
113 | property.append("Read")
114 | property.append(" , ")
115 | }
116 | if (charaProp and BluetoothGattCharacteristic.PROPERTY_WRITE > 0) {
117 | property.append("Write")
118 | property.append(" , ")
119 | }
120 | if (charaProp and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0) {
121 | property.append("Write No Response")
122 | property.append(" , ")
123 | }
124 | if (charaProp and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) {
125 | property.append("Notify")
126 | property.append(" , ")
127 | }
128 | if (charaProp and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0) {
129 | property.append("Indicate")
130 | property.append(" , ")
131 | }
132 | if (property.length > 1) {
133 | property.delete(property.length - 3, property.length)
134 | }
135 | characteristicHolder.binding.tvType.text = "${context.getString(R.string.characteristic)}($property)"
136 | characteristicHolder.binding.iv.setImageDrawable(ContextCompat.getDrawable(context,R.drawable.ic_arrow_right))
137 | return characteristicHolder.binding.root
138 | }
139 |
140 | override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean {
141 | return true
142 | }
143 |
144 | class ViewHolder(val binding: ItemServiceBinding)
145 |
146 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/common/BleConnectStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.common
2 |
3 | import android.bluetooth.BluetoothDevice
4 | import android.os.Build
5 | import androidx.annotation.RequiresApi
6 | import kotlin.math.max
7 |
8 | @Suppress("unused")
9 | class BleConnectStrategy private constructor() {
10 | companion object {
11 | /**
12 | * 当存在mac相同的设备已经在连接的时候,忽略掉后面发起的连接,直至这次连接失败或者成功,已经存在连接成功,不会发起连接
13 | */
14 | const val CONNECT_BACKPRESSURE_DROP: Int = 0
15 |
16 | /**
17 | * 当存在mac相同的设备已经在连接的时候,取消之前的链接,直接用最新发起的,已经存在连接成功,不会发起连接
18 | */
19 | const val CONNECT_BACKPRESSURE_LAST: Int = 1
20 | const val DEFAULT_CONNECT_RETRY_COUNT = 0
21 | const val DEFAULT_CONNECT_RETRY_INTERVAL: Long = 2000
22 | const val DEFAULT_CONNECT_OVER_TIME: Long = 10000
23 |
24 | const val DEFAULT_DISCOVER_SERVICE_TIMEOUT: Long = 5000
25 | }
26 |
27 | var connectBackpressureStrategy = CONNECT_BACKPRESSURE_DROP
28 | private set
29 |
30 | /**
31 | * connect retry count
32 | */
33 | var reConnectCount = DEFAULT_CONNECT_RETRY_COUNT
34 | private set
35 |
36 | /**
37 | * connect retry interval
38 | */
39 | var reConnectInterval = DEFAULT_CONNECT_RETRY_INTERVAL
40 | private set
41 |
42 | /**
43 | * Get operate connect Over Time
44 | *
45 | */
46 | var connectOverTime: Long = DEFAULT_CONNECT_OVER_TIME
47 | private set
48 |
49 | /**
50 | * 发现服务超时时间
51 | */
52 | var discoverServiceTimeout = DEFAULT_DISCOVER_SERVICE_TIMEOUT
53 | private set
54 |
55 | /**
56 | * true 当发起连接的时候,无法找到设备,会保持连接状态,不回调结果(如果设置了超时,会一直等待到超时时间回调连接超时),等待设备可以连接之后,再回调结果。
57 | * false 直接连接,回调结果,默认为false
58 | */
59 | var mAutoConnect = false
60 | private set
61 |
62 | /**
63 | *
64 | * @see BluetoothDevice.TRANSPORT_AUTO
65 | */
66 | var transport: Int = 0
67 | private set
68 |
69 | /**
70 | * @see BluetoothDevice.PHY_LE_1M_MASK
71 | */
72 | var phy: Int = 1
73 | private set
74 |
75 | class Builder() {
76 | private var connectBackpressureStrategy = CONNECT_BACKPRESSURE_DROP
77 | private var reConnectCount = DEFAULT_CONNECT_RETRY_COUNT
78 | private var reConnectInterval = DEFAULT_CONNECT_RETRY_INTERVAL
79 | private var connectOverTime = DEFAULT_CONNECT_OVER_TIME
80 |
81 | private var discoverServiceTimeout = DEFAULT_DISCOVER_SERVICE_TIMEOUT
82 | private var mAutoConnect = false
83 |
84 | /**
85 | *
86 | * @see BluetoothDevice.TRANSPORT_AUTO
87 | */
88 | private var transport: Int = 0
89 |
90 | /**
91 | * @see BluetoothDevice.PHY_LE_1M_MASK
92 | */
93 | private var phy: Int = 1
94 |
95 |
96 | constructor(bleConnectStrategy: BleConnectStrategy) : this() {
97 | connectBackpressureStrategy = bleConnectStrategy.connectBackpressureStrategy
98 | reConnectCount = bleConnectStrategy.reConnectCount
99 | reConnectInterval = bleConnectStrategy.reConnectInterval
100 | connectOverTime = bleConnectStrategy.connectOverTime
101 | mAutoConnect = bleConnectStrategy.mAutoConnect
102 | transport = bleConnectStrategy.transport
103 | phy = bleConnectStrategy.phy
104 | discoverServiceTimeout = bleConnectStrategy.discoverServiceTimeout
105 | }
106 |
107 | fun setConnectBackpressureStrategy(backpressureStrategy: Int): Builder {
108 | this.connectBackpressureStrategy = backpressureStrategy
109 | return this
110 | }
111 |
112 | fun setReConnectCount(count: Int): Builder {
113 | reConnectCount = max(0, count)
114 | return this
115 | }
116 |
117 | fun setReConnectInterval(interval: Long): Builder {
118 | reConnectInterval = interval
119 | return this
120 | }
121 |
122 | fun setConnectOverTime(time: Long): Builder {
123 | connectOverTime = time
124 | return this
125 | }
126 | fun setDiscoverServiceTimeout(discoverServiceTimeout: Long) = apply{
127 | this.discoverServiceTimeout = discoverServiceTimeout
128 | }
129 | fun setAutoConnect(autoConnect: Boolean): Builder {
130 | mAutoConnect = autoConnect
131 | return this
132 | }
133 |
134 | @RequiresApi(Build.VERSION_CODES.M)
135 | fun setTransport(transport: Int) = apply {
136 | this.transport = transport
137 | }
138 |
139 | @RequiresApi(Build.VERSION_CODES.O)
140 | fun setPhy(phy: Int) = apply {
141 | this.phy = phy
142 | }
143 |
144 | fun build(): BleConnectStrategy {
145 | val strategy = BleConnectStrategy()
146 | strategy.reConnectCount = this.reConnectCount
147 | strategy.reConnectInterval = this.reConnectInterval
148 | strategy.connectOverTime = this.connectOverTime
149 | strategy.connectBackpressureStrategy = this.connectBackpressureStrategy
150 | strategy.mAutoConnect = this.mAutoConnect
151 | strategy.transport = this.transport
152 | strategy.phy = this.phy
153 | strategy.discoverServiceTimeout = this.discoverServiceTimeout
154 | return strategy
155 | }
156 | }
157 |
158 | override fun toString(): String {
159 | return "BleConnectStrategy(connectBackpressureStrategy=$connectBackpressureStrategy, reConnectCount=$reConnectCount, reConnectInterval=$reConnectInterval, connectOverTime=$connectOverTime, discoverServiceTimeout=$discoverServiceTimeout, mAutoConnect=$mAutoConnect, transport=$transport, phy=$phy)"
160 | }
161 |
162 |
163 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/ScanRecyclerView.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample
2 |
3 | import android.animation.ObjectAnimator
4 | import android.animation.ValueAnimator
5 | import android.content.Context
6 | import android.util.AttributeSet
7 | import android.view.MotionEvent
8 | import android.view.inputmethod.InputMethodManager
9 | import androidx.core.view.updateLayoutParams
10 | import androidx.recyclerview.widget.ConcatAdapter
11 | import androidx.recyclerview.widget.LinearLayoutManager
12 | import androidx.recyclerview.widget.RecyclerView
13 | import com.huyuhui.blesample.adapter.ScanFilterAdapter
14 |
15 |
16 | class ScanRecyclerView @JvmOverloads constructor(
17 | context: Context,
18 | attrs: AttributeSet? = null,
19 | defStyleAttr: Int = 0
20 | ) : RecyclerView(context, attrs, defStyleAttr) {
21 | companion object {
22 | @JvmStatic
23 | private val FILTER_STATUS_HIDDEN = 1
24 |
25 | @JvmStatic
26 | private val FILTER_STATUS_SHOWN = 2
27 |
28 | @JvmStatic
29 | private val FILTER_STATUS_SHOWING = 3
30 | }
31 |
32 | private var mCurrentFilterStatus = FILTER_STATUS_SHOWN
33 |
34 | private var hasRecord = false
35 | private var mStartEventY: Float = 0f
36 |
37 | private val scanFilterView
38 | get() = ((adapter as ConcatAdapter).adapters[0] as ScanFilterAdapter).scanFilterBinding?.root
39 |
40 | private val filterViewHeight
41 | get() = scanFilterView?.measuredHeight ?: 0
42 |
43 |
44 | override fun onScrolled(dx: Int, dy: Int) {
45 | super.onScrolled(dx, dy)
46 | if (mCurrentFilterStatus == FILTER_STATUS_SHOWN) {
47 | if ((layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 1) {
48 | scanFilterView?.let {
49 | it.updateLayoutParams {
50 | topMargin = -filterViewHeight + 1
51 | }
52 | }
53 | mCurrentFilterStatus = FILTER_STATUS_HIDDEN
54 | closeSoftInput()
55 | }
56 | }
57 | }
58 |
59 | override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
60 | if (mCurrentFilterStatus == FILTER_STATUS_SHOWN) {
61 | return super.dispatchTouchEvent(ev)
62 | }
63 | when (ev?.action) {
64 | MotionEvent.ACTION_DOWN -> {
65 | record(ev)
66 | }
67 |
68 | MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
69 | if (mCurrentFilterStatus == FILTER_STATUS_SHOWING) {
70 | restoreFilterView()
71 | }
72 | hasRecord = false
73 | }
74 |
75 | }
76 | return super.dispatchTouchEvent(ev)
77 | }
78 |
79 | override fun onTouchEvent(ev: MotionEvent?): Boolean {
80 | if (mCurrentFilterStatus == FILTER_STATUS_SHOWN || !canShowFilter()) {
81 | return super.onTouchEvent(ev)
82 | }
83 | when (ev?.action) {
84 | MotionEvent.ACTION_MOVE -> {
85 | record(ev)
86 | val distance = (ev.rawY - mStartEventY) * 0.3
87 | if (distance > 0) {
88 | scrollToPosition(0)
89 | mCurrentFilterStatus = FILTER_STATUS_SHOWING
90 | val marginTop = (distance - filterViewHeight).let {
91 | if (it > 0) {
92 | -1
93 | } else
94 | it
95 | }
96 | scanFilterView?.let {
97 | it.updateLayoutParams {
98 | topMargin = marginTop.toInt()
99 | }
100 | }
101 | if (distance > filterViewHeight - 1) {
102 | mCurrentFilterStatus = FILTER_STATUS_SHOWN
103 | hasRecord = false
104 | }
105 | return false
106 | }
107 | }
108 | }
109 | return super.onTouchEvent(ev)
110 | }
111 |
112 | // 是否滚动到表头了
113 | private fun canShowFilter(): Boolean {
114 | return !canScrollVertically(-1) && scrollY <= 0
115 | }
116 |
117 | private fun restoreFilterView() {
118 | val currentTopMargin = (scanFilterView?.layoutParams as MarginLayoutParams).topMargin
119 | if ((currentTopMargin + filterViewHeight.toFloat()) / filterViewHeight < 0.3) {
120 | val animator: ValueAnimator =
121 | ObjectAnimator.ofFloat(
122 | currentTopMargin.toFloat(),
123 | -filterViewHeight + 1f
124 | )
125 | .setDuration(300)
126 | animator.addUpdateListener {
127 | scanFilterView?.let { v ->
128 | v.updateLayoutParams {
129 | topMargin = (it.animatedValue as Float).toInt()
130 | }
131 | }
132 | }
133 | mCurrentFilterStatus = FILTER_STATUS_HIDDEN
134 | closeSoftInput()
135 | animator.start()
136 | } else {
137 | val animator: ValueAnimator =
138 | ObjectAnimator.ofFloat(
139 | currentTopMargin.toFloat(),
140 | 1f
141 | )
142 | .setDuration(300)
143 | animator.addUpdateListener {
144 | scanFilterView?.let { v ->
145 | v.updateLayoutParams {
146 | topMargin = (it.animatedValue as Float).toInt()
147 | }
148 | scrollToPosition(0)
149 | }
150 | }
151 | mCurrentFilterStatus = FILTER_STATUS_SHOWN
152 | animator.start()
153 | }
154 | }
155 |
156 | private fun closeSoftInput() {
157 | val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
158 | imm?.hideSoftInputFromWindow(windowToken, 0)
159 | }
160 |
161 | private fun record(ev: MotionEvent) {
162 | if (!hasRecord && canShowFilter()) {
163 | mStartEventY = ev.rawY
164 | hasRecord = true
165 | }
166 | }
167 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/bluetooth/MultipleBluetoothController.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.bluetooth
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.BluetoothDevice
5 | import android.bluetooth.BluetoothGatt
6 | import android.bluetooth.BluetoothProfile
7 | import com.huyuhui.fastble.BleManager
8 | import com.huyuhui.fastble.data.BleDevice
9 | import com.huyuhui.fastble.exception.BleException
10 | import com.huyuhui.fastble.utils.BleLruHashMap
11 | import java.util.concurrent.ConcurrentHashMap
12 |
13 | @SuppressLint("MissingPermission")
14 | @Suppress("unused")
15 | internal class MultipleBluetoothController {
16 | //保存已经连接成功的设备
17 | private val connectedDevicesMap: BleLruHashMap by lazy {
18 | BleLruHashMap(BleManager.maxConnectCount)
19 | }
20 |
21 | //保存正在连接的设备
22 | private val connectingDevicesMap: ConcurrentHashMap = ConcurrentHashMap()
23 |
24 | fun buildConnectingBle(bleDevice: BleDevice): BleBluetooth {
25 | return connectingDevicesMap.putIfAbsent(bleDevice.key, BleBluetooth(bleDevice))
26 | ?: connectingDevicesMap[bleDevice.key]!!
27 | }
28 |
29 |
30 | fun removeConnectingBle(bleBluetooth: BleBluetooth?) {
31 | bleBluetooth?.let { connectingDevicesMap.remove(it.deviceKey) }
32 | }
33 |
34 | fun addConnectedBleBluetooth(bleBluetooth: BleBluetooth?) {
35 | if (bleBluetooth == null) return
36 | if (!connectedDevicesMap.containsKey(bleBluetooth.deviceKey)) {
37 | connectedDevicesMap[bleBluetooth.deviceKey] = bleBluetooth
38 | }
39 | }
40 |
41 | fun removeConnectedBleBluetooth(bleBluetooth: BleBluetooth?) {
42 | if (bleBluetooth == null) {
43 | return
44 | }
45 | if (connectedDevicesMap.containsKey(bleBluetooth.deviceKey)) {
46 | connectedDevicesMap.remove(bleBluetooth.deviceKey)
47 | }
48 | }
49 |
50 | fun isConnecting(bleDevice: BleDevice?): Boolean {
51 | return bleDevice != null && connectingDevicesMap.containsKey(bleDevice.key)
52 | }
53 |
54 | fun cancelConnecting(bleDevice: BleDevice?, skip: Boolean) {
55 | bleDevice?.key?.let { key ->
56 | connectingDevicesMap.remove(key)?.let { bleBluetooth ->
57 | // 回调和销毁在锁外执行,不阻塞并发操作
58 | bleBluetooth.bleGattCallback?.onConnectCancel(bleBluetooth.bleDevice, skip)
59 | bleBluetooth.destroy()
60 | }
61 | }
62 | }
63 |
64 | fun cancelOrDisconnect(bleDevice: BleDevice?) {
65 | cancelConnecting(bleDevice, false)
66 | disconnect(bleDevice)
67 | }
68 |
69 | fun cancelAllConnectingDevice() {
70 | connectingDevicesMap.values.forEach { bleBluetooth ->
71 | bleBluetooth.bleGattCallback?.onConnectCancel(bleBluetooth.bleDevice, false)
72 | bleBluetooth.destroy()
73 | }
74 | connectingDevicesMap.clear() // 清空所有连接中设备
75 | }
76 |
77 | fun isConnectedDevice(bleDevice: BleDevice?): Boolean {
78 | return bleDevice != null && connectedDevicesMap.containsKey(bleDevice.key)
79 | }
80 |
81 | fun isConnectedDevice(bluetoothDevice: BluetoothDevice?): Boolean {
82 | return bluetoothDevice != null && isConnectedDevice(
83 | BleManager.convertBleDevice(
84 | bluetoothDevice
85 | )
86 | )
87 | }
88 |
89 | fun getConnectedBleBluetooth(bleDevice: BleDevice?): BleBluetooth? {
90 | return bleDevice?.key?.let { connectedDevicesMap[it] }
91 | }
92 |
93 | fun disconnect(bleDevice: BleDevice?) {
94 | if (isConnectedDevice(bleDevice)) {
95 | getConnectedBleBluetooth(bleDevice)?.disconnect()
96 | }
97 | }
98 |
99 | fun disconnectAllDevice() {
100 | val keys = ArrayList(connectedDevicesMap.keys)
101 | keys.forEach { key ->
102 | connectedDevicesMap[key]?.disconnect()
103 | }
104 | }
105 |
106 | fun destroy() {
107 | // 处理已连接设备
108 | val connectedKeys = ArrayList(connectedDevicesMap.keys)
109 | connectedKeys.forEach { key ->
110 | connectedDevicesMap.remove(key)?.destroy() // 原子操作:移除并销毁
111 | }
112 |
113 | // 处理连接中设备
114 | val connectingKeys = ArrayList(connectingDevicesMap.keys)
115 | connectingKeys.forEach { key ->
116 | connectingDevicesMap.remove(key)?.destroy() // 原子操作
117 | }
118 | }
119 |
120 | fun getConnectedBleBluetoothList(): List {
121 | return connectedDevicesMap.values.toList()
122 | }
123 |
124 | fun getConnectedDeviceList(): List {
125 | refreshConnectedDevice()
126 | return getConnectedBleBluetoothList().map { it.bleDevice }
127 | }
128 |
129 | private fun getConnectingBleBluetoothList(): List {
130 | return connectingDevicesMap.values.toList()
131 | }
132 |
133 | fun getConnectingDeviceList(): List {
134 | return connectingDevicesMap.values.toList().map { it.bleDevice }
135 | }
136 |
137 | private fun refreshConnectedDevice() {
138 | val bluetoothList = getConnectedBleBluetoothList()
139 | bluetoothList.forEach { bleBluetooth ->
140 | if (BleManager.getConnectState(bleBluetooth.bleDevice) != BluetoothProfile.STATE_CONNECTED) {
141 | removeConnectedBleBluetooth(bleBluetooth)
142 | bleBluetooth.destroy()
143 | }
144 | }
145 | }
146 |
147 | fun onBleOff() {
148 | getConnectedBleBluetoothList().forEach {
149 | removeConnectedBleBluetooth(it)
150 | it.bleGattCallback?.onDisConnected(
151 | true,
152 | it.bleDevice,
153 | it.bluetoothGatt,
154 | BluetoothGatt.GATT_SUCCESS
155 | )
156 | it.destroy()
157 | }
158 | connectedDevicesMap.clear()
159 | getConnectingBleBluetoothList().forEach {
160 | removeConnectingBle(it)
161 | it.bleGattCallback?.onConnectFail(
162 | it.bleDevice,
163 | BleException.OtherException(
164 | BleException.BLUETOOTH_NOT_ENABLED,
165 | "Bluetooth is not enabled"
166 | )
167 | )
168 | it.destroy()
169 | }
170 | connectingDevicesMap.clear()
171 | }
172 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/bluetooth/BleIndicateOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.bluetooth
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.BluetoothGatt
5 | import android.bluetooth.BluetoothGattCharacteristic
6 | import android.bluetooth.BluetoothGattDescriptor
7 | import android.bluetooth.BluetoothStatusCodes
8 | import android.os.Build
9 | import com.huyuhui.fastble.callback.BleIndicateCallback
10 | import com.huyuhui.fastble.common.TimeoutTask
11 | import com.huyuhui.fastble.exception.BleException
12 |
13 | @SuppressLint("MissingPermission")
14 | internal class BleIndicateOperator(
15 | bleBluetooth: BleBluetooth,
16 | timeout: Long,
17 | uuidService: String,
18 | uuidCharacteristic: String
19 | ) : BleCharacteristicOperator(bleBluetooth, timeout, uuidService, uuidCharacteristic) {
20 |
21 | var bleIndicateCallback: BleIndicateCallback? = null
22 | private set
23 |
24 | fun enableCharacteristicIndicate(
25 | bleIndicateCallback: BleIndicateCallback?,
26 | useCharacteristicDescriptor: Boolean,
27 | ) {
28 | if (mCharacteristic != null
29 | && mCharacteristic.properties or BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0
30 | ) {
31 | this.bleIndicateCallback = bleIndicateCallback
32 | bleBluetooth.addIndicateOperator(key, this)
33 | timeOutTask.start(this)
34 | setCharacteristicIndication(
35 | mBluetoothGatt, mCharacteristic,
36 | true, bleIndicateCallback, useCharacteristicDescriptor
37 | )
38 | } else {
39 | bleIndicateCallback?.onIndicateFailure(
40 | bleDevice,
41 | mCharacteristic,
42 | BleException.OtherException(
43 | BleException.CHARACTERISTIC_NOT_SUPPORT,
44 | "this characteristic not support indicate!"
45 | )
46 | )
47 | }
48 | }
49 |
50 |
51 | /**
52 | * stop indicate
53 | */
54 | fun disableCharacteristicIndicate(useCharacteristicDescriptor: Boolean): Boolean {
55 | return if (mCharacteristic != null
56 | && mCharacteristic.properties or BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0
57 | ) {
58 | setCharacteristicIndication(
59 | mBluetoothGatt, mCharacteristic,
60 | false, null, useCharacteristicDescriptor
61 | )
62 | } else {
63 | false
64 | }
65 | }
66 |
67 | /**
68 | * indicate setting
69 | */
70 | @Suppress("DEPRECATION")
71 | private fun setCharacteristicIndication(
72 | gatt: BluetoothGatt?,
73 | characteristic: BluetoothGattCharacteristic?,
74 | enable: Boolean,
75 | bleIndicateCallback: BleIndicateCallback?,
76 | useCharacteristicDescriptor: Boolean,
77 | ): Boolean {
78 | if (gatt == null || characteristic == null) {
79 | removeTimeOut()
80 | bleIndicateCallback?.onIndicateFailure(
81 | bleDevice,
82 | mCharacteristic,
83 | BleException.OtherException(
84 | BleException.GATT_NULL,
85 | "gatt or characteristic equal null"
86 | )
87 | )
88 | return false
89 | }
90 | val success1 = gatt.setCharacteristicNotification(characteristic, enable)
91 | if (!success1) {
92 | removeTimeOut()
93 | bleIndicateCallback?.onIndicateFailure(
94 | bleDevice,
95 | mCharacteristic,
96 | BleException.OtherException(
97 | BleException.CHARACTERISTIC_ERROR,
98 | "gatt setCharacteristicNotification fail"
99 | )
100 | )
101 | return false
102 | }
103 | val descriptor: BluetoothGattDescriptor? =
104 | if (useCharacteristicDescriptor) characteristic.getDescriptor(characteristic.uuid) else
105 | characteristic.getDescriptor(fromUUID(UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR))
106 | return if (descriptor == null) {
107 | removeTimeOut()
108 | bleIndicateCallback?.onIndicateFailure(
109 | bleDevice,
110 | mCharacteristic,
111 | BleException.OtherException(BleException.DESCRIPTOR_NULL, "descriptor equals null")
112 | )
113 | false
114 | } else {
115 | val data =
116 | if (enable) BluetoothGattDescriptor.ENABLE_INDICATION_VALUE else BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
117 | descriptor.value = data
118 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
119 | val state = gatt.writeDescriptor(
120 | descriptor,
121 | data!!
122 | )
123 | val success2 = state == BluetoothStatusCodes.SUCCESS
124 | if (!success2) {
125 | removeTimeOut()
126 | bleIndicateCallback?.onIndicateFailure(
127 | bleDevice,
128 | mCharacteristic,
129 | BleException.OtherException(
130 | BleException.DESCRIPTOR_ERROR,
131 | "gatt writeDescriptor fail"
132 | )
133 | )
134 | }
135 | success2
136 | } else {
137 | val success2 = gatt.writeDescriptor(descriptor)
138 | if (!success2) {
139 | removeTimeOut()
140 | bleIndicateCallback?.onIndicateFailure(
141 | bleDevice,
142 | mCharacteristic,
143 | BleException.OtherException(
144 | BleException.DESCRIPTOR_ERROR,
145 | "gatt writeDescriptor fail"
146 | )
147 | )
148 | }
149 | success2
150 | }
151 | }
152 | }
153 |
154 | override fun onTimeout(
155 | task: TimeoutTask,
156 | e: Throwable?,
157 | isActive: Boolean
158 | ) {
159 | bleIndicateCallback?.onIndicateFailure(
160 | bleDevice,
161 | mCharacteristic,
162 | BleException.TimeoutException()
163 | )
164 | }
165 |
166 | override fun destroy() {
167 | super.destroy()
168 | bleIndicateCallback = null
169 | }
170 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/bluetooth/BleNotifyOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.bluetooth
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.BluetoothGatt
5 | import android.bluetooth.BluetoothGattCharacteristic
6 | import android.bluetooth.BluetoothGattDescriptor
7 | import android.bluetooth.BluetoothStatusCodes
8 | import android.os.Build
9 | import com.huyuhui.fastble.callback.BleNotifyCallback
10 | import com.huyuhui.fastble.common.TimeoutTask
11 | import com.huyuhui.fastble.exception.BleException
12 | import kotlinx.coroutines.launch
13 |
14 | @SuppressLint("MissingPermission")
15 | internal class BleNotifyOperator(
16 | bleBluetooth: BleBluetooth,
17 | timeout: Long,
18 | uuidService: String,
19 | uuidCharacteristic: String
20 | ) : BleCharacteristicOperator(bleBluetooth, timeout, uuidService, uuidCharacteristic) {
21 | var bleNotifyCallback: BleNotifyCallback? = null
22 | private set
23 |
24 | /**
25 | * notify
26 | */
27 | fun enableCharacteristicNotify(
28 | bleNotifyCallback: BleNotifyCallback?,
29 | useCharacteristicDescriptor: Boolean,
30 | ) {
31 | if (mCharacteristic != null && mCharacteristic.properties or BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) {
32 | this@BleNotifyOperator.bleNotifyCallback = bleNotifyCallback
33 | bleBluetooth.addNotifyOperator(key, this)
34 | timeOutTask.start(this)
35 | setCharacteristicNotification(
36 | mBluetoothGatt,
37 | mCharacteristic,
38 | true,
39 | bleNotifyCallback,
40 | useCharacteristicDescriptor
41 | )
42 | } else {
43 | launch {
44 | bleNotifyCallback?.onNotifyFailure(
45 | bleDevice,
46 | mCharacteristic,
47 | BleException.OtherException(
48 | BleException.CHARACTERISTIC_NOT_SUPPORT,
49 | "this characteristic not support notify!"
50 | )
51 | )
52 | }
53 | }
54 | }
55 |
56 | /**
57 | * stop notify
58 | */
59 | fun disableCharacteristicNotify(useCharacteristicDescriptor: Boolean): Boolean {
60 | return if (mCharacteristic != null
61 | && mCharacteristic.properties or BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0
62 | ) {
63 | setCharacteristicNotification(
64 | mBluetoothGatt, mCharacteristic, false, null, useCharacteristicDescriptor
65 | )
66 | } else {
67 | false
68 | }
69 | }
70 |
71 |
72 | @Suppress("DEPRECATION")
73 | private fun setCharacteristicNotification(
74 | gatt: BluetoothGatt?,
75 | characteristic: BluetoothGattCharacteristic?,
76 | enable: Boolean,
77 | bleNotifyCallback: BleNotifyCallback?,
78 | useCharacteristicDescriptor: Boolean,
79 | ): Boolean {
80 | if (gatt == null || characteristic == null) {
81 | removeTimeOut()
82 | bleNotifyCallback?.onNotifyFailure(
83 | bleDevice,
84 | characteristic,
85 | BleException.OtherException(
86 | BleException.GATT_NULL,
87 | "gatt or characteristic equal null"
88 | )
89 | )
90 | return false
91 | }
92 | val success1 = gatt.setCharacteristicNotification(characteristic, enable)
93 | if (!success1) {
94 | removeTimeOut()
95 | bleNotifyCallback?.onNotifyFailure(
96 | bleDevice,
97 | characteristic,
98 | BleException.OtherException(
99 | BleException.CHARACTERISTIC_ERROR,
100 | "gatt setCharacteristicNotification fail"
101 | )
102 | )
103 | return false
104 | }
105 | val descriptor: BluetoothGattDescriptor? =
106 | if (useCharacteristicDescriptor) characteristic.getDescriptor(characteristic.uuid) else
107 | characteristic.getDescriptor(fromUUID(UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR))
108 | return if (descriptor == null) {
109 | removeTimeOut()
110 | bleNotifyCallback?.onNotifyFailure(
111 | bleDevice,
112 | characteristic,
113 | BleException.OtherException(BleException.DESCRIPTOR_NULL, "descriptor equals null")
114 | )
115 | false
116 | } else {
117 | val data =
118 | if (enable) BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE else BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
119 | descriptor.value = data
120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
121 | val state = gatt.writeDescriptor(
122 | descriptor,
123 | data
124 | )
125 | val success2 = state == BluetoothStatusCodes.SUCCESS
126 | if (!success2) {
127 | removeTimeOut()
128 | bleNotifyCallback?.onNotifyFailure(
129 | bleDevice,
130 | characteristic,
131 | BleException.OtherException(
132 | BleException.DESCRIPTOR_ERROR,
133 | "gatt writeDescriptor fail"
134 | )
135 | )
136 | }
137 | success2
138 | } else {
139 | val success2 = gatt.writeDescriptor(descriptor)
140 | if (!success2) {
141 | removeTimeOut()
142 | bleNotifyCallback?.onNotifyFailure(
143 | bleDevice,
144 | characteristic,
145 | BleException.OtherException(
146 | BleException.DESCRIPTOR_ERROR,
147 | "gatt writeDescriptor fail"
148 | )
149 | )
150 | }
151 | success2
152 | }
153 | }
154 | }
155 |
156 | override fun onTimeout(
157 | task: TimeoutTask,
158 | e: Throwable?,
159 | isActive: Boolean
160 | ) {
161 | bleNotifyCallback?.onNotifyFailure(
162 | bleDevice,
163 | mCharacteristic,
164 | BleException.TimeoutException()
165 | )
166 | }
167 |
168 | override fun destroy() {
169 | super.destroy()
170 | bleNotifyCallback = null
171 | }
172 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/bluetooth/SplitWriter.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.bluetooth
2 |
3 | import android.bluetooth.BluetoothGattCharacteristic
4 | import com.huyuhui.fastble.callback.BleWriteCallback
5 | import com.huyuhui.fastble.data.BleDevice
6 | import com.huyuhui.fastble.exception.BleException
7 | import com.huyuhui.fastble.utils.DataUtil
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.NonCancellable
10 | import kotlinx.coroutines.channels.Channel
11 | import kotlinx.coroutines.delay
12 | import kotlinx.coroutines.ensureActive
13 | import kotlinx.coroutines.flow.onCompletion
14 | import kotlinx.coroutines.flow.receiveAsFlow
15 | import kotlinx.coroutines.launch
16 | import kotlinx.coroutines.withContext
17 | import java.util.Queue
18 |
19 | internal class SplitWriter(private val writeOperator: BleWriteOperator) {
20 | private var mData: ByteArray? = null
21 | private var mCount = 0
22 | private var mIntervalBetweenTwoPackage: Long = 0
23 | private var mContinueWhenLastFail: Boolean = false
24 | private var mCallback: BleWriteCallback? = null
25 | private var mDataQueue: Queue? = null
26 | private var mTotalNum = 0
27 |
28 | //避免多次调用onWriteFailure
29 | private var closeFromFailure = false
30 | fun splitWrite(
31 | data: ByteArray,
32 | splitNum: Int,
33 | continueWhenLastFail: Boolean,
34 | intervalBetweenTwoPackage: Long = 0,
35 | callback: BleWriteCallback?,
36 | writeType: Int,
37 | ) {
38 | mData = data
39 | mContinueWhenLastFail = continueWhenLastFail
40 | mIntervalBetweenTwoPackage = intervalBetweenTwoPackage
41 | mCallback = callback
42 | mCount = splitNum
43 | closeFromFailure = false
44 | splitWrite(writeType)
45 | }
46 |
47 | val channel = Channel()
48 |
49 | private val callback = object : BleWriteCallback() {
50 | override fun onWriteSuccess(
51 | bleDevice: BleDevice,
52 | characteristic: BluetoothGattCharacteristic,
53 | current: Int,
54 | total: Int,
55 | justWrite: ByteArray,
56 | data: ByteArray,
57 | ) {
58 | val position = mTotalNum - mDataQueue!!.size
59 | mCallback?.onWriteSuccess(
60 | bleDevice,
61 | characteristic,
62 | position,
63 | mTotalNum,
64 | justWrite,
65 | mData!!
66 | )
67 | if (mDataQueue!!.isEmpty()) {
68 | channel.close()
69 | } else {
70 | writeOperator.launch {
71 | ensureActive()//mIntervalBetweenTwoPackage可能为0
72 | delay(mIntervalBetweenTwoPackage)
73 | channel.trySend(mDataQueue!!.poll())
74 | }
75 | }
76 | }
77 |
78 | override fun onWriteFailure(
79 | bleDevice: BleDevice,
80 | characteristic: BluetoothGattCharacteristic?,
81 | exception: BleException,
82 | current: Int,
83 | total: Int,
84 | justWrite: ByteArray?,
85 | data: ByteArray?,
86 | isTotalFail: Boolean,
87 | ) {
88 | val position = mTotalNum - mDataQueue!!.size
89 | if (mContinueWhenLastFail) {
90 | mCallback?.onWriteFailure(
91 | bleDevice,
92 | characteristic,
93 | exception,
94 | position,
95 | mTotalNum,
96 | data,
97 | mData,
98 | mDataQueue?.isEmpty() ?: true
99 | )
100 | if (mDataQueue!!.isEmpty()) {
101 | closeFromFailure = true
102 | channel.close()
103 | } else {
104 | writeOperator.launch {
105 | ensureActive() //mIntervalBetweenTwoPackage可能为0
106 | delay(mIntervalBetweenTwoPackage)
107 | channel.trySend(mDataQueue!!.poll())
108 | }
109 | }
110 | } else {
111 | mCallback?.onWriteFailure(
112 | bleDevice, characteristic,
113 | exception,
114 | position,
115 | mTotalNum,
116 | data,
117 | mData,
118 | true
119 | )
120 | closeFromFailure = true
121 | channel.close()
122 | }
123 | }
124 |
125 | }
126 |
127 | private fun splitWrite(writeType: Int) {
128 | requireNotNull(mData) { "data is Null!" }
129 | require(mCount >= 1) { "split count should higher than 0!" }
130 | writeOperator.launch {
131 | withContext(Dispatchers.IO) {
132 | mDataQueue = DataUtil.splitPacketForByte(mData, mCount)
133 | }
134 | mTotalNum = mDataQueue!!.size
135 | var currentData: ByteArray? = null
136 | launch {
137 | channel.send(mDataQueue!!.poll())
138 | }
139 | channel.receiveAsFlow()
140 | .onCompletion {
141 | withContext(NonCancellable + Dispatchers.Main) {
142 | if (mDataQueue!!.isNotEmpty() && !closeFromFailure) {
143 | val position = mTotalNum - mDataQueue!!.size
144 | mCallback?.onWriteFailure(
145 | writeOperator.bleDevice,
146 | writeOperator.mCharacteristic,
147 | BleException.OtherException(
148 | BleException.COROUTINE_SCOPE_CANCELLED,
149 | "CoroutineScope Cancelled when sending"
150 | ),
151 | position,
152 | mTotalNum,
153 | currentData,
154 | mData,
155 | true
156 | )
157 | }
158 | mDataQueue?.clear()
159 | mCallback = null
160 | mData = null
161 | }
162 | }
163 | .collect {
164 | currentData = it
165 | writeOperator.writeCharacteristic(
166 | it,
167 | callback,
168 | writeType
169 | )
170 | }
171 | }
172 | }
173 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/huyuhui/blesample/operate/SequenceNotifyOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.blesample.operate
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.BluetoothGattCharacteristic
5 | import com.huyuhui.fastble.BleManager
6 | import com.huyuhui.fastble.callback.BleNotifyCallback
7 | import com.huyuhui.fastble.data.BleDevice
8 | import com.huyuhui.fastble.exception.BleException
9 | import com.huyuhui.fastble.queue.TaskResult
10 | import com.huyuhui.fastble.queue.operate.DELAY_WRITE_DEFAULT
11 | import com.huyuhui.fastble.queue.operate.PRIORITY_WRITE_DEFAULT
12 | import com.huyuhui.fastble.queue.operate.SequenceBleOperator
13 | import kotlinx.coroutines.channels.Channel
14 | import java.lang.ref.WeakReference
15 |
16 | @SuppressLint("MissingPermission")
17 | @Suppress("unused")
18 | class SequenceNotifyOperator private constructor(priority: Int, delay: Long) :
19 | SequenceBleOperator(priority, delay) {
20 | var serviceUUID: String? = null
21 | private set
22 | var characteristicUUID: String? = null
23 | private set
24 | var bleNotifyCallback: BleNotifyCallback? = null
25 | private set
26 | var useCharacteristicDescriptor: Boolean = false
27 | private set
28 | private var mContinuous = false
29 | private var mTimeout = 0L
30 | private var channelWeakReference: WeakReference>? = null
31 | private val wrappedBleNotifyCallback = object : BleNotifyCallback() {
32 | override fun onNotifySuccess(
33 | bleDevice: BleDevice,
34 | characteristic: BluetoothGattCharacteristic,
35 | ) {
36 | bleNotifyCallback?.onNotifySuccess(bleDevice, characteristic)
37 | channelWeakReference?.get()
38 | ?.trySend(TaskResult(this@SequenceNotifyOperator, true))
39 | }
40 |
41 | override fun onNotifyFailure(
42 | bleDevice: BleDevice,
43 | characteristic: BluetoothGattCharacteristic?,
44 | exception: BleException,
45 | ) {
46 | bleNotifyCallback?.onNotifyFailure(bleDevice, characteristic, exception)
47 | channelWeakReference?.get()
48 | ?.trySend(TaskResult(this@SequenceNotifyOperator, false))
49 | }
50 |
51 | override fun onNotifyCancel(
52 | bleDevice: BleDevice,
53 | characteristic: BluetoothGattCharacteristic,
54 | ) {
55 | bleNotifyCallback?.onNotifyCancel(bleDevice, characteristic)
56 | }
57 |
58 | override fun onCharacteristicChanged(
59 | bleDevice: BleDevice,
60 | characteristic: BluetoothGattCharacteristic,
61 | data: ByteArray,
62 | ) {
63 | bleNotifyCallback?.onCharacteristicChanged(bleDevice, characteristic, data)
64 | }
65 |
66 | }
67 |
68 | override fun execute(bleDevice: BleDevice, channel: Channel) {
69 | if (serviceUUID.isNullOrEmpty() || characteristicUUID.isNullOrEmpty()) {
70 | if (continuous) {
71 | channel.trySend(TaskResult(this, false))
72 | }
73 | return
74 | }
75 | if (continuous) {
76 | channelWeakReference = WeakReference(channel)
77 | BleManager.notify(
78 | bleDevice,
79 | serviceUUID!!,
80 | characteristicUUID!!,
81 | callback = wrappedBleNotifyCallback,
82 | useCharacteristicDescriptor = useCharacteristicDescriptor
83 | )
84 | } else {
85 | BleManager.notify(
86 | bleDevice,
87 | serviceUUID!!,
88 | characteristicUUID!!,
89 | callback = bleNotifyCallback,
90 | useCharacteristicDescriptor = useCharacteristicDescriptor
91 | )
92 | }
93 | }
94 |
95 | override val continuous: Boolean
96 | get() = mContinuous
97 | override val timeout: Long
98 | get() = mTimeout
99 |
100 | class Builder() {
101 | private var priority: Int = PRIORITY_WRITE_DEFAULT
102 | private var delay: Long = DELAY_WRITE_DEFAULT
103 | private var serviceUUID: String? = null
104 | private var characteristicUUID: String? = null
105 | private var bleNotifyCallback: BleNotifyCallback? = null
106 | private var continuous: Boolean = false
107 | private var timeout: Long = 0
108 | private var useCharacteristicDescriptor = false
109 |
110 | constructor(notifyOperator: SequenceNotifyOperator) : this() {
111 | this.priority = notifyOperator.priority
112 | this.delay = notifyOperator.delay
113 | this.serviceUUID = notifyOperator.serviceUUID
114 | this.characteristicUUID = notifyOperator.characteristicUUID
115 | this.continuous = notifyOperator.continuous
116 | this.timeout = notifyOperator.timeout
117 | this.useCharacteristicDescriptor = notifyOperator.useCharacteristicDescriptor
118 | }
119 |
120 | fun priority(priority: Int): Builder {
121 | this.priority = priority
122 | return this
123 | }
124 |
125 | fun delay(delay: Long): Builder {
126 | this.delay = delay
127 | return this
128 | }
129 |
130 | fun serviceUUID(serviceUUID: String): Builder {
131 | this.serviceUUID = serviceUUID
132 | return this
133 | }
134 |
135 | fun characteristicUUID(characteristicUUID: String): Builder {
136 | this.characteristicUUID = characteristicUUID
137 | return this
138 | }
139 |
140 | fun bleNotifyCallback(bleNotifyCallback: BleNotifyCallback?): Builder {
141 | this.bleNotifyCallback = bleNotifyCallback
142 | return this
143 | }
144 |
145 | fun continuous(continuous: Boolean): Builder {
146 | this.continuous = continuous
147 | return this
148 | }
149 |
150 | fun timeout(timeout: Long): Builder {
151 | this.timeout = timeout
152 | return this
153 | }
154 |
155 | @SuppressLint("PrivateApi")
156 | fun applySequenceNotifyOperator(notifyOperator: SequenceNotifyOperator) {
157 | notifyOperator.serviceUUID = this.serviceUUID
158 | notifyOperator.characteristicUUID = this.characteristicUUID
159 | notifyOperator.bleNotifyCallback = this.bleNotifyCallback
160 | notifyOperator.mContinuous = this.continuous
161 | notifyOperator.mTimeout = this.timeout
162 | notifyOperator.useCharacteristicDescriptor = this.useCharacteristicDescriptor
163 | }
164 |
165 | fun build(): SequenceNotifyOperator {
166 | return SequenceNotifyOperator(priority, delay).apply {
167 | applySequenceNotifyOperator(this)
168 | }
169 | }
170 | }
171 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_operate.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
18 |
19 |
25 |
26 |
34 |
35 |
42 |
43 |
44 |
50 |
51 |
57 |
58 |
63 |
64 |
65 |
71 |
72 |
81 |
82 |
94 |
95 |
106 |
107 |
108 |
115 |
116 |
117 |
123 |
124 |
133 |
134 |
141 |
142 |
143 |
149 |
150 |
158 |
159 |
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/library/src/main/java/com/huyuhui/fastble/queue/operate/SequenceWriteOperator.kt:
--------------------------------------------------------------------------------
1 | package com.huyuhui.fastble.queue.operate
2 |
3 | import android.annotation.SuppressLint
4 | import android.bluetooth.BluetoothGattCharacteristic
5 | import com.huyuhui.fastble.BleManager
6 | import com.huyuhui.fastble.bluetooth.BleOperator
7 | import com.huyuhui.fastble.callback.BleWriteCallback
8 | import com.huyuhui.fastble.data.BleDevice
9 | import com.huyuhui.fastble.exception.BleException
10 | import com.huyuhui.fastble.queue.TaskResult
11 | import kotlinx.coroutines.channels.Channel
12 | import java.lang.ref.WeakReference
13 |
14 | const val PRIORITY_WRITE_DEFAULT = 500
15 | const val DELAY_WRITE_DEFAULT: Long = 100
16 |
17 | @SuppressLint("MissingPermission")
18 | @Suppress("unused")
19 | class SequenceWriteOperator private constructor(priority: Int, delay: Long) :
20 | SequenceBleOperator(priority, delay) {
21 | var serviceUUID: String? = null
22 | private set
23 | var characteristicUUID: String? = null
24 | private set
25 | var data: ByteArray? = null
26 | private set
27 | var bleWriteCallback: BleWriteCallback? = null
28 | private set
29 | var split: Boolean = true
30 | private set
31 |
32 | var splitNum: Int = BleManager.splitWriteNum
33 | private set
34 | var continueWhenLastFail: Boolean = false
35 | private set
36 | var intervalBetweenTwoPackage: Long = 0
37 | private set
38 | var writeType: Int = BleOperator.WRITE_TYPE_DEFAULT
39 | private set
40 | private var mContinuous = false
41 |
42 | //在队列中的超时时间
43 | private var mTimeout = 0L
44 |
45 | //这个写入过程的超时时间
46 | private var operateTimeout = BleManager.operateTimeout
47 | private var channelWeakReference: WeakReference>? = null
48 | private val wrappedBleWriteCallback by lazy {
49 | object : BleWriteCallback() {
50 | override fun onWriteSuccess(
51 | bleDevice: BleDevice,
52 | characteristic: BluetoothGattCharacteristic,
53 | current: Int,
54 | total: Int,
55 | justWrite: ByteArray,
56 | data: ByteArray,
57 | ) {
58 | bleWriteCallback?.onWriteSuccess(
59 | bleDevice,
60 | characteristic,
61 | current,
62 | total,
63 | justWrite,
64 | data
65 | )
66 | if (current == total) {
67 | channelWeakReference?.get()
68 | ?.trySend(TaskResult(this@SequenceWriteOperator, true))
69 | }
70 | }
71 |
72 | override fun onWriteFailure(
73 | bleDevice: BleDevice,
74 | characteristic: BluetoothGattCharacteristic?,
75 | exception: BleException,
76 | current: Int,
77 | total: Int,
78 | justWrite: ByteArray?,
79 | data: ByteArray?,
80 | isTotalFail: Boolean,
81 | ) {
82 | bleWriteCallback?.onWriteFailure(
83 | bleDevice,
84 | characteristic,
85 | exception,
86 | current,
87 | total,
88 | justWrite,
89 | data,
90 | isTotalFail
91 | )
92 | if (isTotalFail) {
93 | channelWeakReference?.get()
94 | ?.trySend(TaskResult(this@SequenceWriteOperator, false))
95 | }
96 | }
97 |
98 | }
99 | }
100 |
101 | override fun execute(bleDevice: BleDevice, channel: Channel) {
102 | if (serviceUUID.isNullOrEmpty() || characteristicUUID.isNullOrEmpty()) {
103 | if (continuous) {
104 | channel.trySend(TaskResult(this, false))
105 | }
106 | return
107 | }
108 | if (continuous) {
109 | channelWeakReference = WeakReference(channel)
110 | BleManager.write(
111 | bleDevice,
112 | serviceUUID!!,
113 | characteristicUUID!!,
114 | callback = wrappedBleWriteCallback,
115 | writeType = writeType,
116 | data = data,
117 | split = split,
118 | splitNum = splitNum,
119 | continueWhenLastFail = continueWhenLastFail,
120 | intervalBetweenTwoPackage = intervalBetweenTwoPackage,
121 | timeout = operateTimeout
122 | )
123 | } else {
124 | BleManager.write(
125 | bleDevice,
126 | serviceUUID!!,
127 | characteristicUUID!!,
128 | callback = bleWriteCallback,
129 | writeType = writeType,
130 | data = data,
131 | split = split,
132 | splitNum = splitNum,
133 | continueWhenLastFail = continueWhenLastFail,
134 | intervalBetweenTwoPackage = intervalBetweenTwoPackage,
135 | timeout = operateTimeout
136 | )
137 | }
138 | }
139 |
140 | override val continuous: Boolean
141 | get() = mContinuous
142 | override val timeout: Long
143 | get() = mTimeout
144 |
145 | open class Builder() {
146 | private var priority: Int = PRIORITY_WRITE_DEFAULT
147 | private var delay: Long = DELAY_WRITE_DEFAULT
148 | private var serviceUUID: String? = null
149 | private var characteristicUUID: String? = null
150 | private var data: ByteArray? = null
151 | private var bleWriteCallback: BleWriteCallback? = null
152 | private var split: Boolean = true
153 | private var splitNum = BleManager.splitWriteNum
154 | private var continueWhenLastFail: Boolean = false
155 | private var intervalBetweenTwoPackage: Long = 0
156 | private var writeType: Int = BleOperator.WRITE_TYPE_DEFAULT
157 | private var continuous: Boolean = false
158 | private var timeout: Long = 0
159 |
160 | private var operateTimeout: Long = BleManager.operateTimeout
161 |
162 | constructor(writeOperator: SequenceWriteOperator) : this() {
163 | this.priority = writeOperator.priority
164 | this.delay = writeOperator.delay
165 | this.serviceUUID = writeOperator.serviceUUID
166 | this.characteristicUUID = writeOperator.characteristicUUID
167 | this.data = writeOperator.data
168 | this.bleWriteCallback = writeOperator.bleWriteCallback
169 | this.split = writeOperator.split
170 | this.splitNum = writeOperator.splitNum
171 | this.continueWhenLastFail = writeOperator.continueWhenLastFail
172 | this.intervalBetweenTwoPackage = writeOperator.intervalBetweenTwoPackage
173 | this.writeType = writeOperator.writeType
174 | this.continuous = writeOperator.continuous
175 | this.timeout = writeOperator.timeout
176 | this.operateTimeout = writeOperator.operateTimeout
177 | }
178 |
179 | fun priority(priority: Int): Builder {
180 | this.priority = priority
181 | return this
182 | }
183 |
184 | fun delay(delay: Long): Builder {
185 | this.delay = delay
186 | return this
187 | }
188 |
189 | fun serviceUUID(serviceUUID: String): Builder {
190 | this.serviceUUID = serviceUUID
191 | return this
192 | }
193 |
194 | fun characteristicUUID(characteristicUUID: String): Builder {
195 | this.characteristicUUID = characteristicUUID
196 | return this
197 | }
198 |
199 | fun data(data: ByteArray?): Builder {
200 | this.data = data
201 | return this
202 | }
203 |
204 | fun bleWriteCallback(bleWriteCallback: BleWriteCallback?): Builder {
205 | this.bleWriteCallback = bleWriteCallback
206 | return this
207 | }
208 |
209 | fun split(split: Boolean): Builder {
210 | this.split = split
211 | return this
212 | }
213 |
214 | fun splitNum(splitNum: Int): Builder {
215 | this.splitNum = splitNum
216 | return this
217 | }
218 |
219 | fun continueWhenLastFail(continueWhenLastFail: Boolean): Builder {
220 | this.continueWhenLastFail = continueWhenLastFail
221 | return this
222 | }
223 |
224 | fun intervalBetweenTwoPackage(intervalBetweenTwoPackage: Long): Builder {
225 | this.intervalBetweenTwoPackage = intervalBetweenTwoPackage
226 | return this
227 | }
228 |
229 | fun writeType(writeType: Int): Builder {
230 | this.writeType = writeType
231 | return this
232 | }
233 |
234 | fun continuous(continuous: Boolean): Builder {
235 | this.continuous = continuous
236 | return this
237 | }
238 |
239 | fun timeout(timeout: Long): Builder {
240 | this.timeout = timeout
241 | return this
242 | }
243 |
244 | fun operateTimeout(operateTimeout: Long) = apply {
245 | this.operateTimeout = operateTimeout
246 | }
247 |
248 | @SuppressLint("PrivateApi")
249 | fun applySequenceWriteOperator(writeOperator: SequenceWriteOperator) {
250 | writeOperator.serviceUUID = this.serviceUUID
251 | writeOperator.characteristicUUID = this.characteristicUUID
252 | writeOperator.data = this.data
253 | writeOperator.bleWriteCallback = this.bleWriteCallback
254 | writeOperator.split = this.split
255 | writeOperator.splitNum = this.splitNum
256 | writeOperator.continueWhenLastFail = this.continueWhenLastFail
257 | writeOperator.intervalBetweenTwoPackage = this.intervalBetweenTwoPackage
258 | writeOperator.writeType = this.writeType
259 | writeOperator.mContinuous = this.continuous
260 | writeOperator.mTimeout = this.timeout
261 | writeOperator.operateTimeout = this.operateTimeout
262 | }
263 |
264 | fun build(): SequenceWriteOperator {
265 | return SequenceWriteOperator(priority, delay).apply {
266 | applySequenceWriteOperator(this)
267 | }
268 | }
269 | }
270 | }
--------------------------------------------------------------------------------