Files
xqwatch/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt
2026-05-11 14:11:14 +09:30

367 lines
14 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.xiaoqu.watch.app
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import androidx.navigation.fragment.NavHostFragment
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.Gson
import com.xiaoqu.watch.R
import com.xiaoqu.watch.data.device.WatchBindInfo
import com.xiaoqu.watch.data.prefs.DevicePrefs
import com.xiaoqu.watch.databinding.ActivityMainBinding
import com.xiaoqu.watch.network.api.CommonApi
import com.xiaoqu.watch.network.safeApiCall
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
import com.xiaoqu.watch.ui.widget.UpdateDialogView
import com.xiaoqu.watch.ui.widget.NotificationBannerView
import dagger.hilt.android.AndroidEntryPoint
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 kotlin.math.abs
/**
* 主 ActivityLauncher 模式,单 Activity + 多 Fragment 架构)
* 职责:全屏设置、物理返回键拦截、系统状态监听注册
*/
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
/** 系统状态监听器(电量、蓝牙状态) */
@Inject lateinit var systemStateMonitor: SystemStateMonitor
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var eventBus: EventBus
/** 加速度计抬手亮屏控制器 */
@Inject lateinit var accelerometerWake: AccelerometerWakeController
/** OTA 更新管理器 */
@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
@Inject lateinit var devicePrefs: DevicePrefs
@Inject lateinit var commonApi: CommonApi
@Inject lateinit var gson: Gson
/** OTA 更新弹窗 */
lateinit var updateDialog: UpdateDialogView
lateinit var notificationBanner: NotificationBannerView
private val activityScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 固定竖屏
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
// 全屏(定制系统无状态栏,此为防御性设置)
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 拦截物理返回键
setupBackButton()
// 注册系统状态监听(电量、蓝牙)
systemStateMonitor.register()
// 加速度计抬手亮屏:当前硬件仅有基础加速度计(无陀螺仪),
// 无法可靠区分"看表"和"小幅摆动"(角度差仅~7°暂不启用。
// 待更换有陀螺仪的硬件后取消注释。
// accelerometerWake.start()
// 初始化通知横幅
notificationBanner = binding.notificationBanner
// 初始化 OTA 更新弹窗
updateDialog = binding.updateDialog
setupUpdateDialog()
// 监听 MQTT type=1 → 处理通知 + 显示横幅
observeMqttMessages()
Timber.d("MainActivity created")
}
override fun onDestroy() {
super.onDestroy()
// accelerometerWake.stop() // 与 start() 对应,暂不启用
systemStateMonitor.unregister()
notificationBanner.destroy()
}
// ===== MQTT 新任务处理 =====
/** 监听 MQTT 消息Activity 级别,不受 Fragment 生命周期影响) */
private fun observeMqttMessages() {
activityScope.launch {
eventBus.events.collect { event ->
when (event) {
is AppEvent.MqttMessageReceived -> {
when (event.type) {
1 -> {
// 新任务通知
notificationManager.onNewTaskMessage(event.rawJson)
val count = notificationManager.pendingCount
if (count > 0) {
notificationBanner.show(count)
}
}
2 -> {
// 绑定成功 → 存用户信息 → 跳首页
Timber.d("MainActivity: 收到绑定消息")
handleBindMessage(event.rawJson)
}
3 -> {
// 解绑 → 清除数据 → 跳绑定页(从任何页面都能跳)
Timber.d("MainActivity: 收到解绑消息")
android.widget.Toast.makeText(this@MainActivity, "收到解绑消息", android.widget.Toast.LENGTH_SHORT).show()
bluetoothScanManager.stop()
userPrefs.clear()
try {
val navHost = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
if (navHost != null) {
navHost.navController.navigate(R.id.action_global_to_bind)
Timber.d("MainActivity: 解绑导航成功")
} else {
Timber.e("MainActivity: NavHostFragment 为 null")
android.widget.Toast.makeText(this@MainActivity, "导航失败NavHost为空", android.widget.Toast.LENGTH_LONG).show()
}
} catch (e: Exception) {
Timber.e(e, "MainActivity: 解绑导航异常")
android.widget.Toast.makeText(this@MainActivity, "导航异常:${e.message}", android.widget.Toast.LENGTH_LONG).show()
}
}
}
}
// 去抖合并完成后 → 更新横幅数字
is AppEvent.NewTaskArrived -> {
if (event.count > 0) {
notificationBanner.show(event.count)
}
}
else -> {}
}
}
}
}
// ===== 下拉手势检测Activity 级别,不可能被子 View 拦截) =====
/** 下拉回调(由 HomeFragment 注册) */
var onSwipeDown: (() -> Unit)? = null
/** 返回键回调(由 HomeFragment 注册)。返回 true = 已处理,不触发主动打卡 */
var onBackKeyPressed: (() -> Boolean)? = null
private var touchStartY = 0f
private var touchStartX = 0f
private var swipeTriggered = false
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// 在事件分发给任何 View 之前,先检测下拉手势
if (onSwipeDown != null) {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
touchStartY = ev.rawY
touchStartX = ev.rawX
swipeTriggered = false
}
MotionEvent.ACTION_MOVE -> {
if (!swipeTriggered) {
val dy = ev.rawY - touchStartY
val dx = ev.rawX - touchStartX
// 下滑超过 50px 且垂直方向为主
if (dy > 50 && abs(dy) > abs(dx) * 1.5f) {
swipeTriggered = true
onSwipeDown?.invoke()
}
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
swipeTriggered = false
}
}
}
return super.dispatchTouchEvent(ev)
}
/**
* 物理返回键拦截:
* - 已绑定用户 → 主动打卡(批量任务打卡 or 硬件开锁)
* - NFC 扫描中 → 忽略(防重复)
* - 未绑定 → 无操作
* - 所有情况阻止默认页面回退
*
* 注意:考勤打卡只从下拉面板触发,不走返回键
*/
private fun setupBackButton() {
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Timber.d("Back button pressed - intercepted")
// 由 HomeFragment 注册的回调处理面板展<E69DBF><E5B195>时收回
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()
}
}
}
// ===== 绑定处理 =====
/** 防止重复处理绑定消息 */
private var bindHandled = false
/** 处理 MQTT type=2 绑定消息Activity 级别,不受 Fragment 生命周期影响) */
private fun handleBindMessage(rawJson: String) {
if (bindHandled) return
bindHandled = true
try {
val bindInfo = gson.fromJson(rawJson, WatchBindInfo::class.java)
// 存入 UserPrefs
userPrefs.saveUser(
userId = bindInfo.userId,
mobile = bindInfo.mobile,
userName = bindInfo.userName,
headUrl = bindInfo.headUrl
)
// 异步调 API 确认(不阻塞导航)
activityScope.launch {
try {
val params = hashMapOf<String, Any>(
"imei" to devicePrefs.imei,
"userId" to bindInfo.userId
)
safeApiCall { commonApi.bindWatchConfirm(params) }
Timber.d("MainActivity: 绑定确认 API 已调用")
} catch (e: Exception) {
Timber.w(e, "MainActivity: 绑定确认 API 异常")
}
}
// 导航到首页
val navHost = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navHost.navController.navigate(R.id.action_global_to_home)
Timber.d("MainActivity: 绑定成功,已导航到首页")
} catch (e: Exception) {
Timber.e(e, "MainActivity: 绑定处理异常")
bindHandled = false
}
}
// ===== OTA 更新 =====
/** 设置更新弹窗按钮回调 */
private fun setupUpdateDialog() {
updateDialog.onActionClick = {
startDownloadAndInstall()
}
}
/**
* 检查版本更新(由 HomeFragment.onResume 调用)
* 有更新时显示弹窗,停止蓝牙扫描
*/
fun checkForUpdate() {
if (updateManager.isUpdating) return
activityScope.launch {
val info = updateManager.checkUpdate() ?: return@launch
// 停止蓝牙扫描
bluetoothScanManager.stop()
// 显示更新弹窗
updateDialog.showDiscover()
// 保存下载地址
pendingUpdateUrl = info.url
}
}
/** 待下载的更新 URL */
private var pendingUpdateUrl: String? = null
/** 上次进度更新时间(限制频率,避免主线程过载) */
private var lastProgressUpdate = 0L
/** 开始下载并安装 APK */
private fun startDownloadAndInstall() {
val url = pendingUpdateUrl ?: return
Timber.d("OTA: 开始下载安装流程, url=%s", url)
updateManager.isUpdating = true
// 保持屏幕常亮
screenController.turnOn()
// 切换到下载状态
updateDialog.showDownloading()
activityScope.launch {
val file = updateManager.downloadApk(url) { progress, bytes ->
// 限制进度更新频率:最多每 200ms 更新一次 UI
val now = System.currentTimeMillis()
if (now - lastProgressUpdate >= 200 || progress >= 100) {
lastProgressUpdate = now
runOnUiThread {
updateDialog.updateProgress(progress, bytes)
}
}
}
// 回到主线程处理结果
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) {
if (file != null) {
Timber.d("OTA: 下载完成,触发安装")
updateManager.installApk(file)
} else {
Timber.d("OTA: 下载失败")
updateManager.isUpdating = false
updateDialog.showError()
}
}
}
}
}