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:
dongliang
2026-04-27 13:50:25 +09:30
parent 5680613e86
commit c7ae11a574
22 changed files with 815 additions and 100 deletions

View File

@@ -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
/**
* 主 ActivityLauncher 模式,单 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 打卡模式(后续模块实现)

View File

@@ -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")
""
}
}
}

View File

@@ -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()
}

View File

@@ -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))
}
}

View File

@@ -0,0 +1,14 @@
package com.xiaoqu.watch.device.screen
/**
* 屏幕控制接口(硬件抽象层)
* 封装亮屏/熄屏操作,换设备时只需替换实现类
*/
interface ScreenController {
/** 获取屏幕是否亮着 */
fun isScreenOn(): Boolean
/** 亮屏 */
fun turnOn()
/** 熄屏 */
fun turnOff()
}

View File

@@ -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 音频资源 IDR.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
}
}

View File

@@ -0,0 +1,14 @@
package com.xiaoqu.watch.device.sensor
/**
* 振动控制接口(硬件抽象层)
* 封装简单振动和方案振动(含循环+音频)
*/
interface VibrationController {
/** 执行一次简单振动 */
fun vibrate(durationMs: Long)
/** 按方案执行振动(含循环和音频反馈) */
fun executePattern(pattern: VibrationPattern)
/** 停止当前振动 */
fun stop()
}

View File

@@ -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
)
)
}

View File

@@ -0,0 +1,29 @@
package com.xiaoqu.watch.device.sensor
/**
* 振动方案数据类
* 对应旧版 shockStore.js 中的振动参数
*
* 振动算法:外层循环 shockCycleTimes 次,每次内层振动 shockTimes 次,
* 每次振动 shockTime 秒,振动间休息 shockIntervalTime 秒
*/
data class VibrationPattern(
/** 方案 ID2-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,
/** 音频资源 IDR.raw.xxx0 表示无音频 */
val audioResId: Int = 0
)

View 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
}

View File

@@ -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()
}

View File

@@ -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)
}
}
}

View File

@@ -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 -> {} // 其他事件不处理
}
}
}
}
}

View File

@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 首页布局NavBar + 内容区 -->
<!-- 首页布局NavBar + 硬件验证 demo -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:orientation="vertical">
<!-- 顶部导航栏(首页模式:状态图标 + 时间 + 电量) -->
<!-- 顶部导航栏 -->
<include layout="@layout/layout_nav_bar" />
<!-- 内容区域 -->
@@ -22,71 +22,44 @@
android:paddingEnd="@dimen/safe_area_right"
android:paddingBottom="@dimen/safe_area_bottom">
<!-- demo 信息展示 -->
<!-- 系统状态信息 -->
<TextView
android:id="@+id/tvDemoInfo"
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:textSize="@dimen/text_small"
android:lineSpacingExtra="3dp"
android:layout_marginBottom="@dimen/spacing_md" />
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- 按钮 demo触发 QuTipDialog -->
<!-- 屏幕控制测试 -->
<TextView
android:id="@+id/btnScreenOff"
style="@style/ActionButton.Warning"
android:text="熄屏测试3秒后亮屏"
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- 振动测试 -->
<TextView
android:id="@+id/btnVibrate"
style="@style/ActionButton.Primary"
android:text="振动测试"
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- NFC 测试 -->
<TextView
android:id="@+id/btnNfcScan"
style="@style/ActionButton.Success"
android:text="NFC 读卡测试"
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- 弹窗测试 -->
<TextView
android:id="@+id/btnShowTip"
style="@style/ActionButton.Primary"
android:text="显示提示弹窗"
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- 按钮 demo触发 QuConfirmDialog -->
<TextView
android:id="@+id/btnShowConfirm"
style="@style/ActionButton.Success"
android:text="显示确认弹窗"
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- 按钮 demo其他样式展示 -->
<TextView
android:id="@+id/btnDanger"
style="@style/ActionButton.Danger"
android:text="危险按钮"
android:layout_marginBottom="@dimen/spacing_sm" />
<TextView
android:id="@+id/btnWarning"
style="@style/ActionButton.Warning"
android:text="警告按钮"
android:layout_marginBottom="@dimen/spacing_sm" />
<TextView
android:id="@+id/btnGrey"
style="@style/ActionButton.Grey"
android:text="灰色按钮"
android:text="提示弹窗测试"
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- 半宽按钮并排 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView
style="@style/ActionButton.Grey"
android:layout_width="0dp"
android:layout_weight="1"
android:text="取消"
android:layout_marginEnd="@dimen/spacing_sm" />
<TextView
style="@style/ActionButton.Primary"
android:layout_width="0dp"
android:layout_weight="1"
android:text="确定" />
</LinearLayout>
</LinearLayout>
</ScrollView>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.