feat(device-interaction): 加速度计抬手亮屏功能
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) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import com.xiaoqu.watch.databinding.ActivityMainBinding
|
import com.xiaoqu.watch.databinding.ActivityMainBinding
|
||||||
import com.xiaoqu.watch.event.AppEvent
|
import com.xiaoqu.watch.event.AppEvent
|
||||||
import com.xiaoqu.watch.event.EventBus
|
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.NotificationManager
|
||||||
import com.xiaoqu.watch.service.manager.SystemStateMonitor
|
import com.xiaoqu.watch.service.manager.SystemStateMonitor
|
||||||
import com.xiaoqu.watch.ui.widget.NotificationBannerView
|
import com.xiaoqu.watch.ui.widget.NotificationBannerView
|
||||||
@@ -34,6 +35,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
@Inject lateinit var systemStateMonitor: SystemStateMonitor
|
@Inject lateinit var systemStateMonitor: SystemStateMonitor
|
||||||
@Inject lateinit var notificationManager: NotificationManager
|
@Inject lateinit var notificationManager: NotificationManager
|
||||||
@Inject lateinit var eventBus: EventBus
|
@Inject lateinit var eventBus: EventBus
|
||||||
|
/** 加速度计抬手亮屏控制器 */
|
||||||
|
@Inject lateinit var accelerometerWake: AccelerometerWakeController
|
||||||
lateinit var notificationBanner: NotificationBannerView
|
lateinit var notificationBanner: NotificationBannerView
|
||||||
private val activityScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
private val activityScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
|
|
||||||
@@ -59,6 +62,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// 注册系统状态监听(电量、蓝牙)
|
// 注册系统状态监听(电量、蓝牙)
|
||||||
systemStateMonitor.register()
|
systemStateMonitor.register()
|
||||||
|
|
||||||
|
// 启动加速度计抬手亮屏
|
||||||
|
accelerometerWake.start()
|
||||||
|
|
||||||
// 初始化通知横幅
|
// 初始化通知横幅
|
||||||
notificationBanner = binding.notificationBanner
|
notificationBanner = binding.notificationBanner
|
||||||
|
|
||||||
@@ -70,6 +76,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
accelerometerWake.stop()
|
||||||
systemStateMonitor.unregister()
|
systemStateMonitor.unregister()
|
||||||
notificationBanner.destroy()
|
notificationBanner.destroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import com.xiaoqu.watch.device.nfc.FiseNfcController
|
|||||||
import com.xiaoqu.watch.device.nfc.NfcController
|
import com.xiaoqu.watch.device.nfc.NfcController
|
||||||
import com.xiaoqu.watch.device.screen.FiseScreenController
|
import com.xiaoqu.watch.device.screen.FiseScreenController
|
||||||
import com.xiaoqu.watch.device.screen.ScreenController
|
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.FiseVibrationController
|
||||||
import com.xiaoqu.watch.device.sensor.VibrationController
|
import com.xiaoqu.watch.device.sensor.VibrationController
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
@@ -35,4 +37,9 @@ abstract class DeviceModule {
|
|||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun bindVibrationController(impl: FiseVibrationController): VibrationController
|
abstract fun bindVibrationController(impl: FiseVibrationController): VibrationController
|
||||||
|
|
||||||
|
/** 加速度计抬手亮屏:方案D(WRIST_TILT)优先,降级方案C(Z轴趋势) */
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindAccelerometerWake(impl: FiseAccelerometerWake): AccelerometerWakeController
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.xiaoqu.watch.device.nfc.NfcController
|
import com.xiaoqu.watch.device.nfc.NfcController
|
||||||
import com.xiaoqu.watch.device.screen.ScreenController
|
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.VibrationController
|
||||||
import com.xiaoqu.watch.device.sensor.VibrationDefaults
|
import com.xiaoqu.watch.device.sensor.VibrationDefaults
|
||||||
import com.xiaoqu.watch.network.ApiResult
|
import com.xiaoqu.watch.network.ApiResult
|
||||||
@@ -29,7 +30,8 @@ class PunchViewModel @Inject constructor(
|
|||||||
private val punchApi: PunchApi,
|
private val punchApi: PunchApi,
|
||||||
private val nfcController: NfcController,
|
private val nfcController: NfcController,
|
||||||
private val vibrationController: VibrationController,
|
private val vibrationController: VibrationController,
|
||||||
private val screenController: ScreenController
|
private val screenController: ScreenController,
|
||||||
|
private val accelerometerWake: AccelerometerWakeController
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -96,7 +98,10 @@ class PunchViewModel @Inject constructor(
|
|||||||
Timber.d("考勤: 开始NFC打卡, punchType=$punchType")
|
Timber.d("考勤: 开始NFC打卡, punchType=$punchType")
|
||||||
_uiState.update { it.copy(isNfcScanning = true, scanningPunchType = punchType) }
|
_uiState.update { it.copy(isNfcScanning = true, scanningPunchType = punchType) }
|
||||||
|
|
||||||
// 1. 开启 NFC + 音效
|
// 1. 暂停加速度计亮屏检测(防止贴卡姿势误触发熄屏)
|
||||||
|
accelerometerWake.pause()
|
||||||
|
|
||||||
|
// 2. 开启 NFC + 音效
|
||||||
nfcController.open()
|
nfcController.open()
|
||||||
playFeedback(PLAN_NFC_OPEN)
|
playFeedback(PLAN_NFC_OPEN)
|
||||||
|
|
||||||
@@ -134,6 +139,8 @@ class PunchViewModel @Inject constructor(
|
|||||||
// 关闭 NFC 硬件(不播关闭音效,成功/失败音效由 API 结果决定)
|
// 关闭 NFC 硬件(不播关闭音效,成功/失败音效由 API 结果决定)
|
||||||
nfcController.stopScan()
|
nfcController.stopScan()
|
||||||
nfcController.close()
|
nfcController.close()
|
||||||
|
// 恢复加速度计亮屏检测
|
||||||
|
accelerometerWake.resume()
|
||||||
// 清除倒计时
|
// 清除倒计时
|
||||||
_uiState.update { it.copy(nfcCountdown = 0) }
|
_uiState.update { it.copy(nfcCountdown = 0) }
|
||||||
|
|
||||||
@@ -237,11 +244,13 @@ class PunchViewModel @Inject constructor(
|
|||||||
_uiState.update { it.copy(punchResult = null, errorMessage = null) }
|
_uiState.update { it.copy(punchResult = null, errorMessage = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 关闭 NFC 硬件 + 播放关闭音效 */
|
/** 关闭 NFC 硬件 + 播放关闭音效 + 恢复加速度计 */
|
||||||
private fun closeNfc() {
|
private fun closeNfc() {
|
||||||
nfcController.stopScan()
|
nfcController.stopScan()
|
||||||
nfcController.close()
|
nfcController.close()
|
||||||
playFeedback(PLAN_NFC_CLOSE)
|
playFeedback(PLAN_NFC_CLOSE)
|
||||||
|
// 恢复加速度计亮屏检测
|
||||||
|
accelerometerWake.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 播放震动+音效反馈 */
|
/** 播放震动+音效反馈 */
|
||||||
@@ -259,6 +268,8 @@ class PunchViewModel @Inject constructor(
|
|||||||
if (nfcController.isOpen()) {
|
if (nfcController.isOpen()) {
|
||||||
nfcController.stopScan()
|
nfcController.stopScan()
|
||||||
nfcController.close()
|
nfcController.close()
|
||||||
|
// 确保加速度计恢复(防止页面销毁时 NFC 未关闭导致永久暂停)
|
||||||
|
accelerometerWake.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user