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 0000000..75e495b Binary files /dev/null and b/app/src/main/res/raw/close_punch.mp3 differ diff --git a/app/src/main/res/raw/no_auth_open.mp3 b/app/src/main/res/raw/no_auth_open.mp3 new file mode 100644 index 0000000..23167ff Binary files /dev/null and b/app/src/main/res/raw/no_auth_open.mp3 differ diff --git a/app/src/main/res/raw/offline.mp3 b/app/src/main/res/raw/offline.mp3 new file mode 100644 index 0000000..db937a0 Binary files /dev/null and b/app/src/main/res/raw/offline.mp3 differ 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 0000000..121874a Binary files /dev/null and b/app/src/main/res/raw/open_failed.mp3 differ diff --git a/app/src/main/res/raw/open_locking.mp3 b/app/src/main/res/raw/open_locking.mp3 new file mode 100644 index 0000000..0f06ddb Binary files /dev/null and b/app/src/main/res/raw/open_locking.mp3 differ 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 0000000..5a26749 Binary files /dev/null and b/app/src/main/res/raw/open_punch.mp3 differ diff --git a/app/src/main/res/raw/open_success.mp3 b/app/src/main/res/raw/open_success.mp3 new file mode 100644 index 0000000..baf9b22 Binary files /dev/null and b/app/src/main/res/raw/open_success.mp3 differ 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 0000000..baf9b22 Binary files /dev/null and b/app/src/main/res/raw/punch_success.mp3 differ