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 fb3ca8c..cfe8736 100644 --- a/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt +++ b/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt @@ -9,9 +9,11 @@ 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.data.prefs.UserPrefs import com.xiaoqu.watch.device.screen.ScreenController import com.xiaoqu.watch.device.sensor.AccelerometerWakeController import com.xiaoqu.watch.service.manager.BluetoothScanManager +import com.xiaoqu.watch.service.manager.NfcTaskManager import com.xiaoqu.watch.service.manager.NotificationManager import com.xiaoqu.watch.service.manager.SystemStateMonitor import com.xiaoqu.watch.service.manager.UpdateManager @@ -45,6 +47,9 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var updateManager: UpdateManager @Inject lateinit var screenController: ScreenController @Inject lateinit var bluetoothScanManager: BluetoothScanManager + /** NFC 任务打卡管理器 */ + @Inject lateinit var nfcTaskManager: NfcTaskManager + @Inject lateinit var userPrefs: UserPrefs /** OTA 更新弹窗 */ lateinit var updateDialog: UpdateDialogView lateinit var notificationBanner: NotificationBannerView @@ -130,8 +135,8 @@ class MainActivity : AppCompatActivity() { /** 下拉回调(由 HomeFragment 注册) */ var onSwipeDown: (() -> Unit)? = null - /** 返回键回调(由 HomeFragment 注册,触发 NFC 打卡) */ - var onBackKeyPressed: (() -> Unit)? = null + /** 返回键回调(由 HomeFragment 注册)。返回 true = 已处理,不触发主动打卡 */ + var onBackKeyPressed: (() -> Boolean)? = null private var touchStartY = 0f private var touchStartX = 0f @@ -167,20 +172,45 @@ class MainActivity : AppCompatActivity() { /** * 物理返回键拦截: - * - 已绑定用户 → 开启 NFC 打卡模式(后续模块实现) + * - 已绑定用户 → 主动打卡(批量任务打卡 or 硬件开锁) + * - NFC 扫描中 → 忽略(防重复) * - 未绑定 → 无操作 * - 所有情况阻止默认页面回退 + * + * 注意:考勤打卡只从下拉面板触发,不走返回键 */ private fun setupBackButton() { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { Timber.d("Back button pressed - intercepted") - // 已绑定用户 → 触发 NFC 打卡(由 HomeFragment 注册回调) - onBackKeyPressed?.invoke() + + // 由 HomeFragment 注册的回调处理(面板展��时收回) + val callback = onBackKeyPressed + if (callback != null && callback()) { + // 回调返回 true = 已处理,不继续 + return + } + // 回调返回 false 或无回调 → 触发主动打卡 + startActivePunchFromBackKey() } }) } + /** 返回键触发主动打卡(批量任务 or 硬件开锁) */ + private fun startActivePunchFromBackKey() { + // 已在 NFC 扫描中 → 忽略 + if (nfcTaskManager.isScanning) return + // 未绑定 → 忽略 + if (!userPrefs.isBound) return + + Timber.d("返回键: 触发主动打卡") + nfcTaskManager.startActivePunch { success, message -> + if (message.isNotEmpty()) { + android.widget.Toast.makeText(this@MainActivity, message, android.widget.Toast.LENGTH_SHORT).show() + } + } + } + // ===== OTA 更新 ===== /** 设置更新弹窗按钮回调 */ diff --git a/app/src/main/java/com/xiaoqu/watch/data/task/NfcTypeResult.kt b/app/src/main/java/com/xiaoqu/watch/data/task/NfcTypeResult.kt new file mode 100644 index 0000000..b6a08aa --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/data/task/NfcTypeResult.kt @@ -0,0 +1,16 @@ +package com.xiaoqu.watch.data.task + +import com.google.gson.annotations.SerializedName + +/** + * checkNfcType API 响应数据 + * GET nfcInfo/nfcOpenLock 返回,判断 NFC 卡是硬件开锁还是任务打卡 + */ +data class NfcTypeResult( + /** true=硬件开锁设备,false=任务打卡信标 */ + @SerializedName("hardwareNfcFlag") val hardwareNfcFlag: Boolean = false, + /** 开锁状态(仅 hardwareNfcFlag=true 时有意义):0=开锁成功 1=设备离线 2=开锁失败 3=无权限 */ + @SerializedName("status") val status: Int = 0, + /** 附加信息 */ + @SerializedName("content") val content: String? = null +) diff --git a/app/src/main/java/com/xiaoqu/watch/network/api/TaskApi.kt b/app/src/main/java/com/xiaoqu/watch/network/api/TaskApi.kt index 74d4b80..4977c4e 100644 --- a/app/src/main/java/com/xiaoqu/watch/network/api/TaskApi.kt +++ b/app/src/main/java/com/xiaoqu/watch/network/api/TaskApi.kt @@ -1,5 +1,6 @@ package com.xiaoqu.watch.network.api +import com.xiaoqu.watch.data.task.NfcTypeResult import com.xiaoqu.watch.data.task.TaskDetail import com.xiaoqu.watch.data.task.TaskItem import com.xiaoqu.watch.data.task.TaskStatistics @@ -37,4 +38,16 @@ interface TaskApi { /** 确认完成 */ @POST("task/completeTask") suspend fun completeTask(@Body params: HashMap): ApiResponse + + /** NFC 单个任务打卡(有场景打卡) */ + @POST("watchTask/nfcToBeginTask") + suspend fun nfcToBeginTask(@Body params: HashMap): ApiResponse + + /** NFC 批量任务打卡(返回键主动打卡) */ + @POST("watchTask/nfcBatchBeginTask") + suspend fun nfcBatchBeginTask(@Body params: HashMap): ApiResponse + + /** 检查 NFC 类型(批量打卡 or 硬件开锁) */ + @GET("nfcInfo/nfcOpenLock") + suspend fun checkNfcType(@Query("nfcId") nfcId: String): ApiResponse } diff --git a/app/src/main/java/com/xiaoqu/watch/service/manager/NfcTaskManager.kt b/app/src/main/java/com/xiaoqu/watch/service/manager/NfcTaskManager.kt new file mode 100644 index 0000000..79750c3 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/service/manager/NfcTaskManager.kt @@ -0,0 +1,276 @@ +package com.xiaoqu.watch.service.manager + +import com.xiaoqu.watch.device.nfc.NfcController +import com.xiaoqu.watch.device.sensor.VibrationController +import com.xiaoqu.watch.network.ApiResult +import com.xiaoqu.watch.network.api.TaskApi +import com.xiaoqu.watch.network.safeApiCall +import kotlinx.coroutines.* +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * NFC 任务打卡管理器 + * + * 统一处理三种 NFC 打卡场景: + * 1. 任务单个打卡(任务详情页"开启打卡"按钮) + * 2. 主动批量打卡(返回键触发,checkNfcType → nfcBatchBeginTask) + * 3. 硬件开锁(返回键触发,checkNfcType → 根据 status 播放语音) + * + * 与 PunchViewModel(考勤打卡)独立,互不干扰。 + * 通过 isScanning 防止两者同时操作 NFC。 + */ +@Singleton +class NfcTaskManager @Inject constructor( + private val nfcController: NfcController, + private val vibrationController: VibrationController, + private val taskApi: TaskApi +) { + companion object { + /** 默认 NFC 超时(毫秒),可被 MQTT type=4 的 nfcOpenTime 覆盖 */ + private const val DEFAULT_NFC_TIMEOUT_MS = 20_000L + // 震动方案 planId + private const val PLAN_PUNCH_SUCCESS = 4 + private const val PLAN_PUNCH_FAIL = 7 + private const val PLAN_NFC_OPEN = 8 + private const val PLAN_NFC_CLOSE = 9 + private const val PLAN_OFFLINE = 10 + private const val PLAN_OPEN_FAILED = 11 + private const val PLAN_NO_AUTH = 12 + private const val PLAN_OPENING = 13 + } + + /** NFC 是否正在扫描(供外部互斥检查) */ + @Volatile + var isScanning = false + private set + + /** NFC 超时时间(毫秒),通过 MQTT type=4 更新 */ + var nfcTimeoutMs: Long = DEFAULT_NFC_TIMEOUT_MS + + /** 协程作用域 */ + private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + /** 超时 Job */ + private var timeoutJob: Job? = null + + /** + * 场景 1:任务单个打卡 + * 开启 NFC → 读卡 → POST nfcToBeginTask → 回调结果 + * + * @param taskId 任务 ID + * @param onResult 结果回调(主线程),success + message + */ + fun startTaskPunch(taskId: Long, onResult: (success: Boolean, message: String) -> Unit) { + if (isScanning) { + Timber.d("NFC任务打卡: 已在扫描中,忽略") + return + } + isScanning = true + Timber.d("NFC任��打卡: 开启单个打卡, taskId=%d", taskId) + + // 开启 NFC + 音效 + vibrationController.executeByPlanId(PLAN_NFC_OPEN) + nfcController.open() + + // 开始扫描 + nfcController.startScan { nfcId -> + Timber.d("NFC任务打卡: 读到卡号 %s", nfcId) + cancelTimeout() + nfcController.stopScan() + nfcController.close() + + // 调用打卡 API + scope.launch { + val params = hashMapOf("id" to taskId, "nfcId" to nfcId) + val result = safeApiCall { taskApi.nfcToBeginTask(params) } + when (result) { + is ApiResult.Success -> { + Timber.d("NFC任务打卡: 打卡成功") + vibrationController.executeByPlanId(PLAN_PUNCH_SUCCESS) + onResult(true, "打卡成功") + } + is ApiResult.Error -> { + Timber.w("NFC任务打卡: 打卡失败 - %s", result.message) + vibrationController.executeByPlanId(PLAN_PUNCH_FAIL) + onResult(false, result.message ?: "��卡失败") + } + is ApiResult.NetworkError -> { + Timber.w(result.exception, "NFC任务打卡: 网络异常") + vibrationController.executeByPlanId(PLAN_PUNCH_FAIL) + onResult(false, "网络异常") + } + } + isScanning = false + } + } + + // 启动超时 + startTimeout { + Timber.d("NFC任务打卡: 超时自动关闭") + closeNfc() + onResult(false, "超���") + isScanning = false + } + } + + /** + * 场景 2+3:主动打卡(���回键触发) + * 开启 NFC → 读卡 → checkNfcType → 批量打卡 or 硬件开锁 + * + * @param onResult 结果回调(主线程),success + message + */ + fun startActivePunch(onResult: (success: Boolean, message: String) -> Unit) { + if (isScanning) { + Timber.d("NFC主动打卡: 已在扫��中,忽略") + return + } + isScanning = true + Timber.d("NFC主动��卡: 开启(返回键触发)") + + // 开启 NFC + ��效 + vibrationController.executeByPlanId(PLAN_NFC_OPEN) + nfcController.open() + + // 开始扫描 + nfcController.startScan { nfcId -> + Timber.d("NFC主动打卡: 读到���号 %s", nfcId) + cancelTimeout() + nfcController.stopScan() + nfcController.close() + vibrationController.executeByPlanId(PLAN_NFC_CLOSE) + + // 判断类型:批量打卡 or 硬件开锁 + scope.launch { + handleNfcType(nfcId, onResult) + isScanning = false + } + } + + // 启动超时 + startTimeout { + Timber.d("NFC主动打卡: 超时自动关闭") + closeNfc() + vibrationController.executeByPlanId(PLAN_NFC_CLOSE) + onResult(false, "") + isScanning = false + } + } + + /** 取消 NFC 扫描 */ + fun cancel() { + if (!isScanning) return + Timber.d("NFC任务: 手动取消") + cancelTimeout() + closeNfc() + vibrationController.executeByPlanId(PLAN_NFC_CLOSE) + isScanning = false + } + + /** + * 判断 NFC 类型并处理 + * hardwareNfcFlag=true → 硬件开锁(播���语音) + * hardwareNfcFlag=false → 批量任务打卡(调 API) + */ + private suspend fun handleNfcType(nfcId: String, onResult: (Boolean, String) -> Unit) { + val result = safeApiCall { taskApi.checkNfcType(nfcId) } + when (result) { + is ApiResult.Success -> { + val data = result.data + if (data == null) { + onResult(false, "数据异常") + return + } + + if (data.hardwareNfcFlag) { + // 硬件开锁 + handleHardwareUnlock(data.status, onResult) + } else { + // 批量任务打卡 + handleBatchPunch(nfcId, onResult) + } + } + is ApiResult.Error -> { + Timber.w("NFC主动打卡: checkNfcType 失败 - %s", result.message) + vibrationController.executeByPlanId(PLAN_PUNCH_FAIL) + onResult(false, result.message ?: "判断失败") + } + is ApiResult.NetworkError -> { + Timber.w(result.exception, "NFC主动打卡: 网络异常") + vibrationController.executeByPlanId(PLAN_PUNCH_FAIL) + onResult(false, "网络异常") + } + } + } + + /** 硬件开锁:根据 status 播放对应语音 */ + private fun handleHardwareUnlock(status: Int, onResult: (Boolean, String) -> Unit) { + Timber.d("NFC��动打卡: 硬件开锁, status=%d", status) + when (status) { + 0 -> { + vibrationController.executeByPlanId(PLAN_OPENING) + onResult(true, "正在开锁") + } + 1 -> { + vibrationController.executeByPlanId(PLAN_OFFLINE) + onResult(false, "设备离线") + } + 2 -> { + vibrationController.executeByPlanId(PLAN_OPEN_FAILED) + onResult(false, "开锁失败") + } + 3 -> { + vibrationController.executeByPlanId(PLAN_NO_AUTH) + onResult(false, "无开锁权限") + } + else -> { + onResult(false, "��知状态") + } + } + } + + /** 批量任务打卡 */ + private suspend fun handleBatchPunch(nfcId: String, onResult: (Boolean, String) -> Unit) { + Timber.d("NFC主动打卡: 批量打卡, nfcId=%s", nfcId) + val params = hashMapOf("nfcId" to nfcId) + val result = safeApiCall { taskApi.nfcBatchBeginTask(params) } + when (result) { + is ApiResult.Success -> { + Timber.d("NFC主动打卡: 批量打卡成功") + vibrationController.executeByPlanId(PLAN_PUNCH_SUCCESS) + onResult(true, "打卡成功") + } + is ApiResult.Error -> { + Timber.w("NFC主动打卡: 批量打卡失败 - %s", result.message) + vibrationController.executeByPlanId(PLAN_PUNCH_FAIL) + onResult(false, result.message ?: "打卡失败") + } + is ApiResult.NetworkError -> { + Timber.w(result.exception, "NFC主动打卡: 网络��常") + vibrationController.executeByPlanId(PLAN_PUNCH_FAIL) + onResult(false, "网络异常") + } + } + } + + /** 关闭 NFC 硬件 */ + private fun closeNfc() { + nfcController.stopScan() + nfcController.close() + } + + /** 启动超时协程 */ + private fun startTimeout(onTimeout: () -> Unit) { + timeoutJob = scope.launch { + delay(nfcTimeoutMs) + onTimeout() + } + } + + /** 取消超时协程 */ + private fun cancelTimeout() { + timeoutJob?.cancel() + timeoutJob = null + } +} 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 c88feda..991ea01 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 @@ -264,24 +264,15 @@ class HomeFragment : BaseFragment() { } } - // 物理返回键触发 NFC 打卡(已绑定用户在首页时) + // 物理返回键回调:面板已展开时收回面板 + // 注意:主动打卡(批量任务+开锁)由 MainActivity 直接处理,不走这里 + // 考勤打卡只从下拉面��触发 mainActivity.onBackKeyPressed = { - if (userPrefs.isBound) { - // 面板已展开 → 收回面板 - if (punchPanel.isShowing) { - punchPanel.dismiss() - } else { - // 展开面板并自动开始上班/下班打卡 - showPunchPanel() - // 延迟一帧等面板展开后,根据状态自动触发打卡 - binding.root.post { - val state = punchViewModel.uiState.value - when { - state.onPunchState == 0 -> punchViewModel.startPunch(0) - state.onPunchState == 1 -> punchViewModel.startPunch(1) - } - } - } + if (punchPanel.isShowing) { + punchPanel.dismiss() + true // 已处理,不触发主动打卡 + } else { + false // 未处理,让 MainActivity 触发主动打卡 } } } diff --git a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt index c32d46d..b56bbce 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskDetailFragment.kt @@ -13,6 +13,7 @@ import com.xiaoqu.watch.databinding.FragmentTaskDetailBinding import com.xiaoqu.watch.network.ApiResult import com.xiaoqu.watch.network.api.TaskApi import com.xiaoqu.watch.network.safeApiCall +import com.xiaoqu.watch.service.manager.NfcTaskManager import com.xiaoqu.watch.ui.common.BaseFragment import com.xiaoqu.watch.ui.widget.QuTipDialog import dagger.hilt.android.AndroidEntryPoint @@ -28,6 +29,7 @@ import javax.inject.Inject class TaskDetailFragment : BaseFragment() { @Inject lateinit var taskApi: TaskApi + @Inject lateinit var nfcTaskManager: NfcTaskManager /** 当前任务数据 */ private var taskDetail: TaskDetail? = null @@ -116,13 +118,12 @@ class TaskDetailFragment : BaseFragment() { // 待打卡 3 -> { if (detail.hasPosition) { - // 有场景 → 橙色「开启打卡」(NFC,后续实现) + // 有场景 → 橙色「开启打卡」(NFC 任务打卡) btn.text = "开启打卡" btn.setBackgroundResource(R.drawable.bg_foot_btn_orange) btn.setTextColor(requireContext().getColor(R.color.background)) btn.setOnClickListener { - // TODO: NFC 打卡流程 - Timber.d("任务详情: NFC 打卡(后续实现)") + startNfcTaskPunch(detail.id) } } else { // 无场景 → 绿色「确认打卡」 @@ -212,6 +213,39 @@ class TaskDetailFragment : BaseFragment() { } } + /** NFC 任务打卡(有场景打卡) */ + private fun startNfcTaskPunch(taskId: Long) { + val btn = binding.btnAction + // 切换到扫描状态 + btn.text = "贴近信标..." + btn.isEnabled = false + btn.setBackgroundResource(R.drawable.bg_foot_btn_grey) + + nfcTaskManager.startTaskPunch(taskId) { success, message -> + if (success) { + tipDialog.show( + status = QuTipDialog.Status.SUCCESS, + title = "打卡成功", + back = true, step = 1, countdown = 2, + onBack = { findNavController().popBackStack() } + ) + } else { + // 失败或超时 → 恢复按钮 + if (message != "超时") { + tipDialog.show( + status = QuTipDialog.Status.ERROR, + title = "打卡失败", + desc = message, + back = false, step = 0, countdown = 3 + ) + } + btn.text = "开启打卡" + btn.isEnabled = true + btn.setBackgroundResource(R.drawable.bg_foot_btn_orange) + } + } + } + /** 确认完成操作 */ private fun doCompleteTask(taskId: Long) { viewLifecycleOwner.lifecycleScope.launch { diff --git a/app/src/main/res/drawable/bg_foot_btn_grey.xml b/app/src/main/res/drawable/bg_foot_btn_grey.xml new file mode 100644 index 0000000..e34b1e7 --- /dev/null +++ b/app/src/main/res/drawable/bg_foot_btn_grey.xml @@ -0,0 +1,7 @@ + + + + + +