From c7ae11a574d1e9d2e10d6b3fe9f7064e1d67e660 Mon Sep 17 00:00:00 2001 From: dongliang Date: Mon, 27 Apr 2026 13:50:25 +0930 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B3=BB=E7=BB=9F=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=20-=20=E7=A1=AC=E4=BB=B6=E6=8A=BD=E8=B1=A1?= =?UTF-8?q?=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增: - 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) --- .../java/com/xiaoqu/watch/app/MainActivity.kt | 18 ++ .../watch/device/nfc/FiseNfcController.kt | 126 +++++++++++++ .../xiaoqu/watch/device/nfc/NfcController.kt | 18 ++ .../device/screen/FiseScreenController.kt | 44 +++++ .../watch/device/screen/ScreenController.kt | 14 ++ .../device/sensor/FiseVibrationController.kt | 136 ++++++++++++++ .../device/sensor/VibrationController.kt | 14 ++ .../watch/device/sensor/VibrationDefaults.kt | 90 ++++++++++ .../watch/device/sensor/VibrationPattern.kt | 29 +++ .../java/com/xiaoqu/watch/di/DeviceModule.kt | 38 ++++ .../java/com/xiaoqu/watch/event/AppEvent.kt | 6 + .../service/manager/SystemStateMonitor.kt | 129 +++++++++++++ .../com/xiaoqu/watch/ui/home/HomeFragment.kt | 170 +++++++++++++----- app/src/main/res/layout/fragment_home.xml | 83 +++------ app/src/main/res/raw/close_punch.mp3 | Bin 0 -> 4869 bytes app/src/main/res/raw/no_auth_open.mp3 | Bin 0 -> 4113 bytes app/src/main/res/raw/offline.mp3 | Bin 0 -> 6057 bytes app/src/main/res/raw/open_failed.mp3 | Bin 0 -> 3465 bytes app/src/main/res/raw/open_locking.mp3 | Bin 0 -> 4113 bytes app/src/main/res/raw/open_punch.mp3 | Bin 0 -> 5193 bytes app/src/main/res/raw/open_success.mp3 | Bin 0 -> 3249 bytes app/src/main/res/raw/punch_success.mp3 | Bin 0 -> 3249 bytes 22 files changed, 815 insertions(+), 100 deletions(-) create mode 100644 app/src/main/java/com/xiaoqu/watch/device/nfc/FiseNfcController.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/device/nfc/NfcController.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/device/screen/FiseScreenController.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/device/screen/ScreenController.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/device/sensor/FiseVibrationController.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationController.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationDefaults.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationPattern.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/di/DeviceModule.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/service/manager/SystemStateMonitor.kt create mode 100644 app/src/main/res/raw/close_punch.mp3 create mode 100644 app/src/main/res/raw/no_auth_open.mp3 create mode 100644 app/src/main/res/raw/offline.mp3 create mode 100644 app/src/main/res/raw/open_failed.mp3 create mode 100644 app/src/main/res/raw/open_locking.mp3 create mode 100644 app/src/main/res/raw/open_punch.mp3 create mode 100644 app/src/main/res/raw/open_success.mp3 create mode 100644 app/src/main/res/raw/punch_success.mp3 diff --git a/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt b/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt index 982b368..2275be3 100644 --- a/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt +++ b/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt @@ -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 打卡模式(后续模块实现) diff --git a/app/src/main/java/com/xiaoqu/watch/device/nfc/FiseNfcController.kt b/app/src/main/java/com/xiaoqu/watch/device/nfc/FiseNfcController.kt new file mode 100644 index 0000000..dd48c88 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/nfc/FiseNfcController.kt @@ -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") + "" + } + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/device/nfc/NfcController.kt b/app/src/main/java/com/xiaoqu/watch/device/nfc/NfcController.kt new file mode 100644 index 0000000..db4007d --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/nfc/NfcController.kt @@ -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() +} diff --git a/app/src/main/java/com/xiaoqu/watch/device/screen/FiseScreenController.kt b/app/src/main/java/com/xiaoqu/watch/device/screen/FiseScreenController.kt new file mode 100644 index 0000000..de55e04 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/screen/FiseScreenController.kt @@ -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)) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/device/screen/ScreenController.kt b/app/src/main/java/com/xiaoqu/watch/device/screen/ScreenController.kt new file mode 100644 index 0000000..415e6ab --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/screen/ScreenController.kt @@ -0,0 +1,14 @@ +package com.xiaoqu.watch.device.screen + +/** + * 屏幕控制接口(硬件抽象层) + * 封装亮屏/熄屏操作,换设备时只需替换实现类 + */ +interface ScreenController { + /** 获取屏幕是否亮着 */ + fun isScreenOn(): Boolean + /** 亮屏 */ + fun turnOn() + /** 熄屏 */ + fun turnOff() +} diff --git a/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseVibrationController.kt b/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseVibrationController.kt new file mode 100644 index 0000000..79a72ef --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseVibrationController.kt @@ -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 振动控制实现 + * ���持简单振动和方案振动(嵌套循环 + 音频反馈) + * + * 振动算法(与旧版 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()) + + /** 当�� MediaPlayer(播放完毕后释��) */ + private var mediaPlayer: MediaPlayer? = null + + /** 执行一次���单振动 */ + @Suppress("DEPRECATION") + override fun vibrate(durationMs: Long) { + Timber.d("振动: ${durationMs}ms") + vibrator.vibrate(durationMs) + } + + /** + * 按方案执行��动(���循环和音频反馈) + * @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() { + // 停止振动��程 + 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) + // 等待振动��成 + 休息间隔 + 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 + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationController.kt b/app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationController.kt new file mode 100644 index 0000000..8d6a330 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationController.kt @@ -0,0 +1,14 @@ +package com.xiaoqu.watch.device.sensor + +/** + * 振动控制接口(硬件抽象层) + * 封装简单振动和方案振动(含循环+音频) + */ +interface VibrationController { + /** 执行一次简单振动 */ + fun vibrate(durationMs: Long) + /** 按方案执行振动(含循环和音频反馈) */ + fun executePattern(pattern: VibrationPattern) + /** 停止当前振动 */ + fun stop() +} diff --git a/app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationDefaults.kt b/app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationDefaults.kt new file mode 100644 index 0000000..3fc9195 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationDefaults.kt @@ -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 = 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 + ) + ) +} diff --git a/app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationPattern.kt b/app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationPattern.kt new file mode 100644 index 0000000..5aecb14 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/sensor/VibrationPattern.kt @@ -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 +) diff --git a/app/src/main/java/com/xiaoqu/watch/di/DeviceModule.kt b/app/src/main/java/com/xiaoqu/watch/di/DeviceModule.kt new file mode 100644 index 0000000..1016ee2 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/di/DeviceModule.kt @@ -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 +} diff --git a/app/src/main/java/com/xiaoqu/watch/event/AppEvent.kt b/app/src/main/java/com/xiaoqu/watch/event/AppEvent.kt index 3b5d817..d6e0fa4 100644 --- a/app/src/main/java/com/xiaoqu/watch/event/AppEvent.kt +++ b/app/src/main/java/com/xiaoqu/watch/event/AppEvent.kt @@ -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() } diff --git a/app/src/main/java/com/xiaoqu/watch/service/manager/SystemStateMonitor.kt b/app/src/main/java/com/xiaoqu/watch/service/manager/SystemStateMonitor.kt new file mode 100644 index 0000000..e7f9549 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/service/manager/SystemStateMonitor.kt @@ -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.EXTRA_DEVICE) + val name = device?.name ?: "未知设备" + Timber.d("系统状态监听: 蓝牙已连接 $name") + emitEvent(AppEvent.BluetoothDeviceConnected(name)) + } + + /** 处理蓝牙设备断开 */ + private fun handleBluetoothDisconnected(intent: Intent) { + val device = intent.getParcelableExtra(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) + } + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt index 414e4ef..e4c93af 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt @@ -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() { @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() { // 初始化设备信息 initDevicePrefs() - // 设置 NavBar 为首页模式(状态图标 + 时间 + 电量) + // 设置 NavBar 为首页模式 NavBarHelper.setupHomePage(binding.root) - // 初始化弹窗(使用 Activity 的全局弹窗容器) + // 初始化弹窗 val dialogContainer = requireActivity().findViewById(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() { 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 ) } } + + /** 监听系统状态事件(电量、蓝牙)���更新 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 -> {} // 其他事件不处理 + } + } + } + } } diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 1a3b2c4..c601981 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -1,12 +1,12 @@ - + - + @@ -22,71 +22,44 @@ android:paddingEnd="@dimen/safe_area_right" android:paddingBottom="@dimen/safe_area_bottom"> - + + android:layout_marginBottom="@dimen/spacing_sm" /> - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/raw/close_punch.mp3 b/app/src/main/res/raw/close_punch.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..75e495b9ab2d348013dc571034a9d56246b9c4de GIT binary patch literal 4869 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 Rygh(D9PRzTS@J(u`9G8S%3}Zk literal 0 HcmV?d00001 diff --git a/app/src/main/res/raw/offline.mp3 b/app/src/main/res/raw/offline.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..db937a00d1414e90c61ddf2213bdbcaba881b8de GIT binary patch literal 6057 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 WvuV}>HYtdJGB(Wk*SY^)hW`KrK@~p$ literal 0 HcmV?d00001 diff --git a/app/src/main/res/raw/open_failed.mp3 b/app/src/main/res/raw/open_failed.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..121874a3aed4be967e7272f7b740de5d5d7d50b8 GIT binary patch literal 3465 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 F{{c`G(MJFP literal 0 HcmV?d00001 diff --git a/app/src/main/res/raw/open_punch.mp3 b/app/src/main/res/raw/open_punch.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5a26749bac9b601dfbf2f54c3d2391de69dabbb2 GIT binary patch literal 5193 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 Fe*ld~0y_Wz literal 0 HcmV?d00001 diff --git a/app/src/main/res/raw/punch_success.mp3 b/app/src/main/res/raw/punch_success.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..baf9b22bbc7d7d08ebe7a9063d8fea35e045048e GIT binary patch literal 3249 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 Fe*ld~0y_Wz literal 0 HcmV?d00001