367 lines
14 KiB
Kotlin
367 lines
14 KiB
Kotlin
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
|
||
|
||
/**
|
||
* 主 Activity(Launcher 模式,单 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()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|