25 changed files with 1306 additions and 348 deletions
@ -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