feat: 系统控制模块 - 硬件抽象层
新增: - ScreenController 屏幕控制接口 + FiseScreenController 实现(ROM广播) - NfcController NFC控制接口 + FiseNfcController 实现(sysfs读写) - VibrationController 振动接口 + FiseVibrationController 实现(13种方案+音频) - SystemStateMonitor 系统状态监听(电量、蓝牙状态广播) - DeviceModule Hilt硬件抽象绑定 - 8个音频文件(res/raw/) - AppEvent 新增4个系统状态事件 修改: - MainActivity 注册 SystemStateMonitor - HomeFragment 硬件验证demo(熄屏/振动/NFC/电量实时显示) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,14 +6,23 @@ import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.xiaoqu.watch.databinding.ActivityMainBinding
|
||||
import com.xiaoqu.watch.service.manager.SystemStateMonitor
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 主 Activity(Launcher 模式,单 Activity + 多 Fragment 架构)
|
||||
* 职责:全屏设置、物理返回键拦截、系统状态监听注册
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
/** 系统状态监听器(电量、蓝牙状态) */
|
||||
@Inject lateinit var systemStateMonitor: SystemStateMonitor
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -33,9 +42,18 @@ class MainActivity : AppCompatActivity() {
|
||||
// 拦截物理返回键
|
||||
setupBackButton()
|
||||
|
||||
// 注册系统状态监听(电量、蓝牙)
|
||||
systemStateMonitor.register()
|
||||
|
||||
Timber.d("MainActivity created")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// 取消系统状态监听
|
||||
systemStateMonitor.unregister()
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理返回键拦截:
|
||||
* - 已绑定用户 → 开启 NFC 打卡模式(后续模块实现)
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.xiaoqu.watch.device.nfc
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* FISE 定制 ROM NFC/RFID 控制实现
|
||||
* 通过 sysfs 文件读写控制 RFID 模块(FISE ROM 特有)
|
||||
*
|
||||
* 硬件接口:
|
||||
* - 上电:读 /sys/bus/i2c/drivers/rfid/1-0050/power_on
|
||||
* - 断电:读 /sys/bus/i2c/drivers/rfid/1-0050/power_off
|
||||
* - 读卡:读 /sys/bus/i2c/drivers/rfid/1-0050/typeA_id(轮询)
|
||||
*/
|
||||
@Singleton
|
||||
class FiseNfcController @Inject constructor() : NfcController {
|
||||
|
||||
companion object {
|
||||
/** RFID 上电文件路径 */
|
||||
private const val RFID_POWER_ON = "/sys/bus/i2c/drivers/rfid/1-0050/power_on"
|
||||
/** RFID 断电文件路径 */
|
||||
private const val RFID_POWER_OFF = "/sys/bus/i2c/drivers/rfid/1-0050/power_off"
|
||||
/** RFID 读卡文件路径 */
|
||||
private const val RFID_TYPEA_ID = "/sys/bus/i2c/drivers/rfid/1-0050/typeA_id"
|
||||
/** 轮询间隔(毫秒) */
|
||||
private const val SCAN_INTERVAL_MS = 1000L
|
||||
}
|
||||
|
||||
/** NFC 开关状态 */
|
||||
private var _isOpen = false
|
||||
|
||||
/** 轮询协程 Job */
|
||||
private var scanJob: Job? = null
|
||||
|
||||
/** 协程作用域 */
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
override fun isOpen(): Boolean = _isOpen
|
||||
|
||||
/** 开启 NFC:读 power_on 文件触发硬件上电 */
|
||||
override fun open() {
|
||||
Timber.d("NFC控制: 开启")
|
||||
readFile(RFID_POWER_ON)
|
||||
_isOpen = true
|
||||
}
|
||||
|
||||
/** 关闭 NFC:读 power_off 文件触发硬件断电,停止轮询 */
|
||||
override fun close() {
|
||||
Timber.d("NFC控制: 关闭")
|
||||
stopScan()
|
||||
readFile(RFID_POWER_OFF)
|
||||
_isOpen = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始轮询读卡
|
||||
* 每 1 秒读一次 typeA_id 文件,解析出卡号后回调
|
||||
* @param callback 读到卡后的回调,参数为 nfcId
|
||||
*/
|
||||
override fun startScan(callback: (nfcId: String) -> Unit) {
|
||||
// 先停止已有的轮询
|
||||
stopScan()
|
||||
|
||||
Timber.d("NFC控制: 开始轮询读卡")
|
||||
scanJob = scope.launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
val content = readFile(RFID_TYPEA_ID)
|
||||
if (content.isNotEmpty()) {
|
||||
// 解析卡号:取第 12 位之后的内容,去除空格
|
||||
// 对应旧版:typeaId.substring(12).replace(/\s*/g, "")
|
||||
val nfcId = if (content.length > 12) {
|
||||
content.substring(12).replace("\\s+".toRegex(), "")
|
||||
} else {
|
||||
content.replace("\\s+".toRegex(), "")
|
||||
}
|
||||
|
||||
if (nfcId.isNotEmpty()) {
|
||||
Timber.d("NFC控制: 读到卡号 $nfcId")
|
||||
// 切回主线程回调
|
||||
withContext(Dispatchers.Main) {
|
||||
callback(nfcId)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "NFC控制: 读卡异常")
|
||||
}
|
||||
delay(SCAN_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止轮询 */
|
||||
override fun stopScan() {
|
||||
scanJob?.cancel()
|
||||
scanJob = null
|
||||
Timber.d("NFC控制: 停止轮询")
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 sysfs 文件内容
|
||||
* @param path 文件路径
|
||||
* @return 文件内容(读取失败返回空字符串)
|
||||
*/
|
||||
private fun readFile(path: String): String {
|
||||
return try {
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
Timber.w("NFC控制: 文件不存在 $path")
|
||||
return ""
|
||||
}
|
||||
BufferedReader(FileReader(file)).use { reader ->
|
||||
reader.readLine() ?: ""
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "NFC控制: 读取文件失败 $path")
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.xiaoqu.watch.device.nfc
|
||||
|
||||
/**
|
||||
* NFC/RFID 控制接口(硬件抽象层)
|
||||
* 封装 NFC 开关和读卡操作,换设备时只需替换实现类
|
||||
*/
|
||||
interface NfcController {
|
||||
/** NFC 是否已开启 */
|
||||
fun isOpen(): Boolean
|
||||
/** 开启 NFC 电源 */
|
||||
fun open()
|
||||
/** 关闭 NFC 电源 */
|
||||
fun close()
|
||||
/** 开始轮询读卡,读到卡后回调 nfcId */
|
||||
fun startScan(callback: (nfcId: String) -> Unit)
|
||||
/** 停止轮询 */
|
||||
fun stopScan()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.xiaoqu.watch.device.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.PowerManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* FISE 定制 ROM 屏幕控制实现
|
||||
* 通过系统广播控制亮屏/熄屏(FISE ROM 特有)
|
||||
*/
|
||||
@Singleton
|
||||
class FiseScreenController @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) : ScreenController {
|
||||
|
||||
companion object {
|
||||
/** FISE ROM 亮屏广播 */
|
||||
private const val ACTION_SCREEN_ON = "com.fise.turn_screen_on"
|
||||
/** FISE ROM 熄屏广播 */
|
||||
private const val ACTION_SCREEN_OFF = "com.fise.turn_screen_off"
|
||||
}
|
||||
|
||||
/** 获取屏幕是否亮着(通过 PowerManager 标准 API) */
|
||||
override fun isScreenOn(): Boolean {
|
||||
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return pm.isScreenOn
|
||||
}
|
||||
|
||||
/** 亮屏:发送 FISE ROM 自定义广播 */
|
||||
override fun turnOn() {
|
||||
Timber.d("屏幕控制: 亮屏")
|
||||
context.sendBroadcast(Intent(ACTION_SCREEN_ON))
|
||||
}
|
||||
|
||||
/** 熄屏:发送 FISE ROM 自定义广播 */
|
||||
override fun turnOff() {
|
||||
Timber.d("屏幕控制: 熄屏")
|
||||
context.sendBroadcast(Intent(ACTION_SCREEN_OFF))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xiaoqu.watch.device.screen
|
||||
|
||||
/**
|
||||
* 屏幕控制接口(硬件抽象层)
|
||||
* 封装亮屏/熄屏操作,换设备时只需替换实现类
|
||||
*/
|
||||
interface ScreenController {
|
||||
/** 获取屏幕是否亮着 */
|
||||
fun isScreenOn(): Boolean
|
||||
/** 亮屏 */
|
||||
fun turnOn()
|
||||
/** 熄屏 */
|
||||
fun turnOff()
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.xiaoqu.watch.device.sensor
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Vibrator
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* FISE 振动控制实现
|
||||
* <20><><EFBFBD>持简单振动和方案振动(嵌套循环 + 音频反馈)
|
||||
*
|
||||
* 振动算法(与旧版 shockStore.js 一致):
|
||||
* 外层循环 shockCycleTimes 次 {
|
||||
* 内层循环 shockTimes 次 {
|
||||
* 振动 shockTime 秒
|
||||
* 休息 shockIntervalTime 秒
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@Singleton
|
||||
class FiseVibrationController @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) : VibrationController {
|
||||
|
||||
/** 系统振动器 */
|
||||
@Suppress("DEPRECATION")
|
||||
private val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
/** 当前振动协程 Job */
|
||||
private var patternJob: Job? = null
|
||||
|
||||
/** 协程作用域 */
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
/** 当<><E5BD93> MediaPlayer(播放完毕后释<E5908E><E9878A>) */
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
|
||||
/** 执行一次<E4B880><E6ACA1><EFBFBD>单振动 */
|
||||
@Suppress("DEPRECATION")
|
||||
override fun vibrate(durationMs: Long) {
|
||||
Timber.d("振动: ${durationMs}ms")
|
||||
vibrator.vibrate(durationMs)
|
||||
}
|
||||
|
||||
/**
|
||||
* 按方案执行<E689A7><E8A18C>动(<E58AA8><EFBC88><EFBFBD>循环和音频反馈)
|
||||
* @param pattern 振动方案
|
||||
*/
|
||||
override fun executePattern(pattern: VibrationPattern) {
|
||||
// 先停止之前的振动
|
||||
stop()
|
||||
|
||||
Timber.d("振动方案: ${pattern.planName}(planId=${pattern.planId})")
|
||||
|
||||
// 播放音频(如果启用且有音频资源)
|
||||
if (pattern.voiceState && pattern.audioResId != 0) {
|
||||
playAudio(pattern.audioResId)
|
||||
}
|
||||
|
||||
// 执行振动(如果启用)
|
||||
if (pattern.shockState) {
|
||||
patternJob = scope.launch {
|
||||
executePatternLoop(pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止当前振动和音频 */
|
||||
override fun stop() {
|
||||
// 停止振动<E68CAF><E58AA8>程
|
||||
patternJob?.cancel()
|
||||
patternJob = null
|
||||
// 停止硬件振动
|
||||
vibrator.cancel()
|
||||
// 停止音频
|
||||
releaseMediaPlayer()
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行振动方案的嵌套循环
|
||||
* 外层:shockCycleTimes 次
|
||||
* 内层:shockTimes 次振动 + 休息
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
private suspend fun executePatternLoop(pattern: VibrationPattern) {
|
||||
val vibrateDurationMs = pattern.shockTime * 1000L
|
||||
val intervalMs = pattern.shockIntervalTime * 1000L
|
||||
|
||||
// 外层循环:频次循环
|
||||
for (cycle in 0 until pattern.shockCycleTimes) {
|
||||
// 内层循环:每频次振动次数
|
||||
for (times in 0 until pattern.shockTimes) {
|
||||
// 振动
|
||||
vibrator.vibrate(vibrateDurationMs)
|
||||
// 等待振动<E68CAF><E58AA8>成 + 休息间隔
|
||||
delay(vibrateDurationMs + intervalMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放音频反馈
|
||||
* @param resId 音频资源 ID(R.raw.xxx)
|
||||
*/
|
||||
private fun playAudio(resId: Int) {
|
||||
try {
|
||||
// 释放上一个 MediaPlayer
|
||||
releaseMediaPlayer()
|
||||
// 创建并播放
|
||||
mediaPlayer = MediaPlayer.create(context, resId)?.apply {
|
||||
setOnCompletionListener {
|
||||
// 播放完毕自动释放
|
||||
it.release()
|
||||
mediaPlayer = null
|
||||
}
|
||||
start()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "振动: 音频播放失败")
|
||||
}
|
||||
}
|
||||
|
||||
/** 释放 MediaPlayer 防止内存泄漏 */
|
||||
private fun releaseMediaPlayer() {
|
||||
try {
|
||||
mediaPlayer?.release()
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
mediaPlayer = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.xiaoqu.watch.device.sensor
|
||||
|
||||
/**
|
||||
* 振动控制接口(硬件抽象层)
|
||||
* 封装简单振动和方案振动(含循环+音频)
|
||||
*/
|
||||
interface VibrationController {
|
||||
/** 执行一次简单振动 */
|
||||
fun vibrate(durationMs: Long)
|
||||
/** 按方案执行振动(含循环和音频反馈) */
|
||||
fun executePattern(pattern: VibrationPattern)
|
||||
/** 停止当前振动 */
|
||||
fun stop()
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.xiaoqu.watch.device.sensor
|
||||
|
||||
import com.xiaoqu.watch.R
|
||||
|
||||
/**
|
||||
* 13 种默认振动方案
|
||||
* 对应旧版 shockStore.js 中的 shockParams
|
||||
* 参数可被服务端下发覆盖
|
||||
*/
|
||||
object VibrationDefaults {
|
||||
|
||||
/** 根据 planId 获取默认振动方案 */
|
||||
fun getPattern(planId: Int): VibrationPattern? = patterns[planId]
|
||||
|
||||
/** 所有默认方案 */
|
||||
val patterns: Map<Int, VibrationPattern> = mapOf(
|
||||
// planId 2: 新消息
|
||||
2 to VibrationPattern(
|
||||
planId = 2, planName = "新消息",
|
||||
shockTime = 1, shockTimes = 2, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = 0
|
||||
),
|
||||
// planId 3: 未读提醒
|
||||
3 to VibrationPattern(
|
||||
planId = 3, planName = "未读提醒",
|
||||
shockTime = 1, shockTimes = 3, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = 0
|
||||
),
|
||||
// planId 4: 单次打卡成功
|
||||
4 to VibrationPattern(
|
||||
planId = 4, planName = "单次打卡成功",
|
||||
shockTime = 1, shockTimes = 1, shockIntervalTime = 0, shockCycleTimes = 1,
|
||||
audioResId = R.raw.punch_success
|
||||
),
|
||||
// planId 5: 批量打卡成功
|
||||
5 to VibrationPattern(
|
||||
planId = 5, planName = "批量打卡成功",
|
||||
shockTime = 1, shockTimes = 2, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = R.raw.punch_success
|
||||
),
|
||||
// planId 6: 进入打卡范围
|
||||
6 to VibrationPattern(
|
||||
planId = 6, planName = "进入打卡范围",
|
||||
shockTime = 1, shockTimes = 1, shockIntervalTime = 0, shockCycleTimes = 1,
|
||||
audioResId = R.raw.open_success
|
||||
),
|
||||
// planId 7: 打卡失败
|
||||
7 to VibrationPattern(
|
||||
planId = 7, planName = "打卡失败",
|
||||
shockTime = 1, shockTimes = 3, shockIntervalTime = 1, shockCycleTimes = 2,
|
||||
audioResId = 0
|
||||
),
|
||||
// planId 8: NFC 开启
|
||||
8 to VibrationPattern(
|
||||
planId = 8, planName = "NFC开启",
|
||||
shockTime = 1, shockTimes = 1, shockIntervalTime = 0, shockCycleTimes = 1,
|
||||
audioResId = R.raw.open_punch
|
||||
),
|
||||
// planId 9: NFC 关闭
|
||||
9 to VibrationPattern(
|
||||
planId = 9, planName = "NFC关闭",
|
||||
shockTime = 1, shockTimes = 1, shockIntervalTime = 0, shockCycleTimes = 1,
|
||||
audioResId = R.raw.close_punch
|
||||
),
|
||||
// planId 10: 离线
|
||||
10 to VibrationPattern(
|
||||
planId = 10, planName = "离线",
|
||||
shockTime = 1, shockTimes = 2, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = R.raw.offline
|
||||
),
|
||||
// planId 11: 开门失败
|
||||
11 to VibrationPattern(
|
||||
planId = 11, planName = "开门失败",
|
||||
shockTime = 1, shockTimes = 2, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = R.raw.open_failed
|
||||
),
|
||||
// planId 12: 无权限开门
|
||||
12 to VibrationPattern(
|
||||
planId = 12, planName = "无权限开门",
|
||||
shockTime = 1, shockTimes = 2, shockIntervalTime = 1, shockCycleTimes = 1,
|
||||
audioResId = R.raw.no_auth_open
|
||||
),
|
||||
// planId 13: 正在开锁
|
||||
13 to VibrationPattern(
|
||||
planId = 13, planName = "正在开锁",
|
||||
shockTime = 1, shockTimes = 1, shockIntervalTime = 0, shockCycleTimes = 1,
|
||||
audioResId = R.raw.open_locking
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.xiaoqu.watch.device.sensor
|
||||
|
||||
/**
|
||||
* 振动方案数据类
|
||||
* 对应旧版 shockStore.js 中的振动参数
|
||||
*
|
||||
* 振动算法:外层循环 shockCycleTimes 次,每次内层振动 shockTimes 次,
|
||||
* 每次振动 shockTime 秒,振动间休息 shockIntervalTime 秒
|
||||
*/
|
||||
data class VibrationPattern(
|
||||
/** 方案 ID(2-13) */
|
||||
val planId: Int,
|
||||
/** 方案名称 */
|
||||
val planName: String,
|
||||
/** 单次振动时长(秒) */
|
||||
val shockTime: Int,
|
||||
/** 每频次振动次数 */
|
||||
val shockTimes: Int,
|
||||
/** 振动间休息时长(秒) */
|
||||
val shockIntervalTime: Int,
|
||||
/** 频次循环次数 */
|
||||
val shockCycleTimes: Int,
|
||||
/** 是否启用振动 */
|
||||
val shockState: Boolean = true,
|
||||
/** 是否启用声音 */
|
||||
val voiceState: Boolean = true,
|
||||
/** 音频资源 ID(R.raw.xxx),0 表示无音频 */
|
||||
val audioResId: Int = 0
|
||||
)
|
||||
38
app/src/main/java/com/xiaoqu/watch/di/DeviceModule.kt
Normal file
38
app/src/main/java/com/xiaoqu/watch/di/DeviceModule.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.xiaoqu.watch.di
|
||||
|
||||
import com.xiaoqu.watch.device.nfc.FiseNfcController
|
||||
import com.xiaoqu.watch.device.nfc.NfcController
|
||||
import com.xiaoqu.watch.device.screen.FiseScreenController
|
||||
import com.xiaoqu.watch.device.screen.ScreenController
|
||||
import com.xiaoqu.watch.device.sensor.FiseVibrationController
|
||||
import com.xiaoqu.watch.device.sensor.VibrationController
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* 硬件抽象层 Hilt 绑定模块
|
||||
* 将接口绑定到 FISE ROM 实现类
|
||||
* 换设备时只需修改此文件的绑定
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class DeviceModule {
|
||||
|
||||
/** 屏幕控制:FISE ROM 广播实现 */
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindScreenController(impl: FiseScreenController): ScreenController
|
||||
|
||||
/** NFC 控制:FISE ROM sysfs 实现 */
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindNfcController(impl: FiseNfcController): NfcController
|
||||
|
||||
/** 振动控制:标准 Vibrator + MediaPlayer 实现 */
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindVibrationController(impl: FiseVibrationController): VibrationController
|
||||
}
|
||||
@@ -17,4 +17,10 @@ sealed class AppEvent {
|
||||
data object DeviceUnbound : AppEvent()
|
||||
data object BindSuccess : AppEvent()
|
||||
data class WorkStateChanged(val isWorking: Boolean) : AppEvent()
|
||||
|
||||
// 系统状态监听
|
||||
data class BatteryChanged(val level: Int, val isCharging: Boolean) : AppEvent()
|
||||
data class BluetoothStateChanged(val isOn: Boolean) : AppEvent()
|
||||
data class BluetoothDeviceConnected(val deviceName: String) : AppEvent()
|
||||
data class BluetoothDeviceDisconnected(val deviceName: String) : AppEvent()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.xiaoqu.watch.service.manager
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.BatteryManager
|
||||
import com.xiaoqu.watch.event.AppEvent
|
||||
import com.xiaoqu.watch.event.EventBus
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* 系统状态监听器
|
||||
* 注册 BroadcastReceiver 监听电量、蓝牙状态变化,通过 EventBus 分发事件
|
||||
*
|
||||
* 使用方式:
|
||||
* - MainActivity.onCreate 中调用 register()
|
||||
* - MainActivity.onDestroy 中调用 unregister()
|
||||
*/
|
||||
@Singleton
|
||||
class SystemStateMonitor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val eventBus: EventBus
|
||||
) {
|
||||
/** 协程作用域(用于发送事件) */
|
||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
/** 是否已注册 */
|
||||
private var registered = false
|
||||
|
||||
/** 广播接收器 */
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
// 电量变化
|
||||
Intent.ACTION_BATTERY_CHANGED -> handleBatteryChanged(intent)
|
||||
// 蓝牙开关状态变化
|
||||
BluetoothAdapter.ACTION_STATE_CHANGED -> handleBluetoothStateChanged(intent)
|
||||
// 蓝牙设备连接
|
||||
BluetoothDevice.ACTION_ACL_CONNECTED -> handleBluetoothConnected(intent)
|
||||
// 蓝牙设备断开
|
||||
BluetoothDevice.ACTION_ACL_DISCONNECTED -> handleBluetoothDisconnected(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 注册广播监听 */
|
||||
fun register() {
|
||||
if (registered) return
|
||||
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_BATTERY_CHANGED)
|
||||
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||
}
|
||||
context.registerReceiver(receiver, filter)
|
||||
registered = true
|
||||
Timber.d("系统状态监听: 已注册")
|
||||
}
|
||||
|
||||
/** 取消注册 */
|
||||
fun unregister() {
|
||||
if (!registered) return
|
||||
|
||||
try {
|
||||
context.unregisterReceiver(receiver)
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "系统状态监听: 取消注册异常")
|
||||
}
|
||||
registered = false
|
||||
Timber.d("系统状态监听: 已取消注册")
|
||||
}
|
||||
|
||||
/** 处理电量变化广播 */
|
||||
private fun handleBatteryChanged(intent: Intent) {
|
||||
// 电量百分比 = level / scale * 100
|
||||
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
|
||||
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)
|
||||
val percent = (level * 100) / scale
|
||||
|
||||
// 充电状态
|
||||
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING
|
||||
|| status == BatteryManager.BATTERY_STATUS_FULL
|
||||
|
||||
emitEvent(AppEvent.BatteryChanged(percent, isCharging))
|
||||
}
|
||||
|
||||
/** 处理蓝牙开关状态变化 */
|
||||
private fun handleBluetoothStateChanged(intent: Intent) {
|
||||
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
|
||||
val isOn = state == BluetoothAdapter.STATE_ON
|
||||
Timber.d("系统状态监听: 蓝牙状态 isOn=$isOn")
|
||||
emitEvent(AppEvent.BluetoothStateChanged(isOn))
|
||||
}
|
||||
|
||||
/** 处理蓝牙设备连接 */
|
||||
private fun handleBluetoothConnected(intent: Intent) {
|
||||
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
|
||||
val name = device?.name ?: "未知设备"
|
||||
Timber.d("系统状态监听: 蓝牙已连接 $name")
|
||||
emitEvent(AppEvent.BluetoothDeviceConnected(name))
|
||||
}
|
||||
|
||||
/** 处理蓝牙设备断开 */
|
||||
private fun handleBluetoothDisconnected(intent: Intent) {
|
||||
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
|
||||
val name = device?.name ?: "未知设备"
|
||||
Timber.d("系统状态监听: 蓝牙已断开 $name")
|
||||
emitEvent(AppEvent.BluetoothDeviceDisconnected(name))
|
||||
}
|
||||
|
||||
/** 通过 EventBus 发送事件 */
|
||||
private fun emitEvent(event: AppEvent) {
|
||||
scope.launch {
|
||||
eventBus.emit(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,34 +5,50 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.xiaoqu.watch.R
|
||||
import com.xiaoqu.watch.data.prefs.DevicePrefs
|
||||
import com.xiaoqu.watch.data.prefs.UserPrefs
|
||||
import com.xiaoqu.watch.databinding.FragmentHomeBinding
|
||||
import com.xiaoqu.watch.device.nfc.NfcController
|
||||
import com.xiaoqu.watch.device.screen.ScreenController
|
||||
import com.xiaoqu.watch.device.sensor.VibrationController
|
||||
import com.xiaoqu.watch.device.sensor.VibrationDefaults
|
||||
import com.xiaoqu.watch.event.AppEvent
|
||||
import com.xiaoqu.watch.event.EventBus
|
||||
import com.xiaoqu.watch.ui.common.BaseFragment
|
||||
import com.xiaoqu.watch.ui.widget.NavBarHelper
|
||||
import com.xiaoqu.watch.ui.widget.QuConfirmDialog
|
||||
import com.xiaoqu.watch.ui.widget.QuTipDialog
|
||||
import com.xiaoqu.watch.util.DateUtil
|
||||
import com.xiaoqu.watch.util.DeviceUtil
|
||||
import com.xiaoqu.watch.util.NetworkUtil
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 首页 Fragment
|
||||
* 当前为 UI 组件 demo 页面,展示 NavBar、按钮样式、弹窗组件
|
||||
* 当前为硬件验证 demo 页面,展示系统状态 + 硬件控制测试按钮
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
||||
|
||||
@Inject lateinit var devicePrefs: DevicePrefs
|
||||
@Inject lateinit var userPrefs: UserPrefs
|
||||
@Inject lateinit var screenController: ScreenController
|
||||
@Inject lateinit var nfcController: NfcController
|
||||
@Inject lateinit var vibrationController: VibrationController
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
|
||||
/** 提示弹窗(挂载到 Activity 的 dialog_container) */
|
||||
/** 提示弹窗 */
|
||||
private lateinit var tipDialog: QuTipDialog
|
||||
/** 确认弹窗 */
|
||||
private lateinit var confirmDialog: QuConfirmDialog
|
||||
|
||||
/** NFC 是否正在扫描 */
|
||||
private var nfcScanning = false
|
||||
|
||||
/** 当前电量和充电状态 */
|
||||
private var batteryLevel = -1
|
||||
private var batteryCharging = false
|
||||
|
||||
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding {
|
||||
return FragmentHomeBinding.inflate(inflater, container, false)
|
||||
@@ -44,19 +60,30 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
||||
// 初始化设备信息
|
||||
initDevicePrefs()
|
||||
|
||||
// 设置 NavBar 为首页模式(状态图标 + 时间 + 电量)
|
||||
// 设置 NavBar 为首页模式
|
||||
NavBarHelper.setupHomePage(binding.root)
|
||||
|
||||
// 初始化弹窗(使用 Activity 的全局弹窗容器)
|
||||
// 初始化弹窗
|
||||
val dialogContainer = requireActivity().findViewById<FrameLayout>(R.id.dialog_container)
|
||||
tipDialog = QuTipDialog(dialogContainer)
|
||||
confirmDialog = QuConfirmDialog(dialogContainer)
|
||||
|
||||
// 显示 demo 信息
|
||||
showDemoInfo()
|
||||
// 显示状态信息
|
||||
updateStatus()
|
||||
|
||||
// 绑定按钮事件
|
||||
// 绑定测试按钮
|
||||
setupButtons()
|
||||
|
||||
// 监听系统状态事件
|
||||
observeSystemEvents()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
// 停止 NFC 扫描
|
||||
if (nfcScanning) {
|
||||
nfcController.stopScan()
|
||||
nfcScanning = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 首次启动时初始化设备信息到 device_prefs */
|
||||
@@ -64,58 +91,111 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
||||
if (!devicePrefs.isInitialized) {
|
||||
val info = DeviceUtil.getDeviceInfo(requireContext())
|
||||
devicePrefs.saveDeviceInfo(
|
||||
imei = info.imei,
|
||||
serial = info.serial,
|
||||
bluetoothName = info.bluetoothName,
|
||||
bluetoothMac = info.bluetoothMac,
|
||||
brand = info.brand,
|
||||
model = info.model,
|
||||
osVersion = info.osVersion,
|
||||
totalMemory = info.totalMemory
|
||||
imei = info.imei, serial = info.serial,
|
||||
bluetoothName = info.bluetoothName, bluetoothMac = info.bluetoothMac,
|
||||
brand = info.brand, model = info.model,
|
||||
osVersion = info.osVersion, totalMemory = info.totalMemory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示设备和网络基本信息 */
|
||||
private fun showDemoInfo() {
|
||||
/** 更新状态信息显示 */
|
||||
private fun updateStatus() {
|
||||
val dateInfo = DateUtil.getDateInfo()
|
||||
val sb = StringBuilder()
|
||||
sb.appendLine("${dateInfo.date} ${dateInfo.week}")
|
||||
sb.appendLine("${dateInfo.date} ${dateInfo.week} ${dateInfo.time}")
|
||||
sb.appendLine("设备: ${devicePrefs.brand} ${devicePrefs.model}")
|
||||
sb.appendLine("网络: ${NetworkUtil.getNetworkTypeName(requireContext())}")
|
||||
sb.appendLine("绑定: ${if (userPrefs.isBound) "是" else "否"}")
|
||||
binding.tvDemoInfo.text = sb.toString()
|
||||
// 电量信息(等收到广播后更新)
|
||||
if (batteryLevel >= 0) {
|
||||
sb.appendLine("电量: ${batteryLevel}% ${if (batteryCharging) "(充电中)" else ""}")
|
||||
}
|
||||
sb.appendLine("屏幕: ${if (screenController.isScreenOn()) "亮" else "灭"}")
|
||||
sb.appendLine("NFC: ${if (nfcController.isOpen()) "开" else "关"}")
|
||||
binding.tvStatus.text = sb.toString()
|
||||
}
|
||||
|
||||
/** 绑定按钮点击事件 */
|
||||
/** 绑定测试按钮 */
|
||||
private fun setupButtons() {
|
||||
// 显示提示弹窗(成功状态,3 秒倒计时后自动关闭)
|
||||
// 熄屏测试:熄屏 3 秒后自动亮屏
|
||||
binding.btnScreenOff.setOnClickListener {
|
||||
screenController.turnOff()
|
||||
binding.root.postDelayed({
|
||||
screenController.turnOn()
|
||||
updateStatus()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 振动测试:执行 planId=4(打卡成功)方案
|
||||
binding.btnVibrate.setOnClickListener {
|
||||
val pattern = VibrationDefaults.getPattern(4)
|
||||
if (pattern != null) {
|
||||
vibrationController.executePattern(pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// NFC 读卡测试:开启/关闭切换
|
||||
binding.btnNfcScan.setOnClickListener {
|
||||
if (nfcScanning) {
|
||||
// 停止扫描
|
||||
nfcController.stopScan()
|
||||
nfcController.close()
|
||||
nfcScanning = false
|
||||
binding.btnNfcScan.text = "NFC 读卡测试"
|
||||
updateStatus()
|
||||
} else {
|
||||
// 开始扫描
|
||||
nfcController.open()
|
||||
nfcController.startScan { nfcId ->
|
||||
// 读到卡号,显示提示
|
||||
tipDialog.show(
|
||||
status = QuTipDialog.Status.SUCCESS,
|
||||
title = "读到NFC卡",
|
||||
desc = "卡号: $nfcId",
|
||||
back = true, step = 0, countdown = 3
|
||||
)
|
||||
}
|
||||
nfcScanning = true
|
||||
binding.btnNfcScan.text = "停止 NFC 扫描"
|
||||
updateStatus()
|
||||
}
|
||||
}
|
||||
|
||||
// 提示弹窗测试
|
||||
binding.btnShowTip.setOnClickListener {
|
||||
tipDialog.show(
|
||||
status = QuTipDialog.Status.SUCCESS,
|
||||
title = "操作成功",
|
||||
desc = "这是一个提示弹窗 demo",
|
||||
back = true,
|
||||
step = 0, // 只关闭,不返回
|
||||
countdown = 3
|
||||
)
|
||||
}
|
||||
|
||||
// 显示确认弹窗
|
||||
binding.btnShowConfirm.setOnClickListener {
|
||||
confirmDialog.showText(
|
||||
text = "确认执行此操作?",
|
||||
onConfirm = {
|
||||
// 确认后显示成功提示
|
||||
tipDialog.show(
|
||||
status = QuTipDialog.Status.SUCCESS,
|
||||
title = "已确认",
|
||||
back = true,
|
||||
step = 0,
|
||||
countdown = 2
|
||||
)
|
||||
}
|
||||
back = true, step = 0, countdown = 3
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听系统状态事件(电量、蓝牙)<E78999><EFBC89><EFBFBD>更新 NavBar 和状态显示 */
|
||||
private fun observeSystemEvents() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
eventBus.events.collect { event ->
|
||||
when (event) {
|
||||
// 电量变化:更新 NavBar 电量图标 + 状态文字
|
||||
is AppEvent.BatteryChanged -> {
|
||||
batteryLevel = event.level
|
||||
batteryCharging = event.isCharging
|
||||
NavBarHelper.updateBattery(binding.root, event.level, event.isCharging)
|
||||
updateStatus()
|
||||
}
|
||||
// 蓝牙开关:更新 NavBar 蓝牙图标
|
||||
is AppEvent.BluetoothStateChanged -> {
|
||||
NavBarHelper.updateBluetooth(binding.root, event.isOn)
|
||||
updateStatus()
|
||||
}
|
||||
// 蓝牙连接/断开
|
||||
is AppEvent.BluetoothDeviceConnected -> updateStatus()
|
||||
is AppEvent.BluetoothDeviceDisconnected -> updateStatus()
|
||||
else -> {} // 其他事件不处理
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user