From e3f6ac3c975a22e7f0659602ff70c740d9bf86fc Mon Sep 17 00:00:00 2001 From: dongliang Date: Thu, 30 Apr 2026 16:54:05 +0930 Subject: [PATCH] =?UTF-8?q?feat(device-interaction):=20=E5=8A=A0=E9=80=9F?= =?UTF-8?q?=E5=BA=A6=E8=AE=A1=E6=8A=AC=E6=89=8B=E4=BA=AE=E5=B1=8F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REQ-20260430-0026 - 新增 AccelerometerWakeController 接口 + FiseAccelerometerWake 实现 - 双模式策略:方案D(WRIST_TILT)优先,不支持自动降级方案C(Z轴变化趋势) - 方案C防误触发:检测Z轴从低(<3)到高(≥6)的变化趋势,非简单阈值 - NFC打卡时 pause/resume 暂停检测,防止贴卡姿势误触发 - 熄屏交系统SCREEN_OFF_TIMEOUT管理,加速度计只管亮屏 - DeviceModule 增加 DI 绑定 - MainActivity 增加 start/stop 生命周期管理 - PunchViewModel 增加 NFC 开关时 pause/resume 调用 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../java/com/xiaoqu/watch/app/MainActivity.kt | 7 + .../sensor/AccelerometerWakeController.kt | 36 ++++ .../device/sensor/FiseAccelerometerWake.kt | 194 ++++++++++++++++++ .../java/com/xiaoqu/watch/di/DeviceModule.kt | 7 + .../xiaoqu/watch/ui/punch/PunchViewModel.kt | 17 +- 5 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/xiaoqu/watch/device/sensor/AccelerometerWakeController.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/device/sensor/FiseAccelerometerWake.kt 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 b6c831a..59ecf1f 100644 --- a/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt +++ b/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt @@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity import com.xiaoqu.watch.databinding.ActivityMainBinding import com.xiaoqu.watch.event.AppEvent import com.xiaoqu.watch.event.EventBus +import com.xiaoqu.watch.device.sensor.AccelerometerWakeController import com.xiaoqu.watch.service.manager.NotificationManager import com.xiaoqu.watch.service.manager.SystemStateMonitor import com.xiaoqu.watch.ui.widget.NotificationBannerView @@ -34,6 +35,8 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var systemStateMonitor: SystemStateMonitor @Inject lateinit var notificationManager: NotificationManager @Inject lateinit var eventBus: EventBus + /** 加速度计抬手亮屏控制器 */ + @Inject lateinit var accelerometerWake: AccelerometerWakeController lateinit var notificationBanner: NotificationBannerView private val activityScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) @@ -59,6 +62,9 @@ class MainActivity : AppCompatActivity() { // 注册系统状态监听(电量、蓝牙) systemStateMonitor.register() + // 启动加速度计抬手亮屏 + accelerometerWake.start() + // 初始化通知横幅 notificationBanner = binding.notificationBanner @@ -70,6 +76,7 @@ class MainActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() + accelerometerWake.stop() systemStateMonitor.unregister() notificationBanner.destroy() } diff --git a/app/src/main/java/com/xiaoqu/watch/device/sensor/AccelerometerWakeController.kt b/app/src/main/java/com/xiaoqu/watch/device/sensor/AccelerometerWakeController.kt new file mode 100644 index 0000000..e489e47 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/sensor/AccelerometerWakeController.kt @@ -0,0 +1,36 @@ +package com.xiaoqu.watch.device.sensor + +/** + * 加速度计抬手亮屏控制器接口 + * + * 检测用户抬手动作,自动唤醒屏幕。 + * 熄屏由系统 SCREEN_OFF_TIMEOUT 管理,本控制器只负责亮屏。 + * + * 实现策略: + * - 优先使用 TYPE_WRIST_TILT_GESTURE(方案D,系统级手势识别) + * - 不支持时降级为 TYPE_ACCELEROMETER Z轴变化趋势检测(方案C) + * + * 使用方式: + * - MainActivity.onCreate → start() + * - MainActivity.onDestroy → stop() + * - NFC 操作开始 → pause()(防止打卡时误熄屏) + * - NFC 操作结束 → resume() + */ +interface AccelerometerWakeController { + + /** 开始监听传感器(注册 SensorEventListener) */ + fun start() + + /** 停止监听(注销 SensorEventListener,释放资源) */ + fun stop() + + /** + * 暂停亮屏检测 + * NFC 操作中调用,防止贴卡姿势导致误熄屏。 + * 覆盖四种 NFC 场景:考勤打卡、任务单个打卡、任务批量打卡、硬件开锁 + */ + fun pause() + + /** 恢复亮屏检测 */ + fun resume() +} diff --git a/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseAccelerometerWake.kt b/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseAccelerometerWake.kt new file mode 100644 index 0000000..173bd96 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/device/sensor/FiseAccelerometerWake.kt @@ -0,0 +1,194 @@ +package com.xiaoqu.watch.device.sensor + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import com.xiaoqu.watch.device.screen.ScreenController +import dagger.hilt.android.qualifiers.ApplicationContext +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * FISE 手表加速度计抬手亮屏实现 + * + * 双模式策略: + * - 方案D:TYPE_WRIST_TILT_GESTURE(type=26),系统级手势识别,最省电最准确 + * - 方案C:TYPE_ACCELEROMETER Z轴变化趋势检测,作为降级方案 + * + * 方案C核心逻辑: + * 检测Z轴从低值(<3,手臂下垂)到高值(≥6,手臂抬起)的变化趋势。 + * 与旧版简单的Z≥5判断相比,可防止: + * - 手表平放桌面常亮(Z一直≈9.8,无"低→高"变化) + * - 走路手臂摆动频繁亮灭(Z波动但无持续趋势) + * + * 参数来源:baseline/04 — Z轴唤醒阈值≥5(home.vue:122),加迟滞调整为低<3/高≥6 + */ +@Singleton +class FiseAccelerometerWake @Inject constructor( + @ApplicationContext private val context: Context, + private val screenController: ScreenController +) : AccelerometerWakeController, SensorEventListener { + + companion object { + /** 方案C:Z轴低位阈值(手臂下垂判定) */ + private const val Z_LOW_THRESHOLD = 3f + /** 方案C:Z轴高位阈值(手臂抬起判定),来源 baseline/04 home.vue:122 原值5,加迟滞调整为6 */ + private const val Z_HIGH_THRESHOLD = 6f + /** 方案C:滑动窗口大小(前半+后半各3个采样,SENSOR_DELAY_NORMAL下约1.2秒) */ + private const val WINDOW_SIZE = 6 + /** 方案C:前半窗口大小 */ + private const val HALF_WINDOW = WINDOW_SIZE / 2 + } + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + + /** 是否已暂停(NFC操作中) */ + private var paused = false + /** 是否使用方案D(WRIST_TILT) */ + private var useWristTilt = false + /** 是否已启动 */ + private var started = false + + // === 方案C:Z轴滑动窗口 === + /** 固定大小的环形缓冲区,存储最近的Z轴采样值 */ + private val zWindow = FloatArray(WINDOW_SIZE) + /** 当前写入位置 */ + private var windowIndex = 0 + /** 窗口是否已填满(至少6个采样后才开始判断) */ + private var windowFilled = false + + /** + * 开始监听传感器 + * 优先尝试 WRIST_TILT(方案D),不支持则降级 ACCELEROMETER(方案C) + */ + override fun start() { + if (started) return + + // 方案D:优先尝试 TYPE_WRIST_TILT_GESTURE (type=26, API 20+) + val wristTilt = sensorManager.getDefaultSensor(26) + if (wristTilt != null) { + sensorManager.registerListener(this, wristTilt, SensorManager.SENSOR_DELAY_NORMAL) + useWristTilt = true + started = true + Timber.i("抬手亮屏: 使用方案D(WRIST_TILT_GESTURE传感器)") + return + } + + // 方案C:降级使用加速度计Z轴变化趋势检测 + val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + if (accelerometer != null) { + sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL) + useWristTilt = false + started = true + Timber.i("抬手亮屏: 使用方案C(加速度计Z轴变化趋势),WRIST_TILT不可用") + } else { + Timber.w("抬手亮屏: 无可用传感器,功能不可用") + } + } + + /** 停止监听,释放传感器资源 */ + override fun stop() { + if (!started) return + sensorManager.unregisterListener(this) + started = false + resetWindow() + Timber.d("抬手亮屏: 已停止") + } + + /** 暂停检测(NFC操作中调用) */ + override fun pause() { + paused = true + Timber.d("抬手亮屏: 已暂停(NFC操作中)") + } + + /** 恢复检测 */ + override fun resume() { + paused = false + resetWindow() // 恢复时清空窗口,避免暂停期间积累的数据干扰判断 + Timber.d("抬手亮屏: 已恢复") + } + + // === SensorEventListener 回调 === + + override fun onSensorChanged(event: SensorEvent) { + if (paused) return + + if (useWristTilt) { + // 方案D:收到 WRIST_TILT 事件即为抬手动作 + wakeScreenIfOff() + return + } + + // 方案C:Z轴变化趋势检测 + detectWristRaise(event.values[2]) + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // 精度变化无需处理 + } + + // === 方案C 核心逻辑 === + + /** + * Z轴变化趋势检测 + * 用环形缓冲区存储最近6个Z轴采样值,分前半(旧)和后半(新)各3个: + * - 前3个均值 < 3(手臂下垂状态) + * - 后3个均值 ≥ 6(手臂抬起状态) + * 满足条件 = 检测到"抬手"动作 + */ + private fun detectWristRaise(z: Float) { + // 写入环形缓冲区 + zWindow[windowIndex % WINDOW_SIZE] = z + windowIndex++ + + // 窗口未填满,等待更多采样 + if (!windowFilled) { + if (windowIndex >= WINDOW_SIZE) { + windowFilled = true + } else { + return + } + } + + // 计算前半(较旧的3个采样)和后半(较新的3个采样)的均值 + // 环形缓冲区中,当前写入位置的前 HALF_WINDOW 个是最新的,再前 HALF_WINDOW 个是较旧的 + val currentPos = windowIndex % WINDOW_SIZE + var oldSum = 0f + var newSum = 0f + for (i in 0 until HALF_WINDOW) { + // 较旧的3个:从 currentPos 往前数第 6,5,4 个位置 + val oldIdx = (currentPos - WINDOW_SIZE + i + WINDOW_SIZE) % WINDOW_SIZE + // 较新的3个:从 currentPos 往前数第 3,2,1 个位置 + val newIdx = (currentPos - HALF_WINDOW + i + WINDOW_SIZE) % WINDOW_SIZE + oldSum += zWindow[oldIdx] + newSum += zWindow[newIdx] + } + val oldAvg = oldSum / HALF_WINDOW + val newAvg = newSum / HALF_WINDOW + + // 判断:从低位(下垂)到高位(抬起)的变化趋势 + if (oldAvg < Z_LOW_THRESHOLD && newAvg >= Z_HIGH_THRESHOLD) { + wakeScreenIfOff() + // 触发后清空窗口,防止连续触发 + resetWindow() + } + } + + /** 屏幕未亮时才发送亮屏指令,防止重复广播 */ + private fun wakeScreenIfOff() { + if (!screenController.isScreenOn()) { + Timber.d("抬手亮屏: 检测到抬手,唤醒屏幕") + screenController.turnOn() + } + } + + /** 重置滑动窗口 */ + private fun resetWindow() { + windowIndex = 0 + windowFilled = false + zWindow.fill(0f) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/di/DeviceModule.kt b/app/src/main/java/com/xiaoqu/watch/di/DeviceModule.kt index 1016ee2..05e09c2 100644 --- a/app/src/main/java/com/xiaoqu/watch/di/DeviceModule.kt +++ b/app/src/main/java/com/xiaoqu/watch/di/DeviceModule.kt @@ -4,6 +4,8 @@ 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.AccelerometerWakeController +import com.xiaoqu.watch.device.sensor.FiseAccelerometerWake import com.xiaoqu.watch.device.sensor.FiseVibrationController import com.xiaoqu.watch.device.sensor.VibrationController import dagger.Binds @@ -35,4 +37,9 @@ abstract class DeviceModule { @Binds @Singleton abstract fun bindVibrationController(impl: FiseVibrationController): VibrationController + + /** 加速度计抬手亮屏:方案D(WRIST_TILT)优先,降级方案C(Z轴趋势) */ + @Binds + @Singleton + abstract fun bindAccelerometerWake(impl: FiseAccelerometerWake): AccelerometerWakeController } diff --git a/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchViewModel.kt b/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchViewModel.kt index b3a5e53..c018fd6 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchViewModel.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.xiaoqu.watch.device.nfc.NfcController import com.xiaoqu.watch.device.screen.ScreenController +import com.xiaoqu.watch.device.sensor.AccelerometerWakeController import com.xiaoqu.watch.device.sensor.VibrationController import com.xiaoqu.watch.device.sensor.VibrationDefaults import com.xiaoqu.watch.network.ApiResult @@ -29,7 +30,8 @@ class PunchViewModel @Inject constructor( private val punchApi: PunchApi, private val nfcController: NfcController, private val vibrationController: VibrationController, - private val screenController: ScreenController + private val screenController: ScreenController, + private val accelerometerWake: AccelerometerWakeController ) : ViewModel() { companion object { @@ -96,7 +98,10 @@ class PunchViewModel @Inject constructor( Timber.d("考勤: 开始NFC打卡, punchType=$punchType") _uiState.update { it.copy(isNfcScanning = true, scanningPunchType = punchType) } - // 1. 开启 NFC + 音效 + // 1. 暂停加速度计亮屏检测(防止贴卡姿势误触发熄屏) + accelerometerWake.pause() + + // 2. 开启 NFC + 音效 nfcController.open() playFeedback(PLAN_NFC_OPEN) @@ -134,6 +139,8 @@ class PunchViewModel @Inject constructor( // 关闭 NFC 硬件(不播关闭音效,成功/失败音效由 API 结果决定) nfcController.stopScan() nfcController.close() + // 恢复加速度计亮屏检测 + accelerometerWake.resume() // 清除倒计时 _uiState.update { it.copy(nfcCountdown = 0) } @@ -237,11 +244,13 @@ class PunchViewModel @Inject constructor( _uiState.update { it.copy(punchResult = null, errorMessage = null) } } - /** 关闭 NFC 硬件 + 播放关闭音效 */ + /** 关闭 NFC 硬件 + 播放关闭音效 + 恢复加速度计 */ private fun closeNfc() { nfcController.stopScan() nfcController.close() playFeedback(PLAN_NFC_CLOSE) + // 恢复加速度计亮屏检测 + accelerometerWake.resume() } /** 播放震动+音效反馈 */ @@ -259,6 +268,8 @@ class PunchViewModel @Inject constructor( if (nfcController.isOpen()) { nfcController.stopScan() nfcController.close() + // 确保加速度计恢复(防止页面销毁时 NFC 未关闭导致永久暂停) + accelerometerWake.resume() } } }