55 changed files with 2317 additions and 710 deletions
@ -0,0 +1,9 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
import '../controllers/ota_upgrade_controller.dart'; |
||||
|
|
||||
|
class OtaUpgradeBinding extends Bindings { |
||||
|
@override |
||||
|
void dependencies() { |
||||
|
Get.lazyPut<OtaUpgradeController>(() => OtaUpgradeController()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,213 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'dart:async'; |
||||
|
import 'dart:convert'; |
||||
|
import 'package:get_storage/get_storage.dart'; |
||||
|
import '../../../data/services/network/api.dart'; |
||||
|
import '../ota.dart'; |
||||
|
import 'dart:io'; |
||||
|
import 'package:dio/dio.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:package_info_plus/package_info_plus.dart'; |
||||
|
import 'package:path_provider/path_provider.dart'; |
||||
|
import 'package:json_annotation/json_annotation.dart'; |
||||
|
|
||||
|
class OtaUpgradeController extends GetxController { |
||||
|
final Dio _dio = Dio(); |
||||
|
final RxString latestVersion = ''.obs; |
||||
|
final RxString currentVersion = ''.obs; |
||||
|
String updatepackageaddress = ''; |
||||
|
final RxDouble downloadProgress = 0.0.obs; |
||||
|
final RxBool isUpdating = false.obs; |
||||
|
final isUpdateAvailable = true.obs; |
||||
|
|
||||
|
final RxDouble progress = 0.0.obs; |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
_getCurrentVersion(); |
||||
|
} |
||||
|
|
||||
|
// 获取当前版本信息 |
||||
|
Future<void> _getCurrentVersion() async { |
||||
|
//发送ble获取固件版本 |
||||
|
var firmwareVersion = "1.1.0"; |
||||
|
currentVersion.value = firmwareVersion; |
||||
|
//发送ble获取产品id |
||||
|
var id = "a2"; |
||||
|
//发送api请求 |
||||
|
final response = await Api.getequipmentinfo({ |
||||
|
"id": id, |
||||
|
}); |
||||
|
if (response['info'] != '') { |
||||
|
print('getLoginToken:进来了'); |
||||
|
// 根据API设计调整 |
||||
|
final resp = OtaConfigResp.fromJson(response); |
||||
|
|
||||
|
print('成功解析: ${resp.id}, ${resp.factory}'); |
||||
|
latestVersion.value = resp.systemversion!; |
||||
|
updatepackageaddress = resp.updatepackageaddress!; |
||||
|
if (_isNewVersionAvailable(resp.systemversion!)) { |
||||
|
//需要升级 |
||||
|
print('需要更新'); |
||||
|
isUpdateAvailable.value = false; |
||||
|
} else { |
||||
|
isUpdateAvailable.value = true; |
||||
|
//已经是最新版本更新ui,按钮隐藏 |
||||
|
} |
||||
|
} else { |
||||
|
throw const FormatException('API 响应缺少 "info" 字段'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 版本比较方法优化 |
||||
|
bool _isNewVersionAvailable(String latest) { |
||||
|
try { |
||||
|
List<int> currentParts = currentVersion.value |
||||
|
.split('.') |
||||
|
.map((e) => int.parse(e.trim())) |
||||
|
.toList(); |
||||
|
List<int> latestParts = |
||||
|
latest.split('.').map((e) => int.parse(e.trim())).toList(); |
||||
|
|
||||
|
for (int i = 0; i < latestParts.length; i++) { |
||||
|
int current = (i < currentParts.length) ? currentParts[i] : 0; |
||||
|
if (latestParts[i] > current) return true; |
||||
|
if (latestParts[i] < current) return false; |
||||
|
} |
||||
|
return false; |
||||
|
} catch (e) { |
||||
|
print('版本比较错误: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 版本更新 |
||||
|
Future<void> checkUpdate() async { |
||||
|
try { |
||||
|
if (latestVersion.value != "") |
||||
|
_showUpdateDialog(latestVersion.value); |
||||
|
else { |
||||
|
Get.snackbar('错误', '获取最新版本信息失败'); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Get.snackbar('错误', '获取版本信息失败'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 显示更新对话框 |
||||
|
void _showUpdateDialog(String latest) { |
||||
|
Get.dialog( |
||||
|
AlertDialog( |
||||
|
title: Text('发现新版本 $latest'), |
||||
|
content: Text('是否立即更新?'), |
||||
|
actions: [ |
||||
|
TextButton( |
||||
|
onPressed: () => Get.back(), |
||||
|
child: Text('取消'), |
||||
|
), |
||||
|
TextButton( |
||||
|
onPressed: () { |
||||
|
Get.back(); |
||||
|
startDownload(); |
||||
|
}, |
||||
|
child: Text('更新'), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 开始下载 |
||||
|
Future<void> startDownload() async { |
||||
|
isUpdating.value = true; |
||||
|
// 查找最后一个 '/' 的位置 |
||||
|
int lastIndex = updatepackageaddress.lastIndexOf('/'); |
||||
|
|
||||
|
// 截取最后一个 '/' 后面的字符串 |
||||
|
String fileName = updatepackageaddress.substring(lastIndex + 1); |
||||
|
String directoryName = "ota_upgrades"; |
||||
|
|
||||
|
final dio = Dio(BaseOptions( |
||||
|
connectTimeout: const Duration(seconds: 5), |
||||
|
receiveTimeout: Duration.zero, |
||||
|
sendTimeout: const Duration(seconds: 5), |
||||
|
headers: {'Accept-Encoding': 'gzip'}, |
||||
|
)); |
||||
|
|
||||
|
Get.snackbar('升级命令已发送', '开始下载...'); |
||||
|
|
||||
|
try { |
||||
|
final Directory? downloadsDir = await getDownloadsDirectory(); |
||||
|
if (downloadsDir == null) { |
||||
|
throw FileSystemException('无法访问下载目录'); |
||||
|
} |
||||
|
|
||||
|
// 创建子目录:Download/APP |
||||
|
final targetDir = Directory('${downloadsDir.path}/$directoryName'); |
||||
|
if (!await targetDir.exists()) { |
||||
|
await targetDir.create(recursive: true); |
||||
|
} |
||||
|
|
||||
|
final savePath = '${targetDir.path}/$fileName'; |
||||
|
print('文件保存路径: $savePath'); // 关键日志 |
||||
|
|
||||
|
final options = Options( |
||||
|
receiveTimeout: Duration.zero, |
||||
|
followRedirects: true, |
||||
|
maxRedirects: 5, |
||||
|
); |
||||
|
|
||||
|
DateTime lastUpdate = DateTime.now(); |
||||
|
await dio.download( |
||||
|
updatepackageaddress, |
||||
|
savePath, |
||||
|
options: options, |
||||
|
onReceiveProgress: (received, total) { |
||||
|
final now = DateTime.now(); |
||||
|
if (now.difference(lastUpdate).inMilliseconds > 1000) { |
||||
|
lastUpdate = now; |
||||
|
String progressMsg = total != -1 |
||||
|
? '${(received / total * 100).toStringAsFixed(1)}%' |
||||
|
: '已下载:${(received / 1024 / 1024).toStringAsFixed(1)}MB'; |
||||
|
Get.snackbar( |
||||
|
'升级进度', |
||||
|
progressMsg, |
||||
|
duration: const Duration(seconds: 1), |
||||
|
); |
||||
|
} |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
// 下载完成后验证文件存在性 |
||||
|
File file = File(savePath); |
||||
|
if (await file.exists()) { |
||||
|
Get.snackbar('下载完成', '文件已保存至: $savePath'); // 明确提示路径 |
||||
|
} else { |
||||
|
Get.snackbar('错误', '文件未找到,请检查写入权限'); |
||||
|
} |
||||
|
isUpdating.value = false; |
||||
|
//下载完成,开始升级 |
||||
|
_installApp(savePath); |
||||
|
} on DioException catch (e) { |
||||
|
Get.snackbar('下载失败', '错误: ${e.message}'); |
||||
|
} on FileSystemException catch (e) { |
||||
|
Get.snackbar('存储失败', '无法写入文件: ${e.message}'); |
||||
|
} catch (e) { |
||||
|
Get.snackbar('未知错误', e.toString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 开始升级 |
||||
|
void _installApp(String path) async { |
||||
|
// final File file = File(path); |
||||
|
// if (await file.exists()) { |
||||
|
// // OpenResult result = await OpenFile.open(path); |
||||
|
// // if (result.type != ResultType.done) { |
||||
|
// // Get.snackbar('错误', '安装失败: ${result.message}'); |
||||
|
// // } |
||||
|
// } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
class OtaConfigResp { |
||||
|
// 单例实例 |
||||
|
static OtaConfigResp? _instance; |
||||
|
|
||||
|
// 单例访问器(增加空安全保护) |
||||
|
static OtaConfigResp get instance { |
||||
|
if (_instance == null) { |
||||
|
throw Exception("User must be initialized first"); |
||||
|
} |
||||
|
return _instance!; |
||||
|
} |
||||
|
|
||||
|
// 最终属性(不可变) |
||||
|
final String? id; |
||||
|
final String? factory; |
||||
|
final String? equipmentname; |
||||
|
final String? systemversion; |
||||
|
final String? updatepackageaddress; |
||||
|
// 私有构造函数 |
||||
|
OtaConfigResp._({ |
||||
|
this.id, |
||||
|
this.factory, |
||||
|
this.equipmentname, |
||||
|
this.systemversion, |
||||
|
this.updatepackageaddress, |
||||
|
}); |
||||
|
|
||||
|
factory OtaConfigResp.fromJson(Map<String, dynamic> json) { |
||||
|
print("数据获取fronTokenJson${json}"); |
||||
|
// 修复1:安全获取嵌套的 user 字段 |
||||
|
final infoData = json['info'] as Map<String, dynamic>?; // 允许空值 |
||||
|
|
||||
|
// 更新单例实例 |
||||
|
return _instance = OtaConfigResp._( |
||||
|
id: (infoData?['id'] as String?) ?? '', |
||||
|
factory: (infoData?['factory'] as String?) ?? '', |
||||
|
equipmentname: (infoData?['equipmentname'] as String?) ?? '', |
||||
|
systemversion: (infoData?['systemversion'] as String?) ?? '', |
||||
|
updatepackageaddress: |
||||
|
(infoData?['updatepackageaddress'] as String?) ?? '', |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 完整的JSON序列化 |
||||
|
Map<String, dynamic> toJson() => { |
||||
|
'user': { |
||||
|
'id': instance.id, |
||||
|
'factory': instance.factory, |
||||
|
'equipmentname': instance.equipmentname, |
||||
|
'systemversion': instance.systemversion, |
||||
|
'updatepackageaddress': instance.updatepackageaddress, |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,109 @@ |
|||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import '../controllers/ota_upgrade_controller.dart'; |
||||
|
|
||||
|
class OtaUpgradeView extends GetView<OtaUpgradeController> { |
||||
|
const OtaUpgradeView({Key? key}) : super(key: key); |
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Scaffold( |
||||
|
body: Center( |
||||
|
// 添加外层Center |
||||
|
child: SingleChildScrollView( |
||||
|
// 添加滚动防止小屏溢出 |
||||
|
child: Padding( |
||||
|
padding: EdgeInsets.all(20), |
||||
|
child: Column( |
||||
|
mainAxisAlignment: MainAxisAlignment.center, |
||||
|
crossAxisAlignment: CrossAxisAlignment.center, // 水平居中 |
||||
|
mainAxisSize: MainAxisSize.min, // 紧凑布局 |
||||
|
children: [ |
||||
|
// 设备图标 |
||||
|
CircleAvatar( |
||||
|
radius: 60, |
||||
|
backgroundColor: Colors.blue[100], |
||||
|
child: Icon( |
||||
|
Icons.phone_iphone, |
||||
|
size: 60, |
||||
|
color: Colors.blue, |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 30), |
||||
|
|
||||
|
// 版本信息 |
||||
|
Column( |
||||
|
children: [ |
||||
|
Text( |
||||
|
'当前版本', |
||||
|
style: TextStyle( |
||||
|
fontSize: 18, |
||||
|
color: Colors.grey, |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 8), |
||||
|
Text( |
||||
|
'v${controller.currentVersion}', |
||||
|
style: TextStyle( |
||||
|
fontSize: 24, |
||||
|
fontWeight: FontWeight.bold, |
||||
|
color: Colors.blue, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
SizedBox(height: 40), |
||||
|
|
||||
|
// 更新按钮/进度条 |
||||
|
Obx(() { |
||||
|
if (controller.isUpdating.value) { |
||||
|
return Column( |
||||
|
children: [ |
||||
|
SizedBox( |
||||
|
width: 200, // 限制进度条宽度 |
||||
|
child: LinearProgressIndicator( |
||||
|
value: controller.downloadProgress.value, |
||||
|
backgroundColor: Colors.grey[200], |
||||
|
valueColor: |
||||
|
AlwaysStoppedAnimation<Color>(Colors.blue), |
||||
|
minHeight: 12, |
||||
|
borderRadius: BorderRadius.circular(10), |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 20), |
||||
|
Text( |
||||
|
'${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%', |
||||
|
style: TextStyle(color: Colors.grey), |
||||
|
) |
||||
|
], |
||||
|
); |
||||
|
} else { |
||||
|
return ElevatedButton( |
||||
|
onPressed: controller.isUpdateAvailable.value |
||||
|
? null |
||||
|
: controller.checkUpdate, |
||||
|
style: ElevatedButton.styleFrom( |
||||
|
backgroundColor: controller.isUpdateAvailable.value |
||||
|
? Colors.grey |
||||
|
: Colors.blue, |
||||
|
foregroundColor: Colors.white, |
||||
|
padding: |
||||
|
EdgeInsets.symmetric(horizontal: 40, vertical: 15), |
||||
|
shape: RoundedRectangleBorder( |
||||
|
borderRadius: BorderRadius.circular(30), |
||||
|
), |
||||
|
), |
||||
|
child: Text( |
||||
|
controller.isUpdateAvailable.value ? '已是最新版本' : '立即更新', |
||||
|
style: TextStyle(fontSize: 16), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
}), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -1,85 +0,0 @@ |
|||||
package com.yunqiinnovation.agent_service |
|
||||
|
|
||||
import android.content.* |
|
||||
import android.bluetooth.le.* |
|
||||
import android.os.Build |
|
||||
import android.view.KeyEvent |
|
||||
import android.media.AudioManager |
|
||||
import com.yunqiinnovation.deepvoice_utils.FileLogger |
|
||||
import com.yunqiinnovation.ble_service.BleService |
|
||||
import com.yunqiinnovation.ble_service.BleConst |
|
||||
/** |
|
||||
* BLE唤醒接收器 |
|
||||
* 当耳机发送含特定UUID广播时,系统会触发此接收器 |
|
||||
*/ |
|
||||
class BleWakeReceiver : BroadcastReceiver() { |
|
||||
|
|
||||
companion object { |
|
||||
private const val TAG = "BleWakeReceiver" |
|
||||
} |
|
||||
|
|
||||
override fun onReceive(ctx: Context, intent: Intent) { |
|
||||
FileLogger.d(TAG, "onReceive, intent: $intent, ${BleConst.ACTION_SCAN_RESULT}") |
|
||||
FileLogger.init(ctx) |
|
||||
try { |
|
||||
|
|
||||
if (intent.action != BleConst.ACTION_SCAN_RESULT) { |
|
||||
return |
|
||||
} |
|
||||
val list = intent.getParcelableArrayListExtra<ScanResult>( |
|
||||
BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT |
|
||||
) |
|
||||
// FileLogger.d(TAG, "list: $list") |
|
||||
|
|
||||
if (list?.isEmpty() == true || list == null) { |
|
||||
FileLogger.d(TAG, "扫描结果为空") |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
val mac = list.firstOrNull()?.device?.address |
|
||||
if (mac == null) { |
|
||||
FileLogger.d(TAG, "没有获取到设备MAC地址") |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
FileLogger.d(TAG, "被系统唤醒,发现设备 $mac") |
|
||||
|
|
||||
if(!BleMediaService.isServiceRunning()) { |
|
||||
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager |
|
||||
|
|
||||
val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK) |
|
||||
am.dispatchMediaKeyEvent(keyEvent) |
|
||||
// val mediaIntent = Intent(Intent.ACTION_MEDIA_BUTTON) |
|
||||
// mediaIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent) |
|
||||
// mediaIntent.putExtra("mac", mac) // 额外传递MAC地址 |
|
||||
|
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
|
||||
// // 通过AudioManager发送媒体按键 |
|
||||
// am.dispatchMediaKeyEvent(keyEvent) |
|
||||
// // 发送UP事件 |
|
||||
// // am.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) |
|
||||
// } else { |
|
||||
// // 通过广播发送媒体按钮 |
|
||||
// mediaIntent.component = ComponentName(ctx.packageName, "androidx.media.session.MediaButtonReceiver") |
|
||||
// ctx.sendBroadcast(mediaIntent) |
|
||||
// } |
|
||||
} |
|
||||
if (!BleService.isConnected()) { |
|
||||
val serviceIntent = Intent(ctx, BleMediaService::class.java).apply { |
|
||||
action = BleConst.ACTION_SCAN_RESULT |
|
||||
putExtra("mac", mac) |
|
||||
} |
|
||||
|
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
|
||||
ctx.startForegroundService(serviceIntent) |
|
||||
} else { |
|
||||
ctx.startService(serviceIntent) |
|
||||
} |
|
||||
return |
|
||||
} |
|
||||
} |
|
||||
catch (e: Exception) { |
|
||||
FileLogger.e(TAG, "接收器异常: ${e}") |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,19 +0,0 @@ |
|||||
package com.yunqiinnovation.agent_service |
|
||||
|
|
||||
import android.content.* |
|
||||
import android.os.Build |
|
||||
|
|
||||
/** |
|
||||
* 系统启动接收器 |
|
||||
* 当系统重启完成后,自动启动BLE前台服务 |
|
||||
* 注意:需要用户在设置中授予应用自启动权限才能生效 |
|
||||
*/ |
|
||||
class BootReceiver : BroadcastReceiver() { |
|
||||
override fun onReceive(ctx: Context, i: Intent) { |
|
||||
if (i.action == Intent.ACTION_BOOT_COMPLETED) { |
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) |
|
||||
ctx.startForegroundService(Intent(ctx, BleMediaService::class.java)) |
|
||||
else ctx.startService(Intent(ctx, BleMediaService::class.java)) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,481 @@ |
|||||
|
package com.yunqiinnovation.ble_service |
||||
|
|
||||
|
import android.app.Activity |
||||
|
import android.app.PendingIntent |
||||
|
import android.bluetooth.BluetoothDevice |
||||
|
import android.bluetooth.BluetoothManager |
||||
|
import android.bluetooth.le.ScanFilter |
||||
|
import android.bluetooth.le.ScanSettings |
||||
|
import android.companion.AssociationInfo |
||||
|
import android.companion.AssociationRequest |
||||
|
import android.companion.BluetoothDeviceFilter |
||||
|
import android.companion.CompanionDeviceManager |
||||
|
import android.content.Context |
||||
|
import android.content.Intent |
||||
|
import android.content.IntentSender |
||||
|
import android.content.SharedPreferences |
||||
|
import android.os.Build |
||||
|
import android.os.ParcelUuid |
||||
|
import androidx.annotation.RequiresApi |
||||
|
import com.yunqiinnovation.deepvoice_utils.FileLogger |
||||
|
import java.util.regex.Pattern |
||||
|
|
||||
|
/** |
||||
|
* BleCompanionManager |
||||
|
* 用于管理BLE设备的配对和关联 |
||||
|
*/ |
||||
|
class BleCompanionManager(private val context: Context) { |
||||
|
private val TAG = "BleCompanionManager" |
||||
|
|
||||
|
// 常量定义 |
||||
|
companion object { |
||||
|
const val REQUEST_CODE_ASSOCIATE = 42 |
||||
|
private const val PREF_NAME = "ble_companion_prefs" |
||||
|
private const val KEY_ASSOCIATION_IDS = "association_ids" |
||||
|
private const val KEY_PAIRED_MAC = "paired_mac" |
||||
|
} |
||||
|
|
||||
|
// 会话保存 |
||||
|
private val sharedPreferences: SharedPreferences by lazy { |
||||
|
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) |
||||
|
} |
||||
|
|
||||
|
// 获取CompanionDeviceManager服务 |
||||
|
private val companionDeviceManager: CompanionDeviceManager? by lazy { |
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
||||
|
context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager |
||||
|
} else { |
||||
|
null |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查是否支持CompanionDeviceManager |
||||
|
*/ |
||||
|
fun isCompanionDeviceManagerSupported(): Boolean { |
||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && companionDeviceManager != null |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取已关联的设备MAC地址 |
||||
|
*/ |
||||
|
fun getPairedMacAddress(): String? { |
||||
|
return sharedPreferences.getString(KEY_PAIRED_MAC, null) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 保存已关联的设备MAC地址 |
||||
|
*/ |
||||
|
fun savePairedMacAddress(macAddress: String) { |
||||
|
sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply() |
||||
|
FileLogger.i(TAG, "已保存配对设备MAC: $macAddress") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清除已关联的设备MAC地址 |
||||
|
*/ |
||||
|
fun clearPairedMacAddress() { |
||||
|
sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply() |
||||
|
FileLogger.i(TAG, "已清除配对设备MAC") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查是否有已关联的设备 |
||||
|
*/ |
||||
|
@RequiresApi(Build.VERSION_CODES.O) |
||||
|
fun hasAssociations(): Boolean { |
||||
|
val associationIds = getAssociationIds() |
||||
|
return associationIds.isNotEmpty() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取所有关联ID |
||||
|
*/ |
||||
|
@RequiresApi(Build.VERSION_CODES.O) |
||||
|
fun getAssociationIds(): List<String> { |
||||
|
// 注意: allAssociations 仅在API 33 (Android 13)及以上版本可用 |
||||
|
// 在早期版本中,使用associations (返回的是String数组) |
||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { |
||||
|
try { |
||||
|
// 由于AssociationInfo类在API 30+引入,使用反射获取更安全 |
||||
|
val method = CompanionDeviceManager::class.java.getMethod("getAllAssociations") |
||||
|
@Suppress("UNCHECKED_CAST") |
||||
|
val associations = method.invoke(companionDeviceManager) as? List<Any> |
||||
|
associations?.map { association -> |
||||
|
// 尝试获取AssociationInfo.id |
||||
|
val idField = association.javaClass.getField("id") |
||||
|
idField.get(association).toString() |
||||
|
} ?: emptyList() |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) |
||||
|
emptyList() |
||||
|
} |
||||
|
} else { |
||||
|
// API 26-32 使用 associations 方法 |
||||
|
companionDeviceManager?.associations?.toList() ?: emptyList() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 开始设备关联流程 |
||||
|
* |
||||
|
* @param activity 当前Activity,用于启动系统关联对话框 |
||||
|
* @param deviceNamePattern 设备名称正则表达式,例如"MyHeadset.*" |
||||
|
* @param serviceUuid 可选的服务UUID |
||||
|
* @param macAddress 可选的MAC地址 |
||||
|
*/ |
||||
|
@RequiresApi(Build.VERSION_CODES.O) |
||||
|
fun startAssociation( |
||||
|
activity: Activity, |
||||
|
deviceNamePattern: String? = null, |
||||
|
serviceUuid: String? = null, |
||||
|
macAddress: String? = null |
||||
|
): Boolean { |
||||
|
if (!isCompanionDeviceManagerSupported()) { |
||||
|
FileLogger.e(TAG, "设备不支持CompanionDeviceManager") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// 构建设备过滤器 |
||||
|
val deviceFilterBuilder = BluetoothDeviceFilter.Builder() |
||||
|
|
||||
|
// 添加设备名称模式 |
||||
|
deviceNamePattern?.let { |
||||
|
deviceFilterBuilder.setNamePattern(Pattern.compile(it)) |
||||
|
} |
||||
|
|
||||
|
// 添加服务UUID |
||||
|
serviceUuid?.let { |
||||
|
try { |
||||
|
val uuid = ParcelUuid(java.util.UUID.fromString(it)) |
||||
|
deviceFilterBuilder.addServiceUuid(uuid, null) |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "设置服务UUID失败: ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 添加MAC地址 - 注意:setAddress方法在不同API版本有差异 |
||||
|
macAddress?.let { |
||||
|
try { |
||||
|
// 检查是否是有效的MAC地址格式 |
||||
|
if (!isValidMacAddress(it)) { |
||||
|
FileLogger.e(TAG, "无效的MAC地址格式: $it") |
||||
|
return@let |
||||
|
} |
||||
|
|
||||
|
// 保存MAC地址,用于后续验证和处理 |
||||
|
savePairedMacAddress(it) |
||||
|
|
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
||||
|
try { |
||||
|
// Android O(8.0)及以上版本都应该使用字符串格式的MAC地址 |
||||
|
deviceFilterBuilder.setAddress(it) |
||||
|
FileLogger.i(TAG, "成功设置MAC地址过滤: $it") |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "setAddress失败: ${e.message}") |
||||
|
// 如果设置MAC地址失败,回退到使用设备名称过滤 |
||||
|
deviceFilterBuilder.setNamePattern(Pattern.compile(".*")) |
||||
|
} |
||||
|
} else { |
||||
|
// Android 8.0以下版本不支持MAC地址过滤,使用设备名称过滤 |
||||
|
FileLogger.w(TAG, "当前Android版本不支持MAC地址过滤,将使用设备名称过滤") |
||||
|
deviceFilterBuilder.setNamePattern(Pattern.compile(".*")) |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "处理MAC地址失败: ${e.message}", e) |
||||
|
// 发生异常时,使用通配名称模式 |
||||
|
deviceFilterBuilder.setNamePattern(Pattern.compile(".*")) |
||||
|
} |
||||
|
} ?: run { |
||||
|
// 如果没有提供MAC地址且没有设置名称模式,则使用通配名称 |
||||
|
if (deviceNamePattern == null) { |
||||
|
deviceFilterBuilder.setNamePattern(Pattern.compile(".*")) |
||||
|
FileLogger.i(TAG, "未提供MAC地址,使用通配名称模式") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 创建请求 |
||||
|
val associationRequest = AssociationRequest.Builder() |
||||
|
.addDeviceFilter(deviceFilterBuilder.build()) |
||||
|
.setSingleDevice(true) // 找到一个即停止 |
||||
|
.build() |
||||
|
|
||||
|
// 发起关联请求 |
||||
|
companionDeviceManager?.associate( |
||||
|
associationRequest, |
||||
|
object : CompanionDeviceManager.Callback() { |
||||
|
override fun onDeviceFound(intentSender: IntentSender) { |
||||
|
try { |
||||
|
activity.startIntentSenderForResult( |
||||
|
intentSender, |
||||
|
REQUEST_CODE_ASSOCIATE, |
||||
|
null, |
||||
|
0, |
||||
|
0, |
||||
|
0 |
||||
|
) |
||||
|
FileLogger.i(TAG, "已启动设备选择对话框") |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// API 26 (Android 8) 使用 onDeviceFound |
||||
|
// API 28+ (Android 9+) 使用 onAssociationPending |
||||
|
@RequiresApi(Build.VERSION_CODES.P) |
||||
|
override fun onAssociationPending(intentSender: IntentSender) { |
||||
|
try { |
||||
|
activity.startIntentSenderForResult( |
||||
|
intentSender, |
||||
|
REQUEST_CODE_ASSOCIATE, |
||||
|
null, |
||||
|
0, |
||||
|
0, |
||||
|
0 |
||||
|
) |
||||
|
FileLogger.i(TAG, "已启动设备选择对话框") |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// API 33+ (Android 13+) 使用 onAssociationCreated |
||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU) |
||||
|
override fun onAssociationCreated(associationInfo: AssociationInfo) { |
||||
|
FileLogger.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}") |
||||
|
// 保存关联ID |
||||
|
try { |
||||
|
val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress") |
||||
|
val mac = macAddressMethod.invoke(associationInfo) as? String |
||||
|
mac?.let { savePairedMacAddress(it) } |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun onFailure(error: CharSequence?) { |
||||
|
FileLogger.e(TAG, "设备关联失败: $error") |
||||
|
} |
||||
|
}, |
||||
|
null |
||||
|
) |
||||
|
|
||||
|
return true |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "启动设备关联失败: ${e.message}", e) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理关联结果 |
||||
|
* |
||||
|
* @param requestCode 请求码 |
||||
|
* @param resultCode 结果码 |
||||
|
* @param data Intent数据 |
||||
|
* @return 是否处理了关联结果 |
||||
|
*/ |
||||
|
@RequiresApi(Build.VERSION_CODES.O) |
||||
|
fun handleAssociationResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { |
||||
|
if (requestCode != REQUEST_CODE_ASSOCIATE) { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
FileLogger.i(TAG, "收到关联结果: resultCode=$resultCode") |
||||
|
|
||||
|
if (resultCode == Activity.RESULT_OK && data != null) { |
||||
|
// 获取选中的蓝牙设备 |
||||
|
val bluetoothDevice: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { |
||||
|
data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE, BluetoothDevice::class.java) |
||||
|
} else { |
||||
|
@Suppress("DEPRECATION") |
||||
|
data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE) |
||||
|
} |
||||
|
|
||||
|
// 获取关联ID (Android 13+) |
||||
|
val associationId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { |
||||
|
try { |
||||
|
// 由于EXTRA_ASSOCIATION_ID常量仅在API 33+可用,直接使用已知的常量字符串值 |
||||
|
// 不再尝试使用反射获取字段 |
||||
|
val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID" |
||||
|
data.getStringExtra(extraAssociationIdKey) |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) |
||||
|
null |
||||
|
} |
||||
|
} else { |
||||
|
null |
||||
|
} |
||||
|
|
||||
|
// 获取并验证MAC地址 |
||||
|
if (bluetoothDevice != null) { |
||||
|
val deviceMac = bluetoothDevice.address |
||||
|
val deviceName = bluetoothDevice.name ?: "未知设备" |
||||
|
|
||||
|
// 验证MAC地址格式 |
||||
|
if (!isValidMacAddress(deviceMac)) { |
||||
|
FileLogger.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 检查是否与预期的MAC地址匹配 |
||||
|
val savedMac = getPairedMacAddress() |
||||
|
if (!savedMac.isNullOrEmpty() && savedMac != deviceMac) { |
||||
|
FileLogger.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($savedMac) 不匹配,但仍会保存") |
||||
|
} |
||||
|
|
||||
|
// 保存新的MAC地址 |
||||
|
savePairedMacAddress(deviceMac) |
||||
|
FileLogger.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId") |
||||
|
|
||||
|
// 关联成功后,自动注册后台扫描 |
||||
|
registerBackgroundScan() |
||||
|
|
||||
|
return true |
||||
|
} else { |
||||
|
FileLogger.e(TAG, "无法获取关联的蓝牙设备信息") |
||||
|
} |
||||
|
} else { |
||||
|
FileLogger.w(TAG, "设备关联被取消或失败: resultCode=$resultCode") |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 注册后台扫描 |
||||
|
* |
||||
|
* @param macAddress 可选的MAC地址,如果不提供则使用已保存的MAC地址 |
||||
|
*/ |
||||
|
fun registerBackgroundScan() { |
||||
|
try { |
||||
|
val mac = getPairedMacAddress() |
||||
|
if (mac.isNullOrEmpty()) { |
||||
|
FileLogger.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager |
||||
|
val bluetoothAdapter = bluetoothManager?.adapter |
||||
|
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner |
||||
|
|
||||
|
if (bluetoothLeScanner == null) { |
||||
|
FileLogger.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 构建过滤器 |
||||
|
val filter = ScanFilter.Builder() |
||||
|
.setDeviceAddress(mac) |
||||
|
.build() |
||||
|
|
||||
|
// 扫描设置 |
||||
|
val settings = ScanSettings.Builder() |
||||
|
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) |
||||
|
.build() |
||||
|
|
||||
|
// 创建PendingIntent |
||||
|
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) |
||||
|
.setClass(context, BleWakeReceiver::class.java) |
||||
|
|
||||
|
val pendingIntent = PendingIntent.getBroadcast( |
||||
|
context, |
||||
|
0, |
||||
|
scanIntent, |
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { |
||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE |
||||
|
} else { |
||||
|
PendingIntent.FLAG_UPDATE_CURRENT |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
// 启动后台扫描 |
||||
|
bluetoothLeScanner.startScan(listOf(filter), settings, pendingIntent) |
||||
|
FileLogger.i(TAG, "已注册后台扫描,目标设备MAC: $mac") |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 取消后台扫描 |
||||
|
*/ |
||||
|
fun unregisterBackgroundScan() { |
||||
|
try { |
||||
|
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager |
||||
|
val bluetoothAdapter = bluetoothManager?.adapter |
||||
|
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner |
||||
|
|
||||
|
if (bluetoothLeScanner == null) { |
||||
|
FileLogger.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 创建PendingIntent |
||||
|
val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) |
||||
|
.setClass(context, BleWakeReceiver::class.java) |
||||
|
|
||||
|
val pendingIntent = PendingIntent.getBroadcast( |
||||
|
context, |
||||
|
0, |
||||
|
scanIntent, |
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { |
||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE |
||||
|
} else { |
||||
|
PendingIntent.FLAG_UPDATE_CURRENT |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
// 停止后台扫描 |
||||
|
bluetoothLeScanner.stopScan(pendingIntent) |
||||
|
FileLogger.i(TAG, "已取消后台扫描") |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "取消后台扫描失败: ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清除所有关联 |
||||
|
*/ |
||||
|
@RequiresApi(Build.VERSION_CODES.O) |
||||
|
fun clearAllAssociations() { |
||||
|
try { |
||||
|
// 清除本地保存的MAC地址 |
||||
|
clearPairedMacAddress() |
||||
|
|
||||
|
// 获取所有关联ID |
||||
|
val associationIds = getAssociationIds() |
||||
|
|
||||
|
// 遍历解除关联 |
||||
|
for (id in associationIds) { |
||||
|
try { |
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { |
||||
|
companionDeviceManager?.disassociate(id) |
||||
|
} else { |
||||
|
companionDeviceManager?.disassociate(id) |
||||
|
} |
||||
|
FileLogger.i(TAG, "已解除设备关联: $id") |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "解除设备关联失败: $id, ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "清除所有关联失败: ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 验证是否是有效的MAC地址格式 |
||||
|
* |
||||
|
* @param macAddress MAC地址字符串 |
||||
|
* @return 是否是有效的MAC地址 |
||||
|
*/ |
||||
|
private fun isValidMacAddress(macAddress: String): Boolean { |
||||
|
// MAC地址格式验证:XX:XX:XX:XX:XX:XX 或 XX-XX-XX-XX-XX-XX |
||||
|
val macPattern = Pattern.compile( |
||||
|
"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$" |
||||
|
) |
||||
|
return macPattern.matcher(macAddress).matches() |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
package com.yunqiinnovation.ble_service |
||||
|
|
||||
|
import android.companion.AssociationInfo |
||||
|
import android.companion.CompanionDeviceService |
||||
|
import android.content.Intent |
||||
|
import android.os.Build |
||||
|
import android.os.IBinder |
||||
|
import androidx.annotation.RequiresApi |
||||
|
import com.yunqiinnovation.deepvoice_utils.FileLogger |
||||
|
|
||||
|
/** |
||||
|
* BleCompanionService |
||||
|
* 用于监听配对设备的出现和消失 |
||||
|
* 注:此服务仅在Android 11 (API 30)及以上版本可用 |
||||
|
*/ |
||||
|
@RequiresApi(Build.VERSION_CODES.R) |
||||
|
class BleCompanionService : CompanionDeviceService() { |
||||
|
private val TAG = "BleCompanionService" |
||||
|
|
||||
|
override fun onCreate() { |
||||
|
super.onCreate() |
||||
|
FileLogger.init(applicationContext) |
||||
|
FileLogger.i(TAG, "BleCompanionService已创建") |
||||
|
} |
||||
|
|
||||
|
override fun onDeviceAppeared(info: AssociationInfo) { |
||||
|
super.onDeviceAppeared(info) |
||||
|
FileLogger.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}") |
||||
|
|
||||
|
// 当配对设备出现时,启动前台服务 |
||||
|
try { |
||||
|
val serviceIntent = Intent().apply { |
||||
|
// 尝试使用BleMediaService,如果不存在则回退到BleService |
||||
|
val serviceClassName = try { |
||||
|
Class.forName("com.yunqiinnovation.agent_service.BleMediaService").name |
||||
|
} catch (e: ClassNotFoundException) { |
||||
|
BleService::class.java.name |
||||
|
} |
||||
|
|
||||
|
setClassName(this@BleCompanionService, serviceClassName) |
||||
|
action = "android.intent.action.DEVICE_APPEARED" |
||||
|
putExtra("assocId", info.id) |
||||
|
putExtra("displayName", info.displayName) |
||||
|
|
||||
|
// 如果设备信息中有MAC地址,也可以传递 |
||||
|
// 使用反射获取MAC地址,因为deviceMacAddress在不同API版本可能不同或不存在 |
||||
|
try { |
||||
|
val macAddressMethod = info.javaClass.getMethod("getDeviceMacAddress") |
||||
|
val mac = macAddressMethod.invoke(info) as? String |
||||
|
mac?.let { putExtra("mac", it) } |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 启动前台服务 |
||||
|
startForegroundService(serviceIntent) |
||||
|
FileLogger.i(TAG, "已启动服务") |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "启动服务失败: ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun onDeviceDisappeared(info: AssociationInfo) { |
||||
|
super.onDeviceDisappeared(info) |
||||
|
FileLogger.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}") |
||||
|
|
||||
|
// 可以选择停止服务或执行其他清理操作 |
||||
|
} |
||||
|
|
||||
|
// 注意: 不需要重写 onBindCompanionDeviceService 和 onUnbindCompanionDeviceService |
||||
|
// 这些方法在 Android 12+ 才添加,我们可以使用父类的默认实现 |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
package com.yunqiinnovation.ble_service |
||||
|
|
||||
|
import android.bluetooth.le.ScanResult |
||||
|
import android.content.BroadcastReceiver |
||||
|
import android.content.Context |
||||
|
import android.content.Intent |
||||
|
import android.os.Build |
||||
|
import com.yunqiinnovation.deepvoice_utils.FileLogger |
||||
|
|
||||
|
/** |
||||
|
* BleWakeReceiver |
||||
|
* 用于接收后台BLE扫描结果,并启动前台服务 |
||||
|
*/ |
||||
|
class BleWakeReceiver : BroadcastReceiver() { |
||||
|
private val TAG = "BleWakeReceiver" |
||||
|
|
||||
|
override fun onReceive(context: Context, intent: Intent) { |
||||
|
FileLogger.init(context) |
||||
|
FileLogger.i(TAG, "收到BLE扫描广播: ${intent.action}") |
||||
|
|
||||
|
if (intent.action == BleConst.ACTION_SCAN_RESULT) { |
||||
|
// 从扫描结果中提取MAC地址 |
||||
|
// val scanResults = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { |
||||
|
// intent.getParcelableArrayListExtra( |
||||
|
// android.bluetooth.le.BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT, |
||||
|
// ScanResult::class.java |
||||
|
// ) |
||||
|
// } else { |
||||
|
// @Suppress("DEPRECATION") |
||||
|
// intent.getParcelableArrayListExtra( |
||||
|
// android.bluetooth.le.BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT |
||||
|
// ) |
||||
|
// } |
||||
|
|
||||
|
|
||||
|
startBleService(context) |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun startBleService(context: Context) { |
||||
|
try { |
||||
|
// 检查是否存在BleMediaService类,如果不存在则使用BleService |
||||
|
val serviceClass = try { |
||||
|
Class.forName("com.yunqiinnovation.agent_service.BleMediaService") |
||||
|
} catch (e: ClassNotFoundException) { |
||||
|
FileLogger.i(TAG, "BleMediaService不存在") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 创建服务Intent,并携带MAC地址 |
||||
|
val serviceIntent = Intent().apply { |
||||
|
setClassName(context, serviceClass.name) |
||||
|
action = BleConst.ACTION_SCAN_RESULT |
||||
|
} |
||||
|
|
||||
|
// 启动前台服务 |
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
||||
|
context.startForegroundService(serviceIntent) |
||||
|
FileLogger.i(TAG, "已启动前台服务: ${serviceClass.name}") |
||||
|
} else { |
||||
|
context.startService(serviceIntent) |
||||
|
FileLogger.i(TAG, "已启动服务: ${serviceClass.name}") |
||||
|
} |
||||
|
} catch (e: Exception) { |
||||
|
FileLogger.e(TAG, "启动BLE服务失败: ${e.message}", e) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
package com.yunqiinnovation.ble_service |
||||
|
|
||||
|
import android.content.* |
||||
|
import android.os.Build |
||||
|
import android.util.Log |
||||
|
/** |
||||
|
* 系统启动接收器 |
||||
|
* 当系统重启完成后,自动启动BLE前台服务 |
||||
|
* 注意:需要用户在设置中授予应用自启动权限才能生效 |
||||
|
*/ |
||||
|
class BootReceiver : BroadcastReceiver() { |
||||
|
private val TAG = "BootReceiver" |
||||
|
override fun onReceive(ctx: Context, i: Intent) { |
||||
|
val serviceClass = try { |
||||
|
Class.forName("com.yunqiinnovation.agent_service.BleMediaService") |
||||
|
} catch (e: ClassNotFoundException) { |
||||
|
Log.i(TAG, "BleMediaService不存在") |
||||
|
return |
||||
|
} |
||||
|
if (i.action == Intent.ACTION_BOOT_COMPLETED) { |
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) |
||||
|
ctx.startForegroundService(Intent(ctx, serviceClass)) |
||||
|
else ctx.startService(Intent(ctx, serviceClass)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
Binary file not shown.
@ -1,2 +1,2 @@ |
|||||
#Fri May 09 15:56:53 IST 2025 |
#Sat May 10 17:59:51 IST 2025 |
||||
gradle.version=8.10 |
gradle.version=8.10 |
||||
|
|||||
Loading…
Reference in new issue