diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 3deebfc3a..000000000 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,213 +0,0 @@ -{ - "originHash" : "fb7ecff82f4ff109531189491cc6cc2299a7357c61874f58bae1e0f7b7d9e8d0", - "pins" : [ - { - "identity" : "abseil-cpp-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/abseil-cpp-binary.git", - "state" : { - "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5", - "version" : "1.2024072200.0" - } - }, - { - "identity" : "app-check", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/app-check.git", - "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" - } - }, - { - "identity" : "appauth-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/openid/AppAuth-iOS.git", - "state" : { - "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", - "version" : "1.7.6" - } - }, - { - "identity" : "dkcamera", - "kind" : "remoteSourceControl", - "location" : "https://github.com/zhangao0086/DKCamera", - "state" : { - "branch" : "master", - "revision" : "5c691d11014b910aff69f960475d70e65d9dcc96" - } - }, - { - "identity" : "dkimagepickercontroller", - "kind" : "remoteSourceControl", - "location" : "https://github.com/zhangao0086/DKImagePickerController", - "state" : { - "branch" : "4.3.9", - "revision" : "0bdfeacefa308545adde07bef86e349186335915" - } - }, - { - "identity" : "dkphotogallery", - "kind" : "remoteSourceControl", - "location" : "https://github.com/zhangao0086/DKPhotoGallery", - "state" : { - "branch" : "master", - "revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d" - } - }, - { - "identity" : "firebase-ios-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/firebase-ios-sdk", - "state" : { - "revision" : "3663b1aa6c7a1bed67ee80fd09dc6d0f9c3bb660", - "version" : "11.13.0" - } - }, - { - "identity" : "flutterfire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/flutterfire", - "state" : { - "revision" : "a80a123386fd4904cad6938673020a7bcf31b2f2", - "version" : "3.13.0-firebase-core-swift" - } - }, - { - "identity" : "googleappmeasurement", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleAppMeasurement.git", - "state" : { - "revision" : "543071966b3fb6613a2fc5c6e7112d1e998184a7", - "version" : "11.13.0" - } - }, - { - "identity" : "googledatatransport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleDataTransport.git", - "state" : { - "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", - "version" : "10.1.0" - } - }, - { - "identity" : "googlesignin-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleSignIn-iOS.git", - "state" : { - "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", - "version" : "7.1.0" - } - }, - { - "identity" : "googleutilities", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleUtilities.git", - "state" : { - "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", - "version" : "8.1.0" - } - }, - { - "identity" : "grpc-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/grpc-binary.git", - "state" : { - "revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71", - "version" : "1.69.0" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", - "version" : "3.5.0" - } - }, - { - "identity" : "gtmappauth", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GTMAppAuth.git", - "state" : { - "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", - "version" : "4.1.1" - } - }, - { - "identity" : "interop-ios-for-google-sdks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/interop-ios-for-google-sdks.git", - "state" : { - "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe", - "version" : "101.0.0" - } - }, - { - "identity" : "leveldb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/leveldb.git", - "state" : { - "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", - "version" : "1.22.5" - } - }, - { - "identity" : "nanopb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/nanopb.git", - "state" : { - "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", - "version" : "2.30910.0" - } - }, - { - "identity" : "promises", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/promises.git", - "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" - } - }, - { - "identity" : "sdwebimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SDWebImage/SDWebImage", - "state" : { - "revision" : "cac9a55a3ae92478a2c95042dcc8d9695d2129ca", - "version" : "5.21.0" - } - }, - { - "identity" : "swift-protobuf", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-protobuf.git", - "state" : { - "revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f", - "version" : "1.29.0" - } - }, - { - "identity" : "swiftygif", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kirualex/SwiftyGif.git", - "state" : { - "revision" : "4430cbc148baa3907651d40562d96325426f409a", - "version" : "5.4.5" - } - }, - { - "identity" : "tocropviewcontroller", - "kind" : "remoteSourceControl", - "location" : "https://github.com/TimOliver/TOCropViewController", - "state" : { - "revision" : "a634cb7cdfd580006e79a6e74e64417fe9e9783b", - "version" : "2.7.4" - } - } - ], - "version" : 3 -} diff --git a/lib/data/services/network/nw_api.dart b/lib/data/services/network/nw_api.dart index c5ed95dd8..94d7115f8 100644 --- a/lib/data/services/network/nw_api.dart +++ b/lib/data/services/network/nw_api.dart @@ -1,6 +1,5 @@ class NWApi { // static const baseApi = "https://www.deapsound.com"; - //测试服务器链接地址,经常修改变动,开发使用 static const baseApi = "https://api.deapsound.com"; - // static const baseApi = "https://api.ideapsound.com"; + //static const baseApi = "https://api.ideapsound.com"; } diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index 992ca6838..c9c827551 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:jl_ota/jl_ota.dart'; +import 'package:ota/ota.dart'; import '../../../data/services/ble_manager.dart'; class OtaUpgradeController extends GetxController { @@ -22,7 +23,7 @@ class OtaUpgradeController extends GetxController { final isUpdateAvailable = true.obs; final RxDouble progress = 0.0.obs; - final JlOta jla = JlOta(); + var jlota = Ota(); // 蓝牙服务 final _bleManager = Get.find(); @override @@ -54,6 +55,7 @@ class OtaUpgradeController extends GetxController { Future _getCurrentVersion() async { try { + jlota.getPlatformVersion(); // 添加获取设备信息的重试机制 DeviceInfo? deviceInfo = _bleManager.deviceInfo.value; @@ -292,7 +294,8 @@ class OtaUpgradeController extends GetxController { // 开始升级 void _installApp(String path) async { - jla.getPlatformVersion(); + jlota.searchDevices(); + jlota.startUpgrade(); // JlOtaPlugin() // final File file = File(path); // if (await file.exists()) { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index abbe6e38a..4ed987ea4 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -24,6 +24,10 @@ import com.microsoft.cognitiveservices.speech.audio.AudioStreamFormat import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit + +import java.io.File +import java.io.FileOutputStream +import java.io.RandomAccessFile /** * Azure语音识别辅助类,支持麦克风和外部音频源的单次和连续语音识别 * @@ -528,7 +532,12 @@ class AzureAsrHelper(private val context: Context) { private inner class MicrophoneStream : PullAudioInputStreamCallback() { private var audioRecord: AudioRecord? = null private var echoCanceler: AcousticEchoCanceler? = null - + private var currentAudioFile: File? = null + private var fos: FileOutputStream? = null + + // 用于存储音频数据的缓冲区 + private val dataBuffer = mutableListOf() + private var totalBytesWritten = 0 // 音频配置 private val sampleRate = 16000 private val channelConfig = AudioFormat.CHANNEL_IN_MONO @@ -546,6 +555,20 @@ class AzureAsrHelper(private val context: Context) { */ private fun initMicrophone() { try { + // 创建新的音频文件 + val timestamp = System.currentTimeMillis() + val filePath = context.getExternalFilesDir(null)?.absolutePath + "/recorded_audio_$timestamp.wav" + currentAudioFile = File(filePath) + currentAudioFile?.createNewFile() + + // 仅写入初始文件头 + fos = FileOutputStream(currentAudioFile).apply { + write(generateWavHeader(0)) // 初始0长度 + close() + } + + // 追加音频数据到文件 + fos = FileOutputStream(currentAudioFile, true) // 创建录音对象 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { val format = AudioFormat.Builder() @@ -616,18 +639,80 @@ class AzureAsrHelper(private val context: Context) { Log.e(tag, "读取音频数据失败: $result") return 0 } + // 将音频数据保存成wav格式的音频文件 + saveAudioDataToWav(buffer, result) return result } catch (e: Exception) { Log.e(tag, "读取音频异常: ${e.message}") return 0 } } + /** + * 保存音频数据到 WAV 文件 + */ +private fun saveAudioDataToWav(buffer: ByteArray, length: Int) { + // 复制数据到新数组 + val copy = ByteArray(length) + System.arraycopy(buffer, 0, copy, 0, length) + + // 添加到缓冲区 + dataBuffer.add(copy) + totalBytesWritten += length + + // 写入文件 + try { + fos?.write(copy, 0, length) + } catch (e: Exception) { + Log.e(tag, "写入音频数据失败: ${e.message}") + } +} + + // 更新文件头生成(修正RIFF长度计算) + private fun generateWavHeader(dataLength: Int): ByteArray { + val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据 + val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数 + return byteArrayOf( + 'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(), + (totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(), + ((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(), + 'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(), + 'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(), + 16, 0, 0, 0, // PCM头长度 + 1, 0, // PCM格式 + 1, 0, // 单声道 + (sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率 + (byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率 + 2, 0, // 块对齐 (通道数 * 样本位数/8) + 16, 0, // 样本位数 + 'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(), + (dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(), + ((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte() + ) + } /** * 关闭音频资源 */ override fun close() { releaseAudioResources() + try { + // 关闭文件流 + fos?.close() + + // 更新WAV文件头 + currentAudioFile?.let { file -> + RandomAccessFile(file, "rw").use { raf -> + raf.seek(0) + raf.write(generateWavHeader(totalBytesWritten)) + } + Log.d(tag, "音频文件保存完成: ${file.absolutePath}") + } + } catch (e: Exception) { + Log.e(tag, "更新WAV文件头失败: ${e.message}") + } finally { + fos = null + currentAudioFile = null + } } /** diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 999d56c3a..7ca013ed9 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -80,7 +80,7 @@ object BleService { private var bluetoothLeScanner: BluetoothLeScanner? = null // GATT连接相关 - private var bluetoothGatt: BluetoothGatt? = null + var bluetoothGatt: BluetoothGatt? = null private var notifyChar: BluetoothGattCharacteristic? = null private var writeChar: BluetoothGattCharacteristic? = null private var audioChar: BluetoothGattCharacteristic? = null diff --git a/local_plugins/ota/.gitignore b/local_plugins/ota/.gitignore new file mode 100644 index 000000000..e7d347d9d --- /dev/null +++ b/local_plugins/ota/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/local_plugins/ota/.metadata b/local_plugins/ota/.metadata new file mode 100644 index 000000000..6015b831a --- /dev/null +++ b/local_plugins/ota/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ea121f8859e4b13e47a8f845e4586164519588bc" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: android + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: ios + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/local_plugins/ota/CHANGELOG.md b/local_plugins/ota/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/local_plugins/ota/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/local_plugins/ota/LICENSE b/local_plugins/ota/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/local_plugins/ota/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/local_plugins/ota/README.md b/local_plugins/ota/README.md new file mode 100644 index 000000000..26563dd24 --- /dev/null +++ b/local_plugins/ota/README.md @@ -0,0 +1,15 @@ +# ota + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/to/develop-plugins), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/local_plugins/ota/analysis_options.yaml b/local_plugins/ota/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/local_plugins/ota/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/local_plugins/ota/android/.gitignore b/local_plugins/ota/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/local_plugins/ota/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/local_plugins/ota/android/build.gradle b/local_plugins/ota/android/build.gradle new file mode 100644 index 000000000..b6ead6ba6 --- /dev/null +++ b/local_plugins/ota/android/build.gradle @@ -0,0 +1,68 @@ +group = "com.example.ota" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "1.8.22" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.7.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "com.example.ota" + + compileSdk = 35 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 21 + } + + dependencies { + implementation("com.jieli.jl_bt_ota:jl_bt_ota:1.9.2") + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + add("compileOnly", project(":ble_service")) + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/local_plugins/ota/android/libs/jl_bt_ota_V1.9.2-release.aar b/local_plugins/ota/android/libs/jl_bt_ota_V1.9.2-release.aar new file mode 100644 index 000000000..9ecf85489 Binary files /dev/null and b/local_plugins/ota/android/libs/jl_bt_ota_V1.9.2-release.aar differ diff --git a/local_plugins/ota/android/settings.gradle b/local_plugins/ota/android/settings.gradle new file mode 100644 index 000000000..3deb53a8d --- /dev/null +++ b/local_plugins/ota/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'ota' diff --git a/local_plugins/ota/android/src/main/AndroidManifest.xml b/local_plugins/ota/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c940a84da --- /dev/null +++ b/local_plugins/ota/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/DeviceConnection.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/DeviceConnection.kt new file mode 100644 index 000000000..0a9abdb19 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/DeviceConnection.kt @@ -0,0 +1,26 @@ +package com.jieli.otasdk.model + +import android.bluetooth.BluetoothDevice + +/** + * @author zqjasonZhong + * @since 2022/9/9 + * @email zhongzhuocheng@zh-jieli.com + * @desc 设备连接状态 + */ +class DeviceConnection(val device: BluetoothDevice?, var state: Int) { + + override fun hashCode(): Int { + return device.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other !is DeviceConnection) return false + return device == other.device + } + + override fun toString(): String { + return "DeviceConnection(device=$device, state=$state)" + } + +} \ No newline at end of file diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt new file mode 100644 index 000000000..cabac136a --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/OtaPlugin.kt @@ -0,0 +1,148 @@ +package com.example.ota + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +import android.bluetooth.BluetoothDevice +import android.content.Context +import com.jieli.otasdk.tool.ota.OTAManager +import com.jieli.jl_bt_ota.constant.ErrorCode +import com.jieli.jl_bt_ota.constant.StateCode +import com.jieli.jl_bt_ota.interfaces.BtEventCallback +import com.jieli.jl_bt_ota.interfaces.IUpgradeCallback +import com.jieli.jl_bt_ota.model.base.BaseError +import com.jieli.jl_bt_ota.model.response.TargetInfoResponse +import com.jieli.jl_bt_ota.util.BluetoothUtil +import com.jieli.jl_bt_ota.util.CHexConver +import com.jieli.jl_bt_ota.util.JL_Log +import com.jieli.otasdk.tool.bluetooth.BluetoothHelper +import androidx.lifecycle.MutableLiveData +import com.jieli.otasdk.model.DeviceConnection +import android.util.Log +import androidx.annotation.NonNull +import com.jieli.otasdk.tool.config.ConfigHelper +import com.yunqiinnovation.ble_service.BleService + class CustomUpdateCallback( + + val device: BluetoothDevice? +) : IUpgradeCallback { + companion object { + private const val TAG = "JlOtaPlugin" + private const val CHANNEL_NAME = "com.yunqiinnovation.jl_ota" + private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_ota/events" + + } + + override fun onStartOTA() { + Log.d(TAG, "onStartOTA开始升级") + // viewModel.otaStateMLD.value = OTAStart(device) + } + + override fun onNeedReconnect(addr: String?, isNewReconnectWay: Boolean) { + Log.d(TAG, "onNeedReconnect需要重新链接") + + } + + override fun onProgress(type: Int, progress: Float) { + Log.d(TAG, "onProgress正在进展") + //viewModel.otaStateMLD.value = OTAWorking(device, type, progress) + } + + override fun onStopOTA() { + Log.d(TAG, "onStartOTA停止升级") + + } + + override fun onCancelOTA() { + Log.d(TAG, "onCancelOTA取消升级") + + } + + override fun onError(error: BaseError?) { + val errorMessage = error?.message ?: "Unknown error" + Log.e(TAG, "升级失败: ${error?.message ?: "未知错误"}") + + // 如果需要打印完整错误对象信息(假设BaseError有重写toString()) + Log.d(TAG, "完整错误对象: ${error ?: "null"}") + } +} + +/** OtaPlugin */ +class OtaPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + private lateinit var context: Context + +private lateinit var bluetoothHelper : BluetoothHelper + val otaConnectionMLD = MutableLiveData() + val mandatoryUpgradeMLD = MutableLiveData() + private lateinit var otaManager : OTAManager + + private val bluetoothCallback = object : BtEventCallback() { + + override fun onConnection(device: BluetoothDevice?, status: Int) { + if ((status == StateCode.CONNECTION_FAILED || status == StateCode.CONNECTION_DISCONNECT) && bluetoothHelper.getConnectedDevice() != null) { + bluetoothHelper.disconnectDevice(bluetoothHelper.getConnectedDevice()) + } + otaConnectionMLD.value = DeviceConnection(device, status) + } + + override fun onMandatoryUpgrade(device: BluetoothDevice?) { + mandatoryUpgradeMLD.postValue(device!!) + } + } + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + context = flutterPluginBinding.applicationContext + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "ota") + channel.setMethodCallHandler(this) + + Log.d("TAG", "onAttachedToEngine") + } + + override fun onMethodCall(call: MethodCall, result: Result) { + if (call.method == "getPlatformVersion") { + Log.d("TAG", "Connection") + // otaManager.configHelper + // 确保先初始化 ConfigHelper + ConfigHelper.initialize(context) + otaManager = OTAManager(context) + bluetoothHelper = BluetoothHelper(context) + // 如果需要额外的初始化逻辑,请在这里添加 + otaManager.registerBluetoothCallback(bluetoothCallback) + + // otaManager.registerBluetoothCallback(bluetoothCallback) + result.success("Android ${android.os.Build.VERSION.RELEASE}") + }else if (call.method == "searchDevices") { + Log.d("TAG", "searchDevices") + // otaManager.configHelper + BleService.disconnect() + // 确保先初始化 ConfigHelper + otaManager.bluetoothHelper.startScan(BleService.bluetoothGatt?.device) + + // otaManager.registerBluetoothCallback(bluetoothCallback) + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else if (call.method == "startUpgrade") { + Log.d("TAG", "startUpgrade") + var filePath = "/storage/emulated/0/Download/JieLiOTA/upgrade/update - v0.1.0_添加杰理OTA,优化主从切换拾音丢失问题.ufw" + otaManager.bluetoothOption.firmwareFilePath = filePath + otaManager.startOTA( + CustomUpdateCallback( + otaManager.bluetoothHelper.getConnectedDevice() + ) + ) + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/BTEventCbHelper.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/BTEventCbHelper.kt new file mode 100644 index 000000000..5661e20a4 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/BTEventCbHelper.kt @@ -0,0 +1,91 @@ +package com.jieli.otasdk.tool.bluetooth + +import android.bluetooth.BluetoothDevice +import android.os.Handler +import android.os.Looper +import com.jieli.otasdk.tool.ota.ble.model.BleScanInfo +import java.util.* + +/** + * @author zqjasonZhong + * @since 2022/9/14 + * @email zhongzhuocheng@zh-jieli.com + * @desc 蓝牙事件回调辅助类 + */ +class BTEventCbHelper : OnBTEventCallback() { + private val callbacks = mutableListOf() + private val uiHandler = Handler(Looper.getMainLooper()) + + fun registerCallback(callback: OnBTEventCallback) { + if (callbacks.contains(callback)) return + callbacks.add(callback) + } + + fun unregisterCallback(callback: OnBTEventCallback) { + if (callbacks.isEmpty()) return + callbacks.remove(callback) + } + + fun release() { + callbacks.clear() + uiHandler.removeCallbacksAndMessages(null) + } + + override fun onAdapterChange(bEnabled: Boolean) { + callbackEvent(object : CallbackImpl { + override fun onCallback(callback: OnBTEventCallback) { + callback.onAdapterChange(bEnabled) + } + }) + } + + override fun onDiscoveryChange(bStart: Boolean, scanType: Int) { + callbackEvent(object : CallbackImpl { + override fun onCallback(callback: OnBTEventCallback) { + callback.onDiscoveryChange(bStart, scanType) + } + }) + } + + override fun onDiscovery(device: BluetoothDevice?, bleScanMessage: BleScanInfo?) { + callbackEvent(object : CallbackImpl { + override fun onCallback(callback: OnBTEventCallback) { + callback.onDiscovery(device, bleScanMessage) + } + }) + } + + override fun onDeviceConnection(device: BluetoothDevice?, way: Int, status: Int) { + callbackEvent(object : CallbackImpl { + override fun onCallback(callback: OnBTEventCallback) { + callback.onDeviceConnection(device, way, status) + } + }) + } + + override fun onReceiveData(device: BluetoothDevice?, way: Int, uuid: UUID?, data: ByteArray?) { + callbackEvent(object : CallbackImpl { + override fun onCallback(callback: OnBTEventCallback) { + callback.onReceiveData(device, way, uuid, data) + } + }) + } + + override fun onBleMtuChange(device: BluetoothDevice?, mtu: Int, status: Int) { + callbackEvent(object : CallbackImpl { + override fun onCallback(callback: OnBTEventCallback) { + callback.onBleMtuChange(device, mtu, status) + } + }) + } + + private fun callbackEvent(impl: CallbackImpl?) { + if (null == impl) return + val runnable = CallbackRunnable(callbacks, impl) + if (Thread.currentThread().id == Looper.getMainLooper().thread.id) { + runnable.run() + } else { + uiHandler.post(runnable) + } + } +} \ No newline at end of file diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/BluetoothHelper.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/BluetoothHelper.kt new file mode 100644 index 000000000..85ec1eb32 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/BluetoothHelper.kt @@ -0,0 +1,204 @@ +package com.jieli.otasdk.tool.bluetooth + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothProfile +import android.content.Context +import com.jieli.jl_bt_ota.util.BluetoothUtil +import com.jieli.jl_bt_ota.util.CHexConver +import com.jieli.otasdk.tool.config.ConfigHelper +import com.jieli.otasdk.tool.ota.ble.BleManager +import com.jieli.otasdk.tool.ota.ble.interfaces.BleEventCallback +import com.jieli.otasdk.tool.ota.ble.model.BleScanInfo +import com.jieli.otasdk.tool.bluetooth.OtaConstant +import java.util.* +import android.util.Log +/** + * @author zqjasonZhong + * @since 2022/9/14 + * @email zhongzhuocheng@zh-jieli.com + * @desc 蓝牙操作辅助类 + */ +class BluetoothHelper(private val context: Context) { + + private val configHelper = ConfigHelper.getInstance() + private val bleManager = BleManager.getInstance(context)!! + private val btEventCbHelper = BTEventCbHelper() + + companion object { + + const val TAG: String = "BluetoothHelper" + + @Volatile + private var instance: BluetoothHelper? = null + + fun getInstance(context: Context): BluetoothHelper = instance ?: synchronized(this) { + instance ?: BluetoothHelper(context).also { instance = it } + } + } + + private val bleEventCallback = object : BleEventCallback() { + + override fun onAdapterChange(bEnabled: Boolean) { + btEventCbHelper.onAdapterChange(bEnabled) + } + + override fun onDiscoveryBleChange(bStart: Boolean) { + btEventCbHelper.onDiscoveryChange(bStart, OtaConstant.PROTOCOL_BLE) + } + + override fun onDiscoveryBle(device: BluetoothDevice?, bleScanMessage: BleScanInfo?) { + btEventCbHelper.onDiscovery(device, bleScanMessage) + } + + override fun onBleConnection(device: BluetoothDevice?, status: Int) { + btEventCbHelper.onDeviceConnection(device, OtaConstant.PROTOCOL_BLE, status) + } + + override fun onBleDataNotification( + device: BluetoothDevice?, + serviceUuid: UUID?, + characteristicsUuid: UUID?, + data: ByteArray? + ) { + btEventCbHelper.onReceiveData( + device, + OtaConstant.PROTOCOL_BLE, + characteristicsUuid, + data + ) + } + + override fun onBleDataBlockChanged(device: BluetoothDevice?, block: Int, status: Int) { + btEventCbHelper.onBleMtuChange(device, block, status) + } + } + + + + init { +// if (configHelper.isBleWay()) { + bleManager.registerBleEventCallback(bleEventCallback) +// } else { + +// } + } + + fun destroy() { + bleManager.unregisterBleEventCallback(bleEventCallback) + bleManager.destroy() + + btEventCbHelper.release() + instance = null + } + + fun registerCallback(callback: OnBTEventCallback) { + btEventCbHelper.registerCallback(callback) + } + + fun unregisterCallback(callback: OnBTEventCallback) { + btEventCbHelper.unregisterCallback(callback) + } + + fun isConnected(): Boolean { + return getConnectedDevice() != null + } + + fun isDeviceConnected(device: BluetoothDevice?): Boolean { + return BluetoothUtil.deviceEquals(getConnectedDevice(), device) + } + + fun isScanning(): Boolean = + bleManager.isBleScanning + + + fun isConnecting(): Boolean = + bleManager.isBleScanning + + + fun getConnectedDevice(): BluetoothDevice? { + + return bleManager.connectedBtDevice + + } + + fun getConnectedGatt(): BluetoothGatt? { + return if (configHelper.isBleWay()) { + bleManager.getConnectedBtGatt(getConnectedDevice()) + } else { + null + } + } + + fun getBleMtu(): Int { + return if (configHelper.isBleWay()) { + return bleManager.getBleMtu(getConnectedDevice()) + } else { + 20 + } + } + + fun startScan(device: BluetoothDevice?) { + Log.i(TAG, ", startScan" + device); + + bleManager.startLeScan(device) + + } + + fun stopScan() { + + bleManager.stopLeScan() + + } + + fun connectDevice(device: BluetoothDevice?): Boolean = + + bleManager.connectBleDevice(device) + + + fun disconnectDevice(device: BluetoothDevice?) { + + bleManager.disconnectBleDevice(device) + + } + + fun connectBleDevice(device: BluetoothDevice?): Boolean = + bleManager.connectBleDevice(device) + + fun reconnectDevice(address: String?, isUseNewAdv: Boolean) { + if (configHelper.isBleWay()) { + bleManager.reconnectDevice(address, isUseNewAdv) + } else { + //TODO:需要增加SPP自定义回连方式 + } + } + + fun writeDataToDevice(bluetoothDevice: BluetoothDevice?, byteArray: ByteArray?): Boolean { + if (null == bluetoothDevice || null == byteArray || byteArray.isEmpty()) return false + if (bleManager.connectedBtDevice != null) {//目前连接的设备是ble + bleManager.writeDataByBleAsync( + bluetoothDevice, + BleManager.BLE_UUID_SERVICE, + BleManager.BLE_UUID_WRITE, + byteArray + ) { device, serviceUUID, characteristicUUID, result, data -> + Log.i( + TAG, + "-writeDataByBleAsync- device:${ + printDeviceInfo(device) + }, result = $result,\ndata:[${ + CHexConver.byte2HexStr(data) + }]" + ) + } + } + return true + } + + private fun printDeviceInfo(device: BluetoothDevice?): String? { + //return AppUtil.printBtDeviceInfo(device) + //注释掉 + return "返回设备信息" + } + +} \ No newline at end of file diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/CallbackImpl.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/CallbackImpl.kt new file mode 100644 index 000000000..52709b99c --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/CallbackImpl.kt @@ -0,0 +1,12 @@ +package com.jieli.otasdk.tool.bluetooth + +/** + * @author zqjasonZhong + * @since 2022/9/14 + * @email zhongzhuocheng@zh-jieli.com + * @desc 回调实现类 + */ +interface CallbackImpl { + + fun onCallback(callback: T) +} \ No newline at end of file diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/CallbackRunnable.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/CallbackRunnable.kt new file mode 100644 index 000000000..62bea6970 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/CallbackRunnable.kt @@ -0,0 +1,23 @@ +package com.jieli.otasdk.tool.bluetooth + +/** + * @author zqjasonZhong + * @since 2022/9/14 + * @email zhongzhuocheng@zh-jieli.com + * @desc 回调处理类 + */ +class CallbackRunnable constructor( + private val callbacks: MutableList, + private val impl: CallbackImpl? +) : + Runnable { + + override fun run() { + if (callbacks.isEmpty() || impl == null) return + val temp = mutableListOf() + temp.addAll(callbacks) + temp.forEach { + impl.onCallback(it) + } + } +} \ No newline at end of file diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/OnBTEventCallback.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/OnBTEventCallback.kt new file mode 100644 index 000000000..41a93eeb4 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/OnBTEventCallback.kt @@ -0,0 +1,38 @@ +package com.jieli.otasdk.tool.bluetooth + +import android.bluetooth.BluetoothDevice +import com.jieli.otasdk.tool.ota.ble.model.BleScanInfo +import java.util.* + +/** + * @author zqjasonZhong + * @since 2022/9/14 + * @email zhongzhuocheng@zh-jieli.com + * @desc 蓝牙事件回调 + */ +open class OnBTEventCallback { + + open fun onAdapterChange(bEnabled: Boolean) { + + } + + open fun onDiscoveryChange(bStart: Boolean, scanType: Int) { + + } + + open fun onDiscovery(device: BluetoothDevice?, bleScanMessage: BleScanInfo?) { + + } + + open fun onDeviceConnection(device: BluetoothDevice?, way: Int, status: Int) { + + } + + open fun onReceiveData(device: BluetoothDevice?, way: Int, uuid: UUID?, data: ByteArray?) { + + } + + open fun onBleMtuChange(device: BluetoothDevice?, mtu: Int, status: Int) { + + } +} \ No newline at end of file diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/OtaConstant.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/OtaConstant.kt new file mode 100644 index 000000000..c7cfb37cb --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/bluetooth/OtaConstant.kt @@ -0,0 +1,54 @@ +package com.jieli.otasdk.tool.bluetooth + +import java.util.* + +/** + * 常量声明 + * + * @author zqjasonZhong + * @date 2019/12/30 + */ +class OtaConstant { + + companion object { + + val UUID_A2DP: UUID = UUID.fromString("0000110b-0000-1000-8000-00805f9b34fb") + val UUID_SPP: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb") + + //Ble协议 + const val PROTOCOL_BLE = 0 + //Spp协议 + const val PROTOCOL_SPP = 1 + + const val CURRENT_PROTOCOL = PROTOCOL_BLE + + //是否使用设备认证 + const val IS_NEED_DEVICE_AUTH = true + + //是否HID设备连接 + const val HID_DEVICE_WAY = false + + //是否需要自定义连接方式 + const val NEED_CUSTOM_RECONNECT_WAY = false + + //是否使用SPP多通道连接 + const val USE_SPP_MULTIPLE_CHANNEL = false + + //是否使用自动化测试 + const val AUTO_TEST_OTA = false + //自动化测试次数 + const val AUTO_TEST_COUNT = 30 + + //是否自动化测试时允许容错 + const val AUTO_FAULT_TOLERANT = false + //容错次数 + const val AUTO_FAULT_TOLERANT_COUNT = 1 + + //dir + const val DIR_ROOT = "JieLiOTA" + const val DIR_UPGRADE = "upgrade" + const val DIR_LOGCAT = "logcat" + + const val SCAN_TIMEOUT = 16 * 1000L + } +} \ No newline at end of file diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/config/ConfigHelper.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/config/ConfigHelper.kt new file mode 100644 index 000000000..2e75a9a0a --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/config/ConfigHelper.kt @@ -0,0 +1,187 @@ +package com.jieli.otasdk.tool.config + +import android.annotation.SuppressLint +import android.content.Context +import androidx.annotation.IntRange +import com.jieli.jl_bt_ota.constant.BluetoothConstant +import com.jieli.otasdk.tool.bluetooth.OtaConstant + +/** + * @author zqjasonZhong + * @since 2022/9/8 + * @email zhongzhuocheng@zh-jieli.com + * @desc 配置辅助类 + */ +open class ConfigHelper private constructor(context: Context) { + + companion object { + //download文件夹 + private const val KEY_DOWNLOAD_URI = "download_uri" + + //通讯方式 + private const val KEY_COMMUNICATION_WAY = "communication_way" + + //是否使用设备认证 + private const val KEY_IS_USE_DEVICE_AUTH = "is_use_device_auth" + + //是否HID设备 + private const val KEY_IS_HID_DEVICE = "is_hid_device" + + //是否使用自定义回连方式 + private const val KEY_USE_CUSTOM_RECONNECT_WAY = "use_custom_reconnect_way" + + //BLE的MTU请求值 + private const val KEY_BLE_MTU_VALUE = "ble_mtu_value" + + //是否启用SPP多通道 + private const val KEY_SPP_MULTIPLE_CHANNEL = "spp_multiple_channel" + + //自定义SPP通道 + private const val KEY_SPP_CUSTOM_UUID = "spp_custom_uuid" + + //是否自动测试OTA + private const val KEY_AUTO_TEST_OTA = "auto_test_ota" + + //自动化测试次数 + private const val KEY_AUTO_TEST_COUNT = "auto_test_count" + + //是否自动测试OTA时,允许容错 + private const val KEY_FAULT_TOLERANT = "fault_tolerant" + + //容错次数 + private const val KEY_FAULT_TOLERANT_COUNT = "fault_tolerant_count" + //扫描过滤参数 + private const val KEY_SCAN_FILTER_STRING = "scan_filter_string" + + //开发者模式 + private const val KEY_DEVELOP_MODE = "develop_mode" + + // 广播音箱模式 + private const val KEY_BROADCAST_BOX = "broadcast_box_switch" + @Volatile + private var instance: ConfigHelper? = null + + // 双重校验锁初始化 + fun initialize(context: Context) = instance ?: synchronized(this) { + instance ?: ConfigHelper(context.applicationContext).also { + instance = it + } + } + + // 安全获取实例方法 + fun getInstance(): ConfigHelper = instance ?: throw IllegalStateException( + "Call ConfigHelper.initialize() first" + ) + } + + private val preferences = context.getSharedPreferences("ota_config_data", Context.MODE_PRIVATE) + + fun isBleWay(): Boolean = preferences.getInt( + KEY_COMMUNICATION_WAY, + OtaConstant.CURRENT_PROTOCOL + ) == OtaConstant.PROTOCOL_BLE + + fun setBleWay(isBle: Boolean) { + val way = if (isBle) OtaConstant.PROTOCOL_BLE else OtaConstant.PROTOCOL_SPP + preferences.edit().putInt(KEY_COMMUNICATION_WAY, way).apply() + } + + fun isUseDeviceAuth(): Boolean = + preferences.getBoolean(KEY_IS_USE_DEVICE_AUTH, OtaConstant.IS_NEED_DEVICE_AUTH) + + fun setUseDeviceAuth(isAuth: Boolean) { + preferences.edit().putBoolean(KEY_IS_USE_DEVICE_AUTH, isAuth).apply() + } + + fun isHidDevice(): Boolean = + preferences.getBoolean(KEY_IS_HID_DEVICE, OtaConstant.HID_DEVICE_WAY) + + fun setHidDevice(isHid: Boolean) { + preferences.edit().putBoolean(KEY_IS_HID_DEVICE, isHid).apply() + } + + fun isUseCustomReConnectWay(): Boolean = preferences.getBoolean( + KEY_USE_CUSTOM_RECONNECT_WAY, + OtaConstant.NEED_CUSTOM_RECONNECT_WAY + ) + + fun setUseCustomReConnectWay(isCustom: Boolean) { + preferences.edit().putBoolean(KEY_USE_CUSTOM_RECONNECT_WAY, isCustom).apply() + } + + fun getBleRequestMtu(): Int = + preferences.getInt(KEY_BLE_MTU_VALUE, 509) + + fun setBleRequestMtu(@IntRange(from = 20, to = 509) mtu: Int) { + preferences.edit().putInt(KEY_BLE_MTU_VALUE, mtu).apply() + } + + fun isUseMultiSppChannel(): Boolean = preferences.getBoolean( + KEY_SPP_MULTIPLE_CHANNEL, + OtaConstant.USE_SPP_MULTIPLE_CHANNEL + ) + + fun setUseMultiSppChannel(isUseMulti: Boolean) { + preferences.edit().putBoolean(KEY_SPP_MULTIPLE_CHANNEL, isUseMulti).apply() + } + + fun getCustomSppChannel(): String? = + preferences.getString(KEY_SPP_CUSTOM_UUID, OtaConstant.UUID_SPP.toString()) + + fun setCustomSppChannel(uuid: String?) { + preferences.edit().putString(KEY_SPP_CUSTOM_UUID, uuid).apply() + } + + fun isAutoTest(): Boolean = preferences.getBoolean(KEY_AUTO_TEST_OTA, OtaConstant.AUTO_TEST_OTA) + + fun setAutoTest(isAutoTest: Boolean) { + preferences.edit().putBoolean(KEY_AUTO_TEST_OTA, isAutoTest).apply() + } + + fun getAutoTestCount(): Int = preferences.getInt( + KEY_AUTO_TEST_COUNT, + OtaConstant.AUTO_TEST_COUNT + ) + + fun setAutoTestCount(count: Int) { + if (!isAutoTest()) return + preferences.edit().putInt(KEY_AUTO_TEST_COUNT, count).apply() + } + + fun isFaultTolerant(): Boolean = preferences.getBoolean(KEY_FAULT_TOLERANT, OtaConstant.AUTO_FAULT_TOLERANT) + + fun setFaultTolerant(isFaultTolerant: Boolean) { + preferences.edit().putBoolean(KEY_FAULT_TOLERANT, isFaultTolerant).apply() + } + + fun getFaultTolerantCount(): Int = preferences.getInt( + KEY_FAULT_TOLERANT_COUNT, + OtaConstant.AUTO_FAULT_TOLERANT_COUNT + ) + + fun setFaultTolerantCount(count: Int) { + if (!isFaultTolerant()) return + preferences.edit().putInt(KEY_FAULT_TOLERANT_COUNT, count).apply() + } + + fun getScanFilter(): String? = + preferences.getString(KEY_SCAN_FILTER_STRING, "") + + fun setScanFilter(scanFilter: String?) { + preferences.edit().putString(KEY_SCAN_FILTER_STRING, scanFilter).apply() + } + + fun isDevelopMode(): Boolean = + preferences.getBoolean(KEY_DEVELOP_MODE, false) + + fun setDevelopMode(developMode: Boolean) { + preferences.edit().putBoolean(KEY_DEVELOP_MODE, developMode).apply() + } + + fun isEnableBroadcastBox(): Boolean = + preferences.getBoolean(KEY_BROADCAST_BOX, false) + + fun enableBroadcastBox(enable: Boolean) { + preferences.edit().putBoolean(KEY_BROADCAST_BOX, enable).apply() + } +} \ No newline at end of file diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt new file mode 100644 index 000000000..10d86a089 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/OTAManager.kt @@ -0,0 +1,149 @@ +package com.jieli.otasdk.tool.ota + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.content.Context +import com.jieli.jl_bt_ota.constant.BluetoothConstant +import com.jieli.jl_bt_ota.constant.StateCode +import com.jieli.jl_bt_ota.impl.BluetoothOTAManager +import com.jieli.jl_bt_ota.impl.RcspAuth +import com.jieli.jl_bt_ota.model.BluetoothOTAConfigure +import com.jieli.jl_bt_ota.util.BluetoothUtil +import com.jieli.jl_bt_ota.util.CHexConver +import com.jieli.otasdk.tool.bluetooth.BluetoothHelper +import com.jieli.otasdk.tool.bluetooth.OnBTEventCallback +import com.jieli.otasdk.tool.config.ConfigHelper +import com.jieli.otasdk.tool.bluetooth.OtaConstant +import java.util.* +import android.util.Log +/** + * 用于RCSP的第三方SDK接入OTA流程 + */ +class OTAManager(context: Context) : BluetoothOTAManager(context) { + + val configHelper = ConfigHelper.getInstance() + val bluetoothHelper = BluetoothHelper.getInstance(context) + + val btEventCallback = object : OnBTEventCallback() { + + override fun onDeviceConnection(device: BluetoothDevice?, way: Int, status: Int) { + super.onDeviceConnection(device, way, status) + // val connectionState = AppUtil.changeConnectStatus(status) + // Log.i( + // TAG, "onConnection >>> device : ${printDeviceInfo(device)}, way = $way" + + // " status :$status, change status : $connectionState" + // ) + // onBtDeviceConnection(device, connectionState) + //注释掉 + Log.i( + TAG, "onConnection >>> device : ${device}, way = $way" + + " status :$status, change status : 3" + ) + onBtDeviceConnection(device, 1) + } + + override fun onReceiveData( + device: BluetoothDevice?, + way: Int, + uuid: UUID?, + data: ByteArray? + ) { + super.onReceiveData(device, way, uuid, data) + Log.d( + TAG, "onReceiveData >>> ${printDeviceInfo(device)}, way = $way,\n" + + "uuid = $uuid, data :${CHexConver.byte2HexStr(data)} " + ) + // if (way == OtaConstant.PROTOCOL_SPP && SppManager.UUID_SPP != uuid) { + // Log.d( + // TAG, "onReceiveData >>> skip spec" + // ) + // return + // } + onReceiveDeviceData(device, data) + } + + override fun onBleMtuChange(device: BluetoothDevice?, mtu: Int, status: Int) { + super.onBleMtuChange(device, mtu, status) + onMtuChanged(bluetoothHelper.getConnectedGatt(), mtu, status) + } + + } + + init { + // ConfigHelper.initialize(context) + val bluetoothOption = BluetoothOTAConfigure() + //选择通讯方式 + bluetoothOption.priority = + BluetoothOTAConfigure.PREFER_BLE + + //是否需要自定义回连方式(默认不需要,如需要自定义回连方式,需要客户自行实现) + bluetoothOption.isUseReconnect = + (configHelper.isUseCustomReConnectWay() && configHelper.isHidDevice()) + Log.d( + TAG, "是否启用设备认证流程(与固件工程师确认) >>> ${configHelper.isUseDeviceAuth()} >>> ${bluetoothOption.isUseReconnect} " + ) + //是否启用设备认证流程(与固件工程师确认) + bluetoothOption.isUseAuthDevice = configHelper.isUseDeviceAuth() + //设置BLE的MTU + bluetoothOption.mtu = BluetoothConstant.BLE_MTU_MIN + //是否需要改变BLE的MTU + bluetoothOption.isNeedChangeMtu = false + //是否启用杰理服务器(暂时不支持) + bluetoothOption.isUseJLServer = false + + //配置OTA参数 + configure(bluetoothOption) + RcspAuth.setAuthTimeout(5000) + bluetoothHelper.registerCallback(btEventCallback) + if (bluetoothHelper.isConnected()) { + onBtDeviceConnection(bluetoothHelper.getConnectedDevice(), StateCode.CONNECTION_OK) + if (configHelper.isBleWay()) { + onMtuChanged( + bluetoothHelper.getConnectedGatt(), + bluetoothHelper.getBleMtu() + 3, + BluetoothGatt.GATT_SUCCESS + ) + } + } + } + + + override fun getConnectedDevice(): BluetoothDevice? = bluetoothHelper.getConnectedDevice() + + override fun getConnectedBluetoothGatt(): BluetoothGatt? = bluetoothHelper.getConnectedGatt() + + override fun connectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { + //仅仅作为回连设备,回连设备现在仅支持BLE + val result: Boolean = bluetoothHelper.connectBleDevice(bluetoothDevice) + if (!result) { + onBtDeviceConnection(bluetoothDevice, StateCode.CONNECTION_FAILED) + } + } + + override fun disconnectBluetoothDevice(bluetoothDevice: BluetoothDevice?) { + bluetoothHelper.disconnectDevice(bluetoothDevice) + } + + override fun sendDataToDevice(bluetoothDevice: BluetoothDevice?, bytes: ByteArray?): Boolean { + Log.d( + TAG, + "sendDataToDevice : device = ${printDeviceInfo(bluetoothDevice)}\n" + + "data = [${CHexConver.byte2HexStr(bytes)}]" + ) + return bluetoothHelper.writeDataToDevice(bluetoothDevice, bytes) + } + + override fun release() { + super.release() + bluetoothHelper.unregisterCallback(btEventCallback) + } + + fun setReconnectAddr(addr: String?) { +// setReconnectAddress(addr) + } + + private fun printDeviceInfo(device: BluetoothDevice?): String? { + return BluetoothUtil.printBtDeviceInfo(context, device) + } + +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleEventCallbackManager.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleEventCallbackManager.java new file mode 100644 index 000000000..60cd91506 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleEventCallbackManager.java @@ -0,0 +1,175 @@ +package com.jieli.otasdk.tool.ota.ble; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGattService; +import android.os.Handler; +import android.os.Looper; + +import com.jieli.otasdk.tool.ota.ble.interfaces.BleEventCallback; +import com.jieli.otasdk.tool.ota.ble.model.BleScanInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Ble事件回调管理器 + * + * @author zqjasonZhong + * @since 2020/12/24 + */ +class BleEventCallbackManager extends BleEventCallback { + private final ArrayList mCallbacks = new ArrayList<>(); + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + public void registerBleEventCallback(BleEventCallback callback) { + if (callback != null && !mCallbacks.contains(callback)) { + mCallbacks.add(callback); + } + } + + public void unregisterBleEventCallback(BleEventCallback callback) { + if (callback != null && !mCallbacks.isEmpty()) { + mCallbacks.remove(callback); + } + } + + public void release() { + mCallbacks.clear(); + mHandler.removeCallbacksAndMessages(null); + } + + @Override + public void onAdapterChange(final boolean bEnabled) { + callbackBleEvent(new BleEventCallbackImpl() { + @Override + public void onCallback(BleEventCallback callback) { + callback.onAdapterChange(bEnabled); + } + }); + } + + @Override + public void onDiscoveryBleChange(final boolean bStart) { + callbackBleEvent(new BleEventCallbackImpl() { + @Override + public void onCallback(BleEventCallback callback) { + callback.onDiscoveryBleChange(bStart); + } + }); + } + + @Override + public void onDiscoveryBle(final BluetoothDevice device, final BleScanInfo bleScanMessage) { + callbackBleEvent(new BleEventCallbackImpl() { + @Override + public void onCallback(BleEventCallback callback) { + callback.onDiscoveryBle(device, bleScanMessage); + } + }); + } + + @Override + public void onBleConnection(final BluetoothDevice device, final int status) { + callbackBleEvent(new BleEventCallbackImpl() { + @Override + public void onCallback(BleEventCallback callback) { + callback.onBleConnection(device, status); + } + }); + } + + @Override + public void onBleServiceDiscovery(final BluetoothDevice device, final int status, final List services) { + callbackBleEvent(new BleEventCallbackImpl() { + @Override + public void onCallback(BleEventCallback callback) { + callback.onBleServiceDiscovery(device, status, services); + } + }); + } + + @Override + public void onBleNotificationStatus(final BluetoothDevice device, final UUID serviceUuid, final UUID characteristicUuid, final int status) { + callbackBleEvent(new BleEventCallbackImpl() { + @Override + public void onCallback(BleEventCallback callback) { + callback.onBleNotificationStatus(device, serviceUuid, characteristicUuid, status); + } + }); + } + + @Override + public void onBleDataBlockChanged(final BluetoothDevice device, final int block, final int status) { + callbackBleEvent(new BleEventCallbackImpl() { + @Override + public void onCallback(BleEventCallback callback) { + callback.onBleDataBlockChanged(device, block, status); + } + }); + } + + @Override + public void onBleDataNotification(final BluetoothDevice device, final UUID serviceUuid, final UUID characteristicsUuid, final byte[] data) { + callbackBleEvent(new BleEventCallbackImpl() { + @Override + public void onCallback(BleEventCallback callback) { + callback.onBleDataNotification(device, serviceUuid, characteristicsUuid, data); + } + }); + } + + @Override + public void onBleWriteStatus(final BluetoothDevice device, final UUID serviceUuid, final UUID characteristicsUuid, final byte[] data, final int status) { + callbackBleEvent(new BleEventCallbackImpl() { + @Override + public void onCallback(BleEventCallback callback) { + callback.onBleWriteStatus(device, serviceUuid, characteristicsUuid, data, status); + } + }); + } + + @Override + public void onConnectionUpdated(final BluetoothDevice device, final int interval, final int latency, final int timeout, final int status) { + callbackBleEvent(new BleEventCallbackImpl() { + @Override + public void onCallback(BleEventCallback callback) { + callback.onConnectionUpdated(device, interval, latency, timeout, status); + } + }); + } + + private void callbackBleEvent(BleEventCallbackImpl impl) { + if (null == impl) return; + OnBleEventRunnable runnable = new OnBleEventRunnable(impl); + if (Thread.currentThread().getId() == Looper.getMainLooper().getThread().getId()) { + runnable.run(); + } else { + mHandler.post(runnable); + } + } + + private static abstract class BleEventCallbackImpl { + + public abstract void onCallback(BleEventCallback callback); + } + + private class OnBleEventRunnable implements Runnable { + private final BleEventCallbackImpl mImpl; + + public OnBleEventRunnable(BleEventCallbackImpl impl) { + mImpl = impl; + } + + @Override + public void run() { + if (!mCallbacks.isEmpty() && mImpl != null) { + for (BleEventCallback callback : new ArrayList<>(mCallbacks)) { + if (callback != null) { + mImpl.onCallback(callback); + } + } + } + } + } +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java new file mode 100644 index 000000000..feec079de --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/BleManager.java @@ -0,0 +1,1194 @@ +package com.jieli.otasdk.tool.ota.ble; + +import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; +import static android.os.Build.VERSION_CODES.LOLLIPOP; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +//import com.jieli.broadcastbox.multidevice.ReConnectHelper; +import com.jieli.jl_bt_ota.constant.BluetoothConstant; +import com.jieli.jl_bt_ota.util.BluetoothUtil; +import com.jieli.jl_bt_ota.util.CHexConver; +import com.jieli.jl_bt_ota.util.CommonUtil; +//import com.jieli.jl_bt_ota.util.Log; +//import com.jieli.otasdk.MainApplication; +import com.jieli.otasdk.tool.config.ConfigHelper; +import com.jieli.otasdk.tool.ota.ble.interfaces.BleEventCallback; +import com.jieli.otasdk.tool.ota.ble.interfaces.OnWriteDataCallback; +import com.jieli.otasdk.tool.ota.ble.model.BleDevice; +import com.jieli.otasdk.tool.ota.ble.model.BleScanInfo; +//import com.jieli.otasdk.util.AppUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import android.util.Log; + + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.Manifest; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.provider.Settings; +/** + * Ble连接管理类 + * + * @author zqjasonZhong + * @since 2020/7/16 + */ +public class BleManager { + private final static String TAG = BleManager.class.getSimpleName(); + private final Context mContext; + @SuppressLint("StaticFieldLeak") + private volatile static BleManager instance; + private final ConfigHelper configHelper = ConfigHelper.Companion.getInstance(); + + private BaseBtAdapterReceiver mAdapterReceiver; + private final BluetoothAdapter mBluetoothAdapter; + private BluetoothLeScanner mBluetoothLeScanner; + // private final ReConnectHelper mReConnectHelper; + + private volatile BluetoothDevice mConnectingBtDevice; //连接中的设备 + private volatile BluetoothDevice mUsingDevice; //正在通讯的设备 + + private final Map mConnectedGattMap = new HashMap<>(); + private final List mDiscoveredBleDevices = new ArrayList<>(); + private final BleEventCallbackManager mCallbackManager = new BleEventCallbackManager(); + + private volatile boolean isBleScanning; + private NotifyCharacteristicRunnable mNotifyCharacteristicRunnable; + + private BluetoothDevice deviced;//需要连接手机设备信息 + private long timeout = 15000;//扫描设备时间 + private int mRetryConnectCount = 0;//连接失败后尝试连接次数 + private long startConnectTime = 0; //开始连接时间戳 + private final static int MAX_RETRY_CONNECT_COUNT = 1;//最大尝试连接次数 + private final static int MIN_CONNECT_TIME = 5 * 1000; //连接最小时间超时 + //BLE服务UUID + public final static UUID BLE_UUID_SERVICE = BluetoothConstant.UUID_SERVICE; + //BLE的写特征UUID + public final static UUID BLE_UUID_WRITE = BluetoothConstant.UUID_WRITE; + //BLE的通知特征UUID + public final static UUID BLE_UUID_NOTIFICATION = BluetoothConstant.UUID_NOTIFICATION; + //BLE的通知特征的描述符UUID + public final static UUID BLE_UUID_NOTIFICATION_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"); + + /** + * 发送数据最大超时 - 8 秒 + */ + public final static int SEND_DATA_MAX_TIMEOUT = 8000; //8 s + private final static int SCAN_BLE_TIMEOUT = 12 * 1000; //建议搜索BLE最小时间 + private final static int CONNECT_BLE_TIMEOUT = 40 * 1000; + + private final static int CALLBACK_TIMEOUT = 6000; + private final static int RECONNECT_BLE_DELAY = 2000; + + private final static int MSG_SCAN_BLE_TIMEOUT = 0x1010; + private final static int MSG_CONNECT_BLE_TIMEOUT = 0x1011; + private final static int MSG_SCAN_HID_DEVICE = 0X1012; + private final static int MSG_NOTIFY_BLE_TIMEOUT = 0x1013; + private final static int MSG_CHANGE_BLE_MTU_TIMEOUT = 0x1014; + private final static int MSG_BLE_DISCOVER_SERVICES_CALLBACK_TIMEOUT = 0x1015; + private final Handler mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { + @Override + public boolean handleMessage(@NonNull Message msg) { + switch (msg.what) { + case MSG_SCAN_BLE_TIMEOUT: + if (isBleScanning) { + stopLeScan(); + } + break; + case MSG_CONNECT_BLE_TIMEOUT: { + if (msg.obj instanceof BluetoothDevice) { + BluetoothDevice device = (BluetoothDevice) msg.obj; + BleDevice bleDevice = getConnectedBle(device); + if (null == bleDevice) { + handleBleConnection(device, BluetoothProfile.STATE_DISCONNECTED); + } + setConnectingBtDevice(null); + } + break; + } + case MSG_SCAN_HID_DEVICE: { + List lists = BluetoothUtil.getSystemConnectedBtDeviceList(mContext); + if (null != lists ) { + //&& AppUtil.checkHasConnectPermission(mContext) + for (BluetoothDevice device : lists) { + if (device.getType() != BluetoothDevice.DEVICE_TYPE_CLASSIC && + device.getBondState() == BluetoothDevice.BOND_BONDED) { + handleDiscoveryBle(device, null); + } + } + } + mHandler.sendEmptyMessageDelayed(MSG_SCAN_HID_DEVICE, 1000); + break; + } + case MSG_NOTIFY_BLE_TIMEOUT: { + if (msg.obj instanceof BluetoothDevice) { + BluetoothDevice device = (BluetoothDevice) msg.obj; + disconnectBleDevice(device); + } + break; + } + case MSG_CHANGE_BLE_MTU_TIMEOUT: { + BluetoothDevice device = (BluetoothDevice) msg.obj; + BleDevice bleDevice = getConnectedBle(device); + Log.i(TAG, "-MSG_CHANGE_BLE_MTU_TIMEOUT- request mtu timeout, device : " + printDeviceInfo(device) + ", " + bleDevice); + if (bleDevice != null) { + handleBleConnectedEvent(device); + } else { + handleBleConnection(device, BluetoothProfile.STATE_DISCONNECTED); + } + break; + } + case MSG_BLE_DISCOVER_SERVICES_CALLBACK_TIMEOUT: + if (msg.obj instanceof BluetoothDevice) { + BluetoothDevice connectedBleDev = (BluetoothDevice) msg.obj; + if (BluetoothUtil.deviceEquals(connectedBleDev, mUsingDevice)) { + boolean isNeedDisconnect = true; + BleDevice bleDevice = getConnectedBle(connectedBleDev); + if (bleDevice != null) { + List services = bleDevice.getGatt().getServices(); + if (services != null && services.size() > 0) { + mBluetoothGattCallback.onServicesDiscovered(bleDevice.getGatt(), BluetoothGatt.GATT_SUCCESS); + isNeedDisconnect = false; + } + } + if (isNeedDisconnect) { + Log.d(TAG, "discover services timeout."); + disconnectBleDevice(connectedBleDev); + reconnectDevice(connectedBleDev.getAddress(), false); + } + } + } + break; + } + return false; + } + }); + + + private BleManager(Context context) { + mContext = CommonUtil.checkNotNull(context); + if (CommonUtil.getMainContext() == null) { + CommonUtil.setMainContext(context); + } + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (Build.VERSION.SDK_INT >= LOLLIPOP && mBluetoothAdapter != null) { + mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); + } + //mReConnectHelper = new ReConnectHelper(context, this); + registerReceiver(); + } + + public static BleManager getInstance(Context context) { + if (instance == null) { + synchronized (BleManager.class) { + if (instance == null) { + instance = new BleManager(context.getApplicationContext()); + Log.w(TAG, "init BleManager.. " + instance); + } + } + } + return instance; + } + /** + * 获取已连接的BLE设备列表 + * + * @param context 上下文 + * @return 已连接的BLE设备列表 + */ + @SuppressLint("MissingPermission") + public static List getConnectedBleDeviceList(Context context) { + if (context == null ) return null; + //|| !AppUtil.checkHasConnectPermission(context) + BluetoothManager mBluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + if (mBluetoothManager != null) { + return mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT); + } + return null; + } + + public void destroy() { + Log.w(TAG, ">>>>>>>>>>>>>>destroy >>>>>>>>>>>>>>> "); + unregisterReceiver(); + stopConnectTimeout(); + clearConnectedBleDevices(); + + if (isBleScanning()) stopLeScan(); + isBleScanning(false); + mDiscoveredBleDevices.clear(); + //mReConnectHelper.release(); + + mCallbackManager.release(); + mHandler.removeCallbacksAndMessages(null); + instance = null; + } + + public void registerBleEventCallback(BleEventCallback callback) { + mCallbackManager.registerBleEventCallback(callback); + } + + public void unregisterBleEventCallback(BleEventCallback callback) { + mCallbackManager.unregisterBleEventCallback(callback); + } + + public boolean isBluetoothEnable() { + return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled(); + } + + public boolean isBleScanning() { + return isBleScanning; + } + public void checkPermissionsAndRequest(Context context) { + if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) { + // 蓝牙未开启,弹窗提示用户开启蓝牙 + new AlertDialog.Builder(context) + .setTitle("蓝牙未开启") + .setMessage("请开启蓝牙以继续使用此功能。") + .setPositiveButton("开启蓝牙", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + context.startActivity(intent); + } + }) + .setNegativeButton("取消", null) + .show(); + } else if (!isLocationPermissionGranted(context)) { + // 定位权限未授权,弹窗提示用户授权定位权限 + new AlertDialog.Builder(context) + .setTitle("定位权限未授权") + .setMessage("请授权定位权限以继续使用此功能。") + .setPositiveButton("授权定位权限", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + context.getPackageName())); + context.startActivity(intent); + } + }) + .setNegativeButton("取消", null) + .show(); + } + } + + /** + * 检查定位权限是否已授权 + */ + private boolean isLocationPermissionGranted(Context context) { + return context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED; + } + @SuppressLint("MissingPermission") + public boolean startLeScan(BluetoothDevice deviced) { + Log.i(TAG, ", timeout = " + timeout); + if (!isBluetoothEnable() || !isLocationPermissionGranted(mContext)) { + checkPermissionsAndRequest(mContext); + return false; + } + if (null == mBluetoothAdapter + // || !AppUtil.checkHasScanPermission(mContext) + ) return false; + Log.i(TAG, ", timeout1 = " + timeout); + if (!isBluetoothEnable() + // || !AppUtil.isHasLocationPermission(mContext) + ) return false; + Log.i(TAG, ", timeout2 = " + timeout); + if (timeout <= 0) timeout = SCAN_BLE_TIMEOUT; + if (isBleScanning) { + Log.i(TAG, "scanning ble ....."); + if (mBluetoothLeScanner != null) { + mBluetoothLeScanner.flushPendingScanResults(mScanCallback); + } + mDiscoveredBleDevices.clear(); + mHandler.removeMessages(MSG_SCAN_BLE_TIMEOUT); + mHandler.sendEmptyMessageDelayed(MSG_SCAN_BLE_TIMEOUT, timeout); + syncSystemBleDevice(); + return true; + } + boolean ret; + if (Build.VERSION.SDK_INT >= LOLLIPOP && mBluetoothLeScanner != null) { + ScanSettings scanSettings; + int scanMode = ScanSettings.SCAN_MODE_BALANCED; //修改搜索BLE模式 -- 均衡模式 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + scanSettings = new ScanSettings.Builder() + .setScanMode(scanMode) + .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + .build(); + } else { + scanSettings = new ScanSettings.Builder() + .setScanMode(scanMode) + .build(); + } + mBluetoothLeScanner.startScan(null, scanSettings, mScanCallback); + ret = true; + } else { + ret = mBluetoothAdapter.startLeScan(mLeScanCallback); + } + Log.i(TAG, "startLeScan : " + ret + ", timeout = " + timeout); + isBleScanning(ret); + if (ret) { + mDiscoveredBleDevices.clear(); + mHandler.removeMessages(MSG_SCAN_BLE_TIMEOUT); + mHandler.sendEmptyMessageDelayed(MSG_SCAN_BLE_TIMEOUT, timeout); + syncSystemBleDevice(); + } + return ret; + } + + @SuppressLint("MissingPermission") + public void stopLeScan() { + if (null == mBluetoothAdapter || !isBluetoothEnable() + // || !AppUtil.checkHasScanPermission(mContext) + ) + return; + if (!isBleScanning()) return; + try { + if (Build.VERSION.SDK_INT >= LOLLIPOP && mBluetoothLeScanner != null) { + mBluetoothLeScanner.stopScan(mScanCallback); + } else { + mBluetoothAdapter.stopLeScan(mLeScanCallback); + } + } catch (Exception e) { + e.printStackTrace(); + } + mHandler.removeMessages(MSG_SCAN_BLE_TIMEOUT); + mHandler.removeMessages(MSG_SCAN_HID_DEVICE); + isBleScanning(false); + } + + public BluetoothDevice getConnectedBtDevice() { + return mUsingDevice; + } + + public BluetoothGatt getConnectedBtGatt(BluetoothDevice device) { + BleDevice bleDevice = getConnectedBle(device); + if (null == bleDevice) return null; + return bleDevice.getGatt(); + } + + public BluetoothDevice getConnectedBLEDevice(String address) { + if (!BluetoothAdapter.checkBluetoothAddress(address)) return null; + List devices = getConnectedDeviceList(); + if (devices.isEmpty()) return null; + for (BluetoothDevice device : devices) { + if (device.getAddress().equals(address)) { + return device; + } + } + return null; + } + + /** + * 获取已连接设备列表 + *

+ * 按照连接时间倒序 + *

+ * + * @return 设备列表 + */ + public List getConnectedDeviceList() { + if (mConnectedGattMap.isEmpty()) return new ArrayList<>(); + List bleDevices = getSortList(); + List devices = new ArrayList<>(); + for (BleDevice bleDevice : bleDevices) { + if (null == bleDevice || null == bleDevice.getGatt().getDevice()) continue; + devices.add(bleDevice.getGatt().getDevice()); + } + return devices; + } + + public void reconnectDevice(String address, boolean isUseAdv) { + Log.d(TAG, "reconnectDevice : address = " + address + ", isUseAdv = " + isUseAdv); + //boolean ret = mReConnectHelper.putParam(new ReConnectHelper.ReconnectParam(address, isUseAdv)); + //注释掉的 + // Log.d(TAG, "reconnectDevice : ret = " + ret); + } + + // public boolean isMatchReConnectDevice(String address, String matchAddress) { + // return mReConnectHelper.isMatchAddress(address, matchAddress); + // } + + public int getBleMtu(BluetoothDevice device) { + BleDevice bleDevice = getConnectedBle(device); + if (null == bleDevice) return 0; + return bleDevice.getMtu(); + } + + public boolean isConnecting() { + return mConnectingBtDevice != null; + } + + public boolean isConnectingDevice(BluetoothDevice device) { + return BluetoothUtil.deviceEquals(mConnectingBtDevice, device); + } + + public boolean isConnectedDevice(BluetoothDevice device) { + if (null == device) return false; + return isConnectedDevice(device.getAddress()); + } + + public boolean isConnectedDevice(String address) { + if (!BluetoothAdapter.checkBluetoothAddress(address)) return false; + List devices = getConnectedDeviceList(); + if (devices.isEmpty()) return false; + for (BluetoothDevice device : devices) { + if (device.getAddress().equals(address)) { + return true; + } + } + return false; + } + + @SuppressLint("MissingPermission") + public boolean connectBleDevice(BluetoothDevice device) { + if (null == device + // || !AppUtil.checkHasConnectPermission(mContext) + ) return false; + /* if (mUsingDevice != null) { + Log.e(TAG, "BleDevice is connected, please call disconnectBleDevice method at first."); + setReconnectDevAddr(null); + return false; + }*/ + if (mConnectingBtDevice != null) { + Log.e(TAG, "BleDevice is connecting, please wait."); + return isConnectingDevice(device); + } + if (isBleScanning()) { + stopLeScan(); + } + BluetoothGatt gatt = null; + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + gatt = device.connectGatt( + mContext, + false, + mBluetoothGattCallback, + BluetoothDevice.TRANSPORT_LE + ); + } else { + gatt = device.connectGatt( + mContext, + false, + mBluetoothGattCallback + ); + } + } catch (Exception e) { + e.printStackTrace(); + } + boolean ret = gatt != null; + if (ret) { +// putConnectedGattInMap(device.getAddress(), gatt); + setConnectingBtDevice(device); + handleBleConnection(device, BluetoothProfile.STATE_CONNECTING); + startConnectTimeout(device); + Log.d(TAG, "connect start...." + printDeviceInfo(device)); + } + return ret; + } + + @SuppressLint("MissingPermission") + public void disconnectBleDevice(BluetoothDevice device) { + if (null == device + // || !AppUtil.checkHasConnectPermission(mContext) + ) return; + BleDevice bleDevice = removeConnectedBle(device); + Log.i(TAG, "disconnectBleDevice : " + printDeviceInfo(device) + ", " + bleDevice); + if (bleDevice != null) { + if (BluetoothUtil.isBluetoothEnable()) { + bleDevice.getGatt().disconnect(); +// bleDevice.getGatt().close(); + } + } else { + Log.i(TAG, "disconnectBleDevice : It is not a connected device."); + } + } + + public void writeDataByBleAsync(BluetoothDevice device, UUID serviceUUID, UUID characteristicUUID, byte[] data, OnWriteDataCallback callback) { + addSendTask(device, serviceUUID, characteristicUUID, data, callback); + } + + private void isBleScanning(boolean isScanning) { + isBleScanning = isScanning; + mCallbackManager.onDiscoveryBleChange(isScanning); + if (isBleScanning && configHelper.isHidDevice()) { + mHandler.sendEmptyMessage(MSG_SCAN_HID_DEVICE); + } + } + + private BleDevice getConnectedBle(BluetoothDevice device) { + if (null == device) return null; + return mConnectedGattMap.get(device.getAddress()); + } + + private void putConnectedGattInMap(String address, BluetoothGatt gatt) { + if (!BluetoothAdapter.checkBluetoothAddress(address) || null == gatt) return; + BleDevice bleDevice = new BleDevice(mContext, gatt); + bleDevice.setConnectedTime(System.currentTimeMillis()); + mConnectedGattMap.put(address, bleDevice); + if (mUsingDevice == null) { + mUsingDevice = gatt.getDevice(); + } + + Log.i(TAG, "putConnectedGattInMap >>>>>>>>>>>>> start"); + for (String addr : mConnectedGattMap.keySet()) { + Log.d(TAG, "putConnectedGattInMap >>>>>>>>>>>>> " + addr); + } + Log.i(TAG, "putConnectedGattInMap >>>>>>>>>>>>> end"); + } + + private BleDevice removeConnectedBle(BluetoothDevice device) { + if (null == device) return null; + return removeConnectedBle(device.getAddress()); + } + + private BleDevice removeConnectedBle(String address) { + if (!BluetoothAdapter.checkBluetoothAddress(address)) return null; + BleDevice bleDevice = mConnectedGattMap.remove(address); + if (null != bleDevice) { + bleDevice.stopSendDataThread(); + if (mConnectedGattMap.isEmpty()) { + setConnectedBtDevice(null); + } else if (bleDevice.getGatt().getDevice() != null && BluetoothUtil.deviceEquals(bleDevice.getGatt().getDevice(), + getConnectedBtDevice())) { + List values = getSortList(); + setConnectedBtDevice(values.get(0).getGatt().getDevice()); + } + } + return bleDevice; + } + + @NonNull + private List getSortList() { + if (mConnectedGattMap.isEmpty()) return new ArrayList<>(); + List bleDevices = new ArrayList<>(mConnectedGattMap.values()); + Collections.sort(bleDevices, (o1, o2) -> { + if (null == o1 && null == o2) return 0; + if (null == o1) return 1; + if (null == o2) return -1; + return Long.compare(o2.getConnectedTime(), o1.getConnectedTime()); + }); + return bleDevices; + } + + private void clearConnectedBleDevices() { + // if (!AppUtil.checkHasConnectPermission(mContext)) return; + if (!mConnectedGattMap.isEmpty()) { + Map clone = new HashMap<>(mConnectedGattMap); + for (String key : clone.keySet()) { + BleDevice bleDevice = clone.get(key); + if (null == bleDevice) continue; + bleDevice.getGatt().disconnect(); + bleDevice.getGatt().close(); + } + mConnectedGattMap.clear(); + } + } + + private void setConnectingBtDevice(BluetoothDevice mConnectingBtDevice) { + this.mConnectingBtDevice = mConnectingBtDevice; + } + + private void setConnectedBtDevice(BluetoothDevice mConnectedBtDevice) { + this.mUsingDevice = mConnectedBtDevice; + } + + @SuppressLint("MissingPermission") + private void filterDevice(BluetoothDevice device, int rssi, byte[] scanRecord, + boolean isBleEnableConnect) { + if ( + // AppUtil.checkHasConnectPermission(mContext) && + isBluetoothEnable() && !TextUtils.isEmpty(device.getName()) + && !mDiscoveredBleDevices.contains(device)) { + Log.d(TAG, "notify device : " + printDeviceInfo(device)); + Log.d(TAG, "device.getAddress : " + device.getAddress()); + if ("4D:9B:09:38:22:F5".equals(device.getAddress())) { + // 匹配成功 + + Log.i(TAG, "filterDevice找到了DeapSound设备"); + + // 直接连接设备 + boolean connected = connectBleDevice(device); + if (connected) { + Log.i(TAG, "成功开始连接设备: ${device.name} (${device.address})"); + } else { + Log.e(TAG, "连接设备失败: ${device.name} (${device.address})"); + } + } + mDiscoveredBleDevices.add(device); + handleDiscoveryBle(device, new BleScanInfo().setRawData(scanRecord).setRssi(rssi).setEnableConnect(isBleEnableConnect)); + } + } + + private void startConnectTimeout(BluetoothDevice device) { + if (!mHandler.hasMessages(MSG_CONNECT_BLE_TIMEOUT)) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CONNECT_BLE_TIMEOUT, device), CONNECT_BLE_TIMEOUT); + } + } + + private void stopConnectTimeout() { + if (mHandler.hasMessages(MSG_CONNECT_BLE_TIMEOUT)) { + mHandler.removeMessages(MSG_CONNECT_BLE_TIMEOUT); + } + } + + private void syncSystemBleDevice() { + List mSysConnectedBleList = getConnectedBleDeviceList(mContext); + if (mSysConnectedBleList != null && !mSysConnectedBleList.isEmpty()) { + for (BluetoothDevice bleDev : mSysConnectedBleList) { + if (!BluetoothUtil.deviceEquals(bleDev, mUsingDevice)) { + if (!mDiscoveredBleDevices.contains(bleDev)) { + mDiscoveredBleDevices.add(bleDev); + handleDiscoveryBle(bleDev, new BleScanInfo().setEnableConnect(true)); + } + } + } + } + } + + private void addSendTask(BluetoothDevice device, UUID serviceUUID, UUID characteristicUUID, + byte[] data, OnWriteDataCallback callback) { + boolean ret = false; + BleDevice bleDevice = getConnectedBle(device); + if (bleDevice != null) { + ret = bleDevice.addSendTask(serviceUUID, characteristicUUID, data, callback); + } + if (!ret && callback != null) { + callback.onBleResult(device, serviceUUID, characteristicUUID, false, data); + } + } + + private void wakeupSendThread(BluetoothGatt gatt, UUID serviceUUID, UUID characteristicUUID, + int status, byte[] data) { + final BleDevice bleDevice = getConnectedBle(gatt.getDevice()); + if (bleDevice != null) { + SendBleDataThread.BleSendTask task = new SendBleDataThread.BleSendTask(gatt, serviceUUID, characteristicUUID, data, null); + task.setStatus(status); + bleDevice.wakeupSendThread(task); + } + } + + private void handleDiscoveryBle(BluetoothDevice device, BleScanInfo bleScanInfo) { + mCallbackManager.onDiscoveryBle(device, bleScanInfo); + } + + private void handleBleConnection(final BluetoothDevice device, final int status) { + if (status == BluetoothProfile.STATE_DISCONNECTED || status == STATE_CONNECTED) { + mHandler.removeMessages(MSG_NOTIFY_BLE_TIMEOUT); + startConnectTime = 0; + } else if (status == BluetoothProfile.STATE_CONNECTING) { + startConnectTime = System.currentTimeMillis(); + } + Log.i(TAG, "handleBleConnection >> device : " + printDeviceInfo(device) + ", status : " + status); + mCallbackManager.onBleConnection(device, status); + } + + /* ---- BroadcastReceiver Handler ---- */ + private void registerReceiver() { + if (mAdapterReceiver == null) { + mAdapterReceiver = new BaseBtAdapterReceiver(); + IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); + intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); + intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + mContext.registerReceiver(mAdapterReceiver, intentFilter); + } + } + + private void unregisterReceiver() { + if (mAdapterReceiver != null) { + mContext.unregisterReceiver(mAdapterReceiver); + mAdapterReceiver = null; + } + } + + /** + * 用于开启蓝牙BLE设备Notification服务 + * + * @param gatt 被连接的ble Gatt服务对象 + * @param serviceUUID 服务UUID + * @param characteristicUUID characteristic UUID + * @return 结果 true 则等待系统回调BLE服务 + */ + @SuppressLint("MissingPermission") + private boolean enableBLEDeviceNotification(BluetoothGatt gatt, UUID serviceUUID, UUID + characteristicUUID) { + if (null == gatt + // || !AppUtil.checkHasConnectPermission(mContext) + ) { + Log.w(TAG, "Bluetooth gatt is null."); + return false; + } + BluetoothGattService gattService = gatt.getService(serviceUUID); + if (null == gattService) { + Log.w(TAG, "BluetoothGattService is null. uuid = " + serviceUUID); + return false; + } + BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(characteristicUUID); + if (null == characteristic) { + Log.w(TAG, "BluetoothGattCharacteristic is null. uuid = " + characteristicUUID); + return false; + } + boolean bRet = gatt.setCharacteristicNotification(characteristic, true); + if (bRet) { + bRet = false; //重置标识 + List descriptors = characteristic.getDescriptors(); + for (BluetoothGattDescriptor descriptor : descriptors) { + if (!BLE_UUID_NOTIFICATION_DESCRIPTOR.equals(descriptor.getUuid())) + continue; //跳过不相关描述符 + bRet = tryToWriteDescriptor(gatt, descriptor, 0, false); + if (!bRet) { + Log.w(TAG, "tryToWriteDescriptor failed...."); + } else { //正常只有一个描述符,使能即可 + break; + } + } + } else { + Log.w(TAG, "setCharacteristicNotification is failed...."); + } + Log.w(TAG, "enableBLEDeviceNotification ret : " + bRet + ", serviceUUID : " + serviceUUID + ", characteristicUUID : " + characteristicUUID); + return bRet; + } + + @SuppressLint("MissingPermission") + private boolean tryToWriteDescriptor(BluetoothGatt bluetoothGatt, BluetoothGattDescriptor + descriptor, int retryCount, boolean isSkipSetValue) { + // if (!AppUtil.checkHasConnectPermission(mContext)) return false; + boolean ret = isSkipSetValue; + if (!ret) { + ret = descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + Log.i(TAG, "..descriptor : .setValue ret : " + ret); + if (!ret) { + retryCount++; + if (retryCount >= 3) { + return false; + } else { + Log.i(TAG, "-tryToWriteDescriptor- : retryCount : " + retryCount + ", isSkipSetValue : false"); + SystemClock.sleep(50); + tryToWriteDescriptor(bluetoothGatt, descriptor, retryCount, false); + } + } else { + retryCount = 0; + } + } + if (ret) { + ret = bluetoothGatt.writeDescriptor(descriptor); + Log.i(TAG, "..bluetoothGatt : .writeDescriptor ret : " + ret); + if (!ret) { + retryCount++; + if (retryCount >= 3) { + return false; + } else { + Log.i(TAG, "-tryToWriteDescriptor- 2222 : retryCount : " + retryCount + ", isSkipSetValue : true"); + SystemClock.sleep(50); + tryToWriteDescriptor(bluetoothGatt, descriptor, retryCount, true); + } + } + } + return ret; + } + + + //开始调整BLE协议MTU + @SuppressLint("MissingPermission") + private void startChangeMtu(BluetoothGatt gatt, int mtu) { + if (gatt == null + // || !AppUtil.checkHasConnectPermission(mContext) + ) { + Log.w(TAG, "-startChangeMtu- param is error."); + return; + } + BluetoothDevice device = gatt.getDevice(); + if (device == null) { + Log.w(TAG, "-startChangeMtu- device is null."); + return; + } + if (mHandler.hasMessages(MSG_CHANGE_BLE_MTU_TIMEOUT)) { + Log.w(TAG, "-startChangeMtu- Adjusting the MTU for BLE"); + return; + } + boolean ret = false; + if (mtu > BluetoothConstant.BLE_MTU_MIN) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ret = gatt.requestMtu(mtu + 3); + } else { + ret = true; + } + } + Log.d(TAG, "-startChangeMtu- ret = " + ret); + if (ret) { //调整成功,开始超时任务 + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHANGE_BLE_MTU_TIMEOUT, device), CALLBACK_TIMEOUT); + } else { + handleBleConnectedEvent(device); + } + } + + //回收调整MTU的超时任务 + private void stopChangeMtu() { + mHandler.removeMessages(MSG_CHANGE_BLE_MTU_TIMEOUT); + } + + private void handleBleConnectedEvent(final BluetoothDevice device) { + if (device == null) { + Log.e(TAG, "-handleBleConnectedEvent- device is null."); + return; + } + stopChangeMtu(); + BleDevice bleDevice = getConnectedBle(device); + bleDevice.startSendDataThread(); + handleBleConnection(device, STATE_CONNECTED); + } + + private String printDeviceInfo(BluetoothDevice device) { + return BluetoothUtil.printBtDeviceInfo(mContext, device); + } + + private final BluetoothAdapter.LeScanCallback mLeScanCallback = (device, rssi, scanRecord) -> filterDevice(device, rssi, scanRecord, true); + + private final ScanCallback mScanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + if (result != null && result.getScanRecord() != null) { + BluetoothDevice device = result.getDevice(); + boolean isBleEnableConnect = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + isBleEnableConnect = result.isConnectable(); + } +// Log.i("onScanResult", printDeviceInfo(device)); + filterDevice(device, result.getRssi(), result.getScanRecord().getBytes(), isBleEnableConnect); + } + } + + @Override + public void onBatchScanResults(List results) { + + } + + @Override + public void onScanFailed(int errorCode) { + Log.d(TAG, "onScanFailed : " + errorCode); + stopLeScan(); + } + }; + + private final BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() { + + public void onConnectionUpdated(BluetoothGatt gatt, int interval, int latency, int timeout, int status) { + if (null == gatt + // || !AppUtil.checkHasConnectPermission(mContext) + ) return; + BluetoothDevice device = gatt.getDevice(); + if (null == device) return; + Log.e(TAG, "onConnectionUpdated >> device : " + printDeviceInfo(device) + ", interval : " + + interval + ", latency : " + latency + ", timeout : " + timeout + ", status : " + status); + mCallbackManager.onConnectionUpdated(device, interval, latency, timeout, status); + } + + @SuppressLint("MissingPermission") + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + if (null == gatt + // || !AppUtil.checkHasConnectPermission(mContext) + ) return; + final BluetoothDevice device = gatt.getDevice(); + if (null == device) return; + Log.i(TAG, String.format(Locale.getDefault(), "onConnectionStateChange : device : %s, status = %d, newState = %d.", + printDeviceInfo(device), status, newState)); + if (newState == BluetoothProfile.STATE_DISCONNECTED || newState == BluetoothProfile.STATE_DISCONNECTING + || newState == STATE_CONNECTED) { + stopConnectTimeout(); + setConnectingBtDevice(null); + if (newState == STATE_CONNECTED) { //BLE连接成功 + mRetryConnectCount = 0; + boolean ret = gatt.discoverServices(); + Log.d(TAG, "onConnectionStateChange >> discoverServices : " + ret); + putConnectedGattInMap(device.getAddress(), gatt); +// setConnectedBtDevice(device); + if (ret) { + mHandler.removeMessages(MSG_BLE_DISCOVER_SERVICES_CALLBACK_TIMEOUT); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_BLE_DISCOVER_SERVICES_CALLBACK_TIMEOUT, device), CALLBACK_TIMEOUT); + } else { + disconnectBleDevice(device); + } + return; + } else { + removeConnectedBle(device); + // AppUtil.refreshBleDeviceCache(mContext, gatt); //强制更新缓存 + gatt.close(); + + long usedConnectTime = System.currentTimeMillis() - startConnectTime; //连接已耗费时间 + Log.d(TAG, "onConnectionStateChange >> usedConnectTime = " + usedConnectTime + ", limit time = " + MIN_CONNECT_TIME); + if (status == 133 && usedConnectTime < MIN_CONNECT_TIME) { //Todo: 遇到了异常断开情况, 尝试重连设备 + if (mRetryConnectCount < MAX_RETRY_CONNECT_COUNT) { + mRetryConnectCount++; + connectBleDevice(device); + return; + } else { + mRetryConnectCount = 0; + } + } + } + } + handleBleConnection(device, newState); + } + + @SuppressLint("MissingPermission") + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + if (null == gatt + // || !AppUtil.checkHasConnectPermission(mContext) + ) return; + BluetoothDevice device = gatt.getDevice(); + if (null == device) return; + mHandler.removeMessages(MSG_BLE_DISCOVER_SERVICES_CALLBACK_TIMEOUT); + mCallbackManager.onBleServiceDiscovery(device, status, gatt.getServices()); + boolean ret = false; + if (status == BluetoothGatt.GATT_SUCCESS) { + // AppUtil.printBleGattServices(mContext, device, gatt, status); + for (BluetoothGattService service : gatt.getServices()) { + if (BLE_UUID_SERVICE.equals(service.getUuid()) && null != service.getCharacteristic(BLE_UUID_WRITE) + && null != service.getCharacteristic(BLE_UUID_NOTIFICATION)) { + Log.i(TAG, "start NotifyCharacteristicRunnable..."); + mNotifyCharacteristicRunnable = new NotifyCharacteristicRunnable(gatt, BLE_UUID_SERVICE, BLE_UUID_NOTIFICATION); + mHandler.post(mNotifyCharacteristicRunnable); + ret = true; + break; + } + } + } + Log.i(TAG, "onServicesDiscovered : " + ret); + if (!ret) { + disconnectBleDevice(device); + } + } + + @SuppressLint("MissingPermission") + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (null == gatt + // || !AppUtil.checkHasConnectPermission(mContext) + ) return; + BluetoothDevice device = gatt.getDevice(); + if (null == device || null == characteristic) return; + UUID serviceUUID = null; + UUID characteristicUUID = characteristic.getUuid(); + byte[] data = characteristic.getValue(); + BluetoothGattService gattService = characteristic.getService(); + if (gattService != null) { + serviceUUID = gattService.getUuid(); + } + Log.d(TAG, String.format(Locale.getDefault(), "onCharacteristicChanged : deice : %s, serviceUuid = %s, characteristicUuid = %s, \ndata : [%s]", + printDeviceInfo(device), serviceUUID, characteristicUUID, CHexConver.byte2HexStr(data))); + mCallbackManager.onBleDataNotification(device, serviceUUID, characteristicUUID, data); + } + + @SuppressLint("MissingPermission") + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (null == gatt || null == gatt.getDevice() || null == characteristic + // || !AppUtil.checkHasConnectPermission(mContext) + ) return; + BluetoothDevice device = gatt.getDevice(); + UUID serviceUUID = null; + UUID characteristicUUID = characteristic.getUuid(); + BluetoothGattService gattService = characteristic.getService(); + if (gattService != null) serviceUUID = gattService.getUuid(); + byte[] data = characteristic.getValue(); + Log.d(TAG, String.format(Locale.getDefault(), "onCharacteristicWrite : device : %s, serviceUuid = %s, characteristicUuid = %s, status = %d, \ndata : [%s]", + printDeviceInfo(device), serviceUUID, characteristicUUID, status, CHexConver.byte2HexStr(data))); + wakeupSendThread(gatt, serviceUUID, characteristicUUID, status, data); + mCallbackManager.onBleWriteStatus(device, serviceUUID, characteristicUUID, data, status); + } + + @SuppressLint("MissingPermission") + @Override + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + if (null == gatt + // || !AppUtil.checkHasConnectPermission(mContext) + ) return; + BluetoothDevice device = gatt.getDevice(); + if (null == device || null == descriptor) return; + UUID serviceUuid = null; + UUID characteristicUuid = null; + BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic(); + if (null != characteristic) { + characteristicUuid = characteristic.getUuid(); + BluetoothGattService bluetoothGattService = characteristic.getService(); + if (null != bluetoothGattService) { + serviceUuid = bluetoothGattService.getUuid(); + } + } + Log.i(TAG, String.format(Locale.getDefault(), "onDescriptorWrite : device : %s, serviceUuid = %s, characteristicUuid = %s, descriptor = %s, status = %d", + printDeviceInfo(device), serviceUuid, characteristicUuid, descriptor.getUuid(), status)); + mCallbackManager.onBleNotificationStatus(device, serviceUuid, characteristicUuid, status); + if (mNotifyCharacteristicRunnable != null && BluetoothUtil.deviceEquals(device, mNotifyCharacteristicRunnable.getBleDevice()) + && serviceUuid != null && serviceUuid.equals(mNotifyCharacteristicRunnable.getServiceUUID()) + && characteristicUuid != null && characteristicUuid.equals(mNotifyCharacteristicRunnable.getCharacteristicUUID()) + && descriptor.getUuid() != null && descriptor.getUuid().equals(mNotifyCharacteristicRunnable.mDescriptorUUID)) { + if (status == BluetoothGatt.GATT_SUCCESS) { + mNotifyCharacteristicRunnable = null; + // TODO: 2022/6/28 由于部分手机(小米11 Lite)的兼容性问题,MTU不能调整到最大,建议适当减小MTU +// startChangeMtu(gatt, 128); //调整MTU最大为128 + int requestMTU = configHelper.getBleRequestMtu(); + if (requestMTU > 509) { + requestMTU = 509; + } + startChangeMtu(gatt, requestMTU); +// handleBleConnectedEvent(device); + } else { + int num = mNotifyCharacteristicRunnable.getRetryNum(); + if (num < 3) { + mNotifyCharacteristicRunnable.setRetryNum(++num); + mHandler.postDelayed(mNotifyCharacteristicRunnable, 100); + } else { + disconnectBleDevice(device); + } + } + } + } + + @SuppressLint("MissingPermission") + @Override + public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { + if (null == gatt + // || !AppUtil.checkHasConnectPermission(mContext) + ) return; + BluetoothDevice device = gatt.getDevice(); + if (null == device) return; + Log.d(TAG, String.format(Locale.getDefault(), "onMtuChanged : device : %s, mtu = %d, status = %d", printDeviceInfo(device), mtu, status)); + mCallbackManager.onBleDataBlockChanged(device, mtu, status); + BleDevice bleDevice = getConnectedBle(device); + if (BluetoothGatt.GATT_SUCCESS == status) { + // 需要减去3个字节的数据包头部信息 + int bleMtu = mtu - 3; + if (bleDevice != null && mHandler.hasMessages(MSG_CHANGE_BLE_MTU_TIMEOUT)) { //调整MTU的回调 + stopChangeMtu(); + bleDevice.setMtu(bleMtu); + Log.i(TAG, "-onMtuChanged- handleBleConnectedEvent"); + handleBleConnectedEvent(device); + } + } + } + }; + + private class BaseBtAdapterReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null) { + String action = intent.getAction(); + if (action == null) return; + switch (action) { + case BluetoothAdapter.ACTION_STATE_CHANGED: { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1); + if (mBluetoothAdapter != null && state == -1) { + state = mBluetoothAdapter.getState(); + } + if (state == BluetoothAdapter.STATE_OFF) { + isBleScanning(false); + mDiscoveredBleDevices.clear(); + mCallbackManager.onDiscoveryBleChange(false); + disconnectBleDevice(getConnectedBtDevice()); + mCallbackManager.onAdapterChange(false); + } else if (state == BluetoothAdapter.STATE_ON) { + mCallbackManager.onAdapterChange(true); + } + break; + } + case BluetoothDevice.ACTION_ACL_CONNECTED: { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + Log.i(TAG, "BaseBtAdapterReceiver: ACTION_ACL_CONNECTED, device : " + + printDeviceInfo(device)); + break; + } + case BluetoothDevice.ACTION_ACL_DISCONNECTED: { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + Log.i(TAG, "BaseBtAdapterReceiver: ACTION_ACL_DISCONNECTED, device : " + + printDeviceInfo(device)); + break; + } + + } + } + } + } + + private class NotifyCharacteristicRunnable implements Runnable { + private final BluetoothGatt mGatt; + private final UUID mServiceUUID; + private final UUID mCharacteristicUUID; + public final UUID mDescriptorUUID = BLE_UUID_NOTIFICATION_DESCRIPTOR; + private int retryNum = 0; + + private NotifyCharacteristicRunnable(BluetoothGatt gatt, UUID serviceUUID, UUID characteristicUUID) { + this.mGatt = gatt; + this.mServiceUUID = serviceUUID; + this.mCharacteristicUUID = characteristicUUID; + } + + private void setRetryNum(int retryNum) { + this.retryNum = retryNum; + } + + private int getRetryNum() { + return retryNum; + } + + private BluetoothDevice getBleDevice() { + if (mGatt == null) return null; + return mGatt.getDevice(); + } + + private UUID getServiceUUID() { + return mServiceUUID; + } + + private UUID getCharacteristicUUID() { + return mCharacteristicUUID; + } + + @Override + public void run() { + boolean ret = enableBLEDeviceNotification(mGatt, mServiceUUID, mCharacteristicUUID); + Log.w(TAG, String.format(Locale.getDefault(), "enableBLEDeviceNotification ===> %s, service uuid = %s, characteristic uuid = %s", + ret, mServiceUUID, mCharacteristicUUID)); + if (!ret) { + if (mGatt != null) { + disconnectBleDevice(mGatt.getDevice()); + } + } else { + mHandler.removeMessages(MSG_NOTIFY_BLE_TIMEOUT); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_NOTIFY_BLE_TIMEOUT, mGatt.getDevice()), CALLBACK_TIMEOUT); + } + } + } +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/SendBleDataThread.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/SendBleDataThread.java new file mode 100644 index 000000000..c095bdddd --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/SendBleDataThread.java @@ -0,0 +1,295 @@ +package com.jieli.otasdk.tool.ota.ble; + +import android.bluetooth.BluetoothGatt; + +import com.jieli.jl_bt_ota.util.JL_Log; +import com.jieli.otasdk.tool.ota.ble.interfaces.IBleOp; +import com.jieli.otasdk.tool.ota.ble.interfaces.OnThreadStateListener; +import com.jieli.otasdk.tool.ota.ble.interfaces.OnWriteDataCallback; + +import java.util.Arrays; +import java.util.UUID; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * 发送BLE数据线程 + * + * @author zqjasonZhong + * @date 2019/10/8 + */ +public class SendBleDataThread extends Thread { + private final static String TAG = "SendBleDataThread"; + private final LinkedBlockingQueue mQueue = new LinkedBlockingQueue<>(); + private volatile boolean isDataSend = false; + private volatile boolean isThreadWaiting = false; + private volatile boolean isWaitingForCallback = false; + private volatile int retryNum = 0; + private final IBleOp mBleManager; + private final OnThreadStateListener mListener; + + private BleSendTask mCurrentTask; + + public SendBleDataThread(IBleOp manager, OnThreadStateListener listener) { + super(TAG); + mBleManager = manager; + mListener = listener; + } + + public boolean isRunning() { + return isDataSend; + } + + public boolean addSendTask(BluetoothGatt gatt, UUID serviceUUID, UUID characteristicUUID, byte[] data, OnWriteDataCallback callback) { + if (null == mBleManager || null == gatt || null == serviceUUID || null == characteristicUUID || null == data || data.length == 0) { + return false; + } + int mtu = mBleManager.getBleMtu(); + JL_Log.d(TAG, "addSendTask : " + mtu); + int dataLen = data.length; + int blockCount = dataLen / mtu; + boolean ret = false; + for (int i = 0; i < blockCount; i++) { + byte[] mBlockData = new byte[mtu]; + System.arraycopy(data, i * mtu, mBlockData, 0, mBlockData.length); + ret = addSendData(gatt, serviceUUID, characteristicUUID, mBlockData, callback); + } + + if (0 != dataLen % mtu) { + JL_Log.d(TAG, "addSendTask : " + mtu); + byte[] noBlockData = new byte[dataLen % mtu]; + System.arraycopy(data, dataLen - dataLen % mtu, noBlockData, 0, noBlockData.length); + ret = addSendData(gatt, serviceUUID, characteristicUUID, noBlockData, callback); + } + return ret; + } + + public void wakeupSendThread(BleSendTask sendTask) { + if (null == sendTask || mCurrentTask != null && mCurrentTask.equals(sendTask)) { + if (sendTask != null) { + sendTask.setCallback(mCurrentTask.getCallback()); + mCurrentTask = sendTask; + } + synchronized (mQueue) { + if (isThreadWaiting) { + if (isWaitingForCallback) { + mQueue.notifyAll(); + } else { + mQueue.notify(); + } + } else if (isWaitingForCallback) { + mQueue.notify(); + } + } + } + } + + private boolean addSendData(BluetoothGatt gatt, UUID serviceUUID, UUID characteristicUUID, byte[] data, OnWriteDataCallback callback) { + boolean ret = false; + if (isDataSend) { + BleSendTask sendTask = new BleSendTask(gatt, serviceUUID, characteristicUUID, data, callback); + try { + mQueue.put(sendTask); + ret = true; + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (ret && isThreadWaiting && !isWaitingForCallback) { + isThreadWaiting = false; + synchronized (mQueue) { + mQueue.notify(); + } + } + } + return ret; + } + + @Override + public synchronized void start() { + isDataSend = true; + super.start(); + } + + public synchronized void stopThread() { + isDataSend = false; + wakeupSendThread(null); + } + + private void callbackResult(BleSendTask task, boolean result) { + if (task != null && task.getCallback() != null) { + if (task.getBleGatt() == null) return; + task.getCallback().onBleResult(task.getBleGatt().getDevice(), task.getServiceUUID(), + task.getCharacteristicUUID(), result, task.getData()); + } else { + JL_Log.i(TAG, "getCallback is null."); + } + } + + @Override + public void run() { + JL_Log.d(TAG, "send ble data thread is started."); + if (mListener != null) { + mListener.onStart(getId(), getName()); + } + if (mBleManager != null) { + synchronized (mQueue) { + while (isDataSend) { + mCurrentTask = null; + isThreadWaiting = false; + isWaitingForCallback = false; + if (mQueue.isEmpty()) { + isThreadWaiting = true; + JL_Log.d(TAG, "queue is empty, so waiting for data"); + try { + mQueue.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + mCurrentTask = mQueue.peek(); + if (mCurrentTask != null) { + isWaitingForCallback = mBleManager.writeDataByBle(mCurrentTask.mGatt, mCurrentTask.getServiceUUID(), + mCurrentTask.getCharacteristicUUID(), mCurrentTask.getData()); + if (isWaitingForCallback) { + try { + mQueue.wait(BleManager.SEND_DATA_MAX_TIMEOUT); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + mCurrentTask.setStatus(-1); + } + JL_Log.d(TAG, "data send ret :" + mCurrentTask.getStatus()); + if (mCurrentTask.getStatus() != BluetoothGatt.GATT_SUCCESS) { //发送失败 + retryNum++; + if (retryNum >= 3) { //重发次数超过限制 + callbackResult(mCurrentTask, false); + mQueue.clear(); + } else { + if (mCurrentTask.getStatus() != -1) { + mCurrentTask.setStatus(-1); + try { + sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + continue; + } + } else { //发送成功 + callbackResult(mCurrentTask, true); + } + } + retryNum = 0; + if (!mQueue.isEmpty()) mQueue.poll(); + } + } + } + + isWaitingForCallback = false; + isThreadWaiting = false; + mQueue.clear(); + if (mListener != null) { + mListener.onEnd(getId(), getName()); + } + JL_Log.d(TAG, "send ble data thread exit."); + } + } + + + public static class BleSendTask { + private BluetoothGatt mGatt; + private UUID serviceUUID; + private UUID characteristicUUID; + private byte[] data; + private int status = -1; + private OnWriteDataCallback mCallback; + + public BleSendTask(BluetoothGatt gatt, UUID serviceUUID, UUID characteristicUUID, byte[] data, OnWriteDataCallback callback) { + this.mGatt = gatt; + this.serviceUUID = serviceUUID; + this.characteristicUUID = characteristicUUID; + this.data = data; + this.mCallback = callback; + } + + public BluetoothGatt getBleGatt() { + return mGatt; + } + + public void setDevice(BluetoothGatt gatt) { + mGatt = gatt; + } + + public UUID getServiceUUID() { + return serviceUUID; + } + + public void setServiceUUID(UUID serviceUUID) { + this.serviceUUID = serviceUUID; + } + + public UUID getCharacteristicUUID() { + return characteristicUUID; + } + + public void setCharacteristicUUID(UUID characteristicUUID) { + this.characteristicUUID = characteristicUUID; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + public OnWriteDataCallback getCallback() { + return mCallback; + } + + public void setCallback(OnWriteDataCallback callback) { + mCallback = callback; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + @Override + public String toString() { + return "BleSendTask{" + + "mGatt=" + mGatt + + ", serviceUUID=" + serviceUUID + + ", characteristicUUID=" + characteristicUUID + + ", data=" + Arrays.toString(data) + + ", status=" + status + + ", mCallback=" + mCallback + + '}'; + } + + @Override + public int hashCode() { + if (mGatt != null && serviceUUID != null && characteristicUUID != null) { + return mGatt.hashCode() + serviceUUID.hashCode() + characteristicUUID.hashCode(); + } + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof BleSendTask) { + BleSendTask other = (BleSendTask) obj; + if (mGatt != null && serviceUUID != null && characteristicUUID != null) { + return mGatt.equals(other.getBleGatt()) && serviceUUID.equals(other.getServiceUUID()) + && characteristicUUID.equals(other.getCharacteristicUUID()); + } + } + return false; + } + } +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/BleEventCallback.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/BleEventCallback.java new file mode 100644 index 000000000..99569b3b7 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/BleEventCallback.java @@ -0,0 +1,68 @@ +package com.jieli.otasdk.tool.ota.ble.interfaces; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGattService; + +import com.jieli.otasdk.tool.ota.ble.model.BleScanInfo; + +import java.util.List; +import java.util.UUID; + +/** + * Ble事件回调抽象类 + * + * @author zqjasonZhong + * @since 2020/7/16 + */ +public abstract class BleEventCallback implements IBleEventCallback { + + @Override + public void onAdapterChange(boolean bEnabled) { + + } + + @Override + public void onDiscoveryBleChange(boolean bStart) { + + } + + @Override + public void onDiscoveryBle(BluetoothDevice device, BleScanInfo bleScanMessage) { + + } + + @Override + public void onBleConnection(BluetoothDevice device, int status) { + + } + + @Override + public void onBleServiceDiscovery(BluetoothDevice device, int status, List services) { + + } + + @Override + public void onBleNotificationStatus(BluetoothDevice device, UUID serviceUuid, UUID characteristicUuid, int status) { + + } + + @Override + public void onBleDataBlockChanged(BluetoothDevice device, int block, int status) { + + } + + @Override + public void onBleDataNotification(BluetoothDevice device, UUID serviceUuid, UUID characteristicsUuid, byte[] data) { + + } + + @Override + public void onBleWriteStatus(BluetoothDevice device, UUID serviceUuid, UUID characteristicsUuid, byte[] data, int status) { + + } + + @Override + public void onConnectionUpdated(BluetoothDevice device, int interval, int latency, int timeout, int status) { + + } +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/IBleEventCallback.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/IBleEventCallback.java new file mode 100644 index 000000000..b001e61f3 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/IBleEventCallback.java @@ -0,0 +1,108 @@ +package com.jieli.otasdk.tool.ota.ble.interfaces; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGattService; + +import com.jieli.otasdk.tool.ota.ble.model.BleScanInfo; + +import java.util.List; +import java.util.UUID; + +/** + * Ble事件回调 + * + * @author zqjasonZhong + * @since 2020/7/16 + */ +public interface IBleEventCallback { + + /** + * 蓝牙适配器开关回调 + * + * @param bEnabled 开关 + */ + void onAdapterChange(boolean bEnabled); + + /** + * 搜索蓝牙设备的状态回调 + * + * @param bStart 搜索状态 + */ + void onDiscoveryBleChange(boolean bStart); + + /** + * 发现蓝牙设备的回调 + * + * @param device 蓝牙设备对象 + * @param bleScanInfo BLE扫描数据 + */ + void onDiscoveryBle(BluetoothDevice device, BleScanInfo bleScanInfo); + + /** + * BLE连接状态回调 + * + * @param device 蓝牙设备对象 + * @param status 连接状态 + */ + void onBleConnection(BluetoothDevice device, int status); + + /** + * BLE服务发现回调 + * + * @param device 蓝牙设备对象 + * @param status 状态 + * @param services 服务列表 + */ + void onBleServiceDiscovery(BluetoothDevice device, int status, List services); + + /** + * BLE特征状态回调 + * + * @param device 蓝牙设备对象 + * @param serviceUuid 服务UUID + * @param characteristicUuid 特征值UUID + * @param status 状态 + */ + void onBleNotificationStatus(BluetoothDevice device, UUID serviceUuid, UUID characteristicUuid, int status); + + /** + * BLE MTU改变回调 + * + * @param device 蓝牙设备对象 + * @param block 协商后的MTU + * @param status 状态 + */ + void onBleDataBlockChanged(BluetoothDevice device, int block, int status); + + /** + * BLE数据回调 + * + * @param device 蓝牙设备对象 + * @param serviceUuid 服务UUID + * @param characteristicsUuid 特征值UUID + * @param data 数据 + */ + void onBleDataNotification(BluetoothDevice device, UUID serviceUuid, UUID characteristicsUuid, byte[] data); + + /** + * BLE 写数据回调 + * + * @param device 蓝牙设备对象 + * @param serviceUuid 服务UUID + * @param characteristicsUuid 特征值UUID + * @param data 数据 + * @param status 状态 + */ + void onBleWriteStatus(BluetoothDevice device, UUID serviceUuid, UUID characteristicsUuid, byte[] data, int status); + + /** + * BLE 连接参数回调 + * + * @param device 蓝牙设备对象 + * @param interval 发数间隔 + * @param latency 延时参数 + * @param timeout 超时时间 + * @param status 状态 + */ + void onConnectionUpdated(BluetoothDevice device, int interval, int latency, int timeout, int status); +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/IBleOp.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/IBleOp.java new file mode 100644 index 000000000..212910aba --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/IBleOp.java @@ -0,0 +1,16 @@ +package com.jieli.otasdk.tool.ota.ble.interfaces; + +import android.bluetooth.BluetoothGatt; + +import java.util.UUID; + +/** + * @author zqjasonZhong + * @since 2020/12/24 + */ +public interface IBleOp { + + int getBleMtu(); + + boolean writeDataByBle(BluetoothGatt gatt, UUID serviceUUID, UUID characteristicUUID, byte[] data); +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/OnThreadStateListener.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/OnThreadStateListener.java new file mode 100644 index 000000000..87ba5326a --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/OnThreadStateListener.java @@ -0,0 +1,14 @@ +package com.jieli.otasdk.tool.ota.ble.interfaces; + +/** + * 线程生命周期监听器 + * + * @author zqjasonZhong + * @date 2019/9/20 + */ +public interface OnThreadStateListener { + + void onStart(long id, String name); + + void onEnd(long id, String name); +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/OnWriteDataCallback.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/OnWriteDataCallback.java new file mode 100644 index 000000000..2d0f716b0 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/interfaces/OnWriteDataCallback.java @@ -0,0 +1,25 @@ +package com.jieli.otasdk.tool.ota.ble.interfaces; + +import android.bluetooth.BluetoothDevice; + +import java.util.UUID; + +/** + * 写数据的结果回调 + * + * @author zqjasonZhong + * @date 2019/9/25 + */ +public interface OnWriteDataCallback { + + /** + * 回调BLE发送结果 + * + * @param device 蓝牙设备 + * @param serviceUUID 服务UUID + * @param characteristicUUID 特征值UUID + * @param result 发送结果 + * @param data 发送数据 + */ + void onBleResult(BluetoothDevice device, UUID serviceUUID, UUID characteristicUUID, boolean result, byte[] data); +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/model/BleDevice.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/model/BleDevice.java new file mode 100644 index 000000000..894cb07da --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/model/BleDevice.java @@ -0,0 +1,155 @@ +package com.jieli.otasdk.tool.ota.ble.model; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.jieli.jl_bt_ota.constant.BluetoothConstant; +import com.jieli.jl_bt_ota.util.CHexConver; + +import com.jieli.otasdk.tool.ota.ble.SendBleDataThread; +import com.jieli.otasdk.tool.ota.ble.interfaces.IBleOp; +import com.jieli.otasdk.tool.ota.ble.interfaces.OnThreadStateListener; +import com.jieli.otasdk.tool.ota.ble.interfaces.OnWriteDataCallback; +//import com.jieli.otasdk.util.AppUtil; + +import java.util.UUID; +import android.util.Log; +/** + * @author zqjasonZhong + * @email zhongzhuocheng@zh-jieli.com + * @desc BLE设备 + * @since 2022/12/5 + */ +public class BleDevice { + private final String tag = "BleManager"; + private final Context context; //上下文 + private final BluetoothGatt gatt; //BleGatt控制对象 + private int mtu = BluetoothConstant.BLE_MTU_MIN; //MTU + private long connectedTime; //连接的时间戳 + + private SendBleDataThread sendDataThread; //发送数据线程 + + public BleDevice(@NonNull Context context, @NonNull BluetoothGatt gatt) { + this.context = context; + this.gatt = gatt; + } + + @NonNull + public BluetoothGatt getGatt() { + return gatt; + } + + public int getMtu() { + if (mtu > 128) { + mtu -= 6; + } + return mtu; + } + + public void setMtu(int mtu) { + this.mtu = mtu; + } + + public long getConnectedTime() { + return connectedTime; + } + + public void setConnectedTime(long connectedTime) { + this.connectedTime = connectedTime; + } + + public void startSendDataThread() { + if (sendDataThread == null || !sendDataThread.isRunning()) { + sendDataThread = new SendBleDataThread(new IBleOp() { + @Override + public int getBleMtu() { + return getMtu(); + } + + @Override + public boolean writeDataByBle(BluetoothGatt gatt, UUID serviceUUID, UUID characteristicUUID, byte[] data) { + Log.d(tag, "writeDataByBle " + gatt +"serviceUUID "+ serviceUUID + "characteristicUUID "+ characteristicUUID + data ); + return writeDataToDeviceByBle(gatt, serviceUUID, characteristicUUID, data); + } + }, new OnThreadStateListener() { + @Override + public void onStart(long id, String name) { + + } + + @Override + public void onEnd(long id, String name) { + if (sendDataThread != null && sendDataThread.getId() == id) { + sendDataThread = null; + } + } + }); + sendDataThread.start(); + } + } + + public void stopSendDataThread() { + if (sendDataThread != null) { + sendDataThread.stopThread(); + } + } + + public void wakeupSendThread(SendBleDataThread.BleSendTask task) { + if (sendDataThread != null && task != null && gatt.equals(task.getBleGatt())) { + sendDataThread.wakeupSendThread(task); + } + } + + public boolean addSendTask(UUID serviceUUID, UUID characteristicUUID, byte[] data, OnWriteDataCallback callback) { + boolean ret = false; + if (sendDataThread != null && sendDataThread.isRunning()) { + ret = sendDataThread.addSendTask(gatt, serviceUUID, characteristicUUID, data, callback); + } + return ret; + } + + @SuppressLint("MissingPermission") + private boolean writeDataToDeviceByBle(BluetoothGatt gatt, UUID serviceUUID, UUID characteristicUUID, byte[] data) { + if (gatt == null || null == serviceUUID || null == characteristicUUID || null == data || data.length == 0 + // || !AppUtil.checkHasConnectPermission(this.context) + ) { + Log.d(tag, "writeDataByBle : param is invalid."); + return false; + } + BluetoothGattService gattService = gatt.getService(serviceUUID); + if (null == gattService) { + Log.d(tag, "writeDataByBle : service is null."); + return false; + } + BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(characteristicUUID); + if (null == gattCharacteristic) { + Log.d(tag, "writeDataByBle : characteristic is null"); + return false; + } + boolean ret = false; + try { + gattCharacteristic.setValue(data); + ret = gatt.writeCharacteristic(gattCharacteristic); + } catch (Exception e) { + e.printStackTrace(); + } + Log.d(tag, "writeDataByBle : send ret : " + ret + ", data = " + CHexConver.byte2HexStr(data)); + return ret; + } + + @Override + public String toString() { + return "BleDevice{" + + "context=" + context + + ", gatt=" + gatt + + ", mtu=" + mtu + + ", connectedTime=" + connectedTime + + ", sendDataThread=" + sendDataThread + + '}'; + } +} diff --git a/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/model/BleScanInfo.java b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/model/BleScanInfo.java new file mode 100644 index 000000000..863e28594 --- /dev/null +++ b/local_plugins/ota/android/src/main/kotlin/com/example/ota/tool/ota/ble/model/BleScanInfo.java @@ -0,0 +1,99 @@ +package com.jieli.otasdk.tool.ota.ble.model; + + +import android.os.Parcel; +import android.os.Parcelable; + +import com.jieli.jl_bt_ota.util.CHexConver; + +/** + * BLE 设备扫描信息 + *

+ * Created by zqjasonzhong on 2018/10/17. + */ + +public class BleScanInfo implements Parcelable { + /** + * 原始数据 + */ + private byte[] rawData; + /** + * 信号强度 + */ + private int rssi; + /** + * 是否允许连接 + *

说明: 默认允许连接,特殊情况不允许

+ */ + private boolean isEnableConnect = true; + + public BleScanInfo() { + + } + + protected BleScanInfo(Parcel in) { + rawData = in.createByteArray(); + rssi = in.readInt(); + isEnableConnect = in.readByte() != 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public BleScanInfo createFromParcel(Parcel in) { + return new BleScanInfo(in); + } + + @Override + public BleScanInfo[] newArray(int size) { + return new BleScanInfo[size]; + } + }; + + public byte[] getRawData() { + return rawData; + } + + public BleScanInfo setRawData(byte[] rawData) { + this.rawData = rawData; + return this; + } + + public int getRssi() { + return rssi; + } + + public BleScanInfo setRssi(int rssi) { + this.rssi = rssi; + return this; + } + + public boolean isEnableConnect() { + return isEnableConnect; + } + + public BleScanInfo setEnableConnect(boolean enableConnect) { + isEnableConnect = enableConnect; + return this; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByteArray(rawData); + dest.writeInt(rssi); + dest.writeByte((byte) (isEnableConnect ? 1 : 0)); + } + + @Override + public String toString() { + return "BleScanMessage{" + + "rawData=" + CHexConver.byte2HexStr(rawData) + + ", rssi=" + rssi + + ", isEnableConnect=" + isEnableConnect + + '}'; + } +} diff --git a/local_plugins/ota/ios/.gitignore b/local_plugins/ota/ios/.gitignore new file mode 100644 index 000000000..034771fc9 --- /dev/null +++ b/local_plugins/ota/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh diff --git a/local_plugins/ota/ios/Assets/.gitkeep b/local_plugins/ota/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/local_plugins/ota/ios/Classes/OtaPlugin.swift b/local_plugins/ota/ios/Classes/OtaPlugin.swift new file mode 100644 index 000000000..8b67367c7 --- /dev/null +++ b/local_plugins/ota/ios/Classes/OtaPlugin.swift @@ -0,0 +1,19 @@ +import Flutter +import UIKit + +public class OtaPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "ota", binaryMessenger: registrar.messenger()) + let instance = OtaPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/local_plugins/ota/ios/Resources/PrivacyInfo.xcprivacy b/local_plugins/ota/ios/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..a34b7e2e6 --- /dev/null +++ b/local_plugins/ota/ios/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/local_plugins/ota/ios/ota.podspec b/local_plugins/ota/ios/ota.podspec new file mode 100644 index 000000000..ecbf38449 --- /dev/null +++ b/local_plugins/ota/ios/ota.podspec @@ -0,0 +1,29 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint ota.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'ota' + s.version = '0.0.1' + s.summary = 'A new Flutter project.' + s.description = <<-DESC +A new Flutter project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '12.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + # If your plugin requires a privacy manifest, for example if it uses any + # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your + # plugin's privacy impact, and then uncomment this line. For more information, + # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + # s.resource_bundles = {'ota_privacy' => ['Resources/PrivacyInfo.xcprivacy']} +end diff --git a/local_plugins/ota/lib/ota.dart b/local_plugins/ota/lib/ota.dart new file mode 100644 index 000000000..d3d2606f1 --- /dev/null +++ b/local_plugins/ota/lib/ota.dart @@ -0,0 +1,19 @@ +import 'ota_platform_interface.dart'; + +class Ota { + Future getPlatformVersion() { + return OtaPlatform.instance.getPlatformVersion(); + } + + Future searchDevices() { + return OtaPlatform.instance.searchDevices(); + } + + Future connectDevice() { + return OtaPlatform.instance.connectDevice(); + } + + Future startUpgrade() { + return OtaPlatform.instance.startUpgrade(); + } +} diff --git a/local_plugins/ota/lib/ota_method_channel.dart b/local_plugins/ota/lib/ota_method_channel.dart new file mode 100644 index 000000000..56e8fde86 --- /dev/null +++ b/local_plugins/ota/lib/ota_method_channel.dart @@ -0,0 +1,34 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'ota_platform_interface.dart'; + +/// An implementation of [OtaPlatform] that uses method channels. +class MethodChannelOta extends OtaPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('ota'); + + @override + Future getPlatformVersion() async { + final version = await methodChannel.invokeMethod( + 'getPlatformVersion', + ); + return version; + } + + Future searchDevices() async { + final version = await methodChannel.invokeMethod('searchDevices'); + return version; + } + + Future connectDevice() async { + final version = await methodChannel.invokeMethod('connectDevice'); + return version; + } + + Future startUpgrade() async { + final version = await methodChannel.invokeMethod('startUpgrade'); + return version; + } +} diff --git a/local_plugins/ota/lib/ota_platform_interface.dart b/local_plugins/ota/lib/ota_platform_interface.dart new file mode 100644 index 000000000..77ade98ad --- /dev/null +++ b/local_plugins/ota/lib/ota_platform_interface.dart @@ -0,0 +1,41 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'ota_method_channel.dart'; + +abstract class OtaPlatform extends PlatformInterface { + /// Constructs a OtaPlatform. + OtaPlatform() : super(token: _token); + + static final Object _token = Object(); + + static OtaPlatform _instance = MethodChannelOta(); + + /// The default instance of [OtaPlatform] to use. + /// + /// Defaults to [MethodChannelOta]. + static OtaPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [OtaPlatform] when + /// they register themselves. + static set instance(OtaPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } + + Future searchDevices() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } + + Future connectDevice() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } + + Future startUpgrade() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/local_plugins/ota/pubspec.yaml b/local_plugins/ota/pubspec.yaml new file mode 100644 index 000000000..cb281e3be --- /dev/null +++ b/local_plugins/ota/pubspec.yaml @@ -0,0 +1,72 @@ +name: ota +description: "A new Flutter project." +version: 0.0.1 +homepage: + +environment: + sdk: ^3.7.2 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.example.ota + pluginClass: OtaPlugin + ios: + pluginClass: OtaPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/pubspec.yaml b/pubspec.yaml index 55f6887b5..8c59349d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,7 +75,8 @@ dependencies: path: local_plugins/chat_storage spotify_sdk: path: local_plugins/spotify_sdk - + ota: + path: local_plugins/ota jl_ota: path: local_plugins/jl_ota ble_service: