diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 91af49a..e495bb6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,12 @@
+
+
+
+
+
+
@@ -73,6 +79,12 @@
android:resource="@xml/file_paths" />
+
+
+
+
+
+
diff --git a/app/src/main/java/com/xiaoqu/watch/app/BootReceiver.kt b/app/src/main/java/com/xiaoqu/watch/app/BootReceiver.kt
index 8836802..40e0a74 100644
--- a/app/src/main/java/com/xiaoqu/watch/app/BootReceiver.kt
+++ b/app/src/main/java/com/xiaoqu/watch/app/BootReceiver.kt
@@ -3,17 +3,27 @@ package com.xiaoqu.watch.app
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import com.xiaoqu.watch.service.MqttAlarmReceiver
+import com.xiaoqu.watch.service.MqttService
import timber.log.Timber
/**
* 开机自启广播接收器
- * 收到 BOOT_COMPLETED 后启动 MainActivity
+ * 收到 BOOT_COMPLETED 后启动 MQTT 服务 + MainActivity
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
- Timber.d("Boot completed, launching MainActivity")
+ Timber.d("Boot completed, starting MqttService + MainActivity")
+
+ // 先启动 MQTT 前台服务(尽早恢复连接)
+ MqttService.start(context)
+
+ // 启动连接健康检查定时器
+ MqttAlarmReceiver.schedule(context)
+
+ // 启动主界面
val launchIntent = Intent(context, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
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 cfe8736..69d7919 100644
--- a/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt
+++ b/app/src/main/java/com/xiaoqu/watch/app/MainActivity.kt
@@ -1,7 +1,12 @@
package com.xiaoqu.watch.app
+import android.content.Intent
import android.content.pm.ActivityInfo
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
+import android.os.PowerManager
+import android.provider.Settings
import android.view.MotionEvent
import android.view.View
import androidx.activity.OnBackPressedCallback
@@ -92,6 +97,9 @@ class MainActivity : AppCompatActivity() {
// 监听 MQTT type=1 → 处理通知 + 显示横幅
observeMqttMessages()
+ // 请求电池优化白名单(绕过 Doze 模式限制)
+ requestBatteryWhitelist()
+
Timber.d("MainActivity created")
}
@@ -211,6 +219,31 @@ class MainActivity : AppCompatActivity() {
}
}
+ // ===== 电池优化白名单 =====
+
+ /**
+ * 请求加入电池优化白名单(Doze 模式下允许后台网络访问)
+ * 已在白名单中则跳过,避免重复弹窗
+ */
+ private fun requestBatteryWhitelist() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val pm = getSystemService(PowerManager::class.java)
+ if (!pm.isIgnoringBatteryOptimizations(packageName)) {
+ try {
+ val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
+ data = Uri.parse("package:$packageName")
+ }
+ startActivity(intent)
+ Timber.d("电池白名单: 已请求")
+ } catch (e: Exception) {
+ Timber.w(e, "电池白名单: 请求失败")
+ }
+ } else {
+ Timber.d("电池白名单: 已在白名单中")
+ }
+ }
+ }
+
// ===== OTA 更新 =====
/** 设置更新弹窗按钮回调 */
diff --git a/app/src/main/java/com/xiaoqu/watch/service/MqttAlarmReceiver.kt b/app/src/main/java/com/xiaoqu/watch/service/MqttAlarmReceiver.kt
new file mode 100644
index 0000000..fe5f911
--- /dev/null
+++ b/app/src/main/java/com/xiaoqu/watch/service/MqttAlarmReceiver.kt
@@ -0,0 +1,85 @@
+package com.xiaoqu.watch.service
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.SystemClock
+import com.xiaoqu.watch.service.manager.MqttManager
+import dagger.hilt.android.AndroidEntryPoint
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * MQTT 连接健康检查广播接收器
+ *
+ * 通过 AlarmManager.setExactAndAllowWhileIdle() 定期触发,
+ * 在 Doze 模式的维护窗口中检查 MQTT 连接状态,断连则重连。
+ * 同时确保 MqttService 前台服务存活。
+ */
+@AndroidEntryPoint
+class MqttAlarmReceiver : BroadcastReceiver() {
+
+ companion object {
+ /** 检查间隔:5 分钟(Doze 维护窗口至少 10 分钟,5 分钟能抓到窗口期) */
+ private const val CHECK_INTERVAL_MS = 5 * 60 * 1000L
+
+ /** 启动定时健康检查 */
+ fun schedule(context: Context) {
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ val intent = Intent(context, MqttAlarmReceiver::class.java)
+ val pendingIntent = PendingIntent.getBroadcast(
+ context, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ // setExactAndAllowWhileIdle: Doze 模式下也能触发
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ alarmManager.setExactAndAllowWhileIdle(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + CHECK_INTERVAL_MS,
+ pendingIntent
+ )
+ } else {
+ alarmManager.setExact(
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + CHECK_INTERVAL_MS,
+ pendingIntent
+ )
+ }
+ Timber.d("MqttAlarm: 已设置 ${CHECK_INTERVAL_MS / 1000}s 后检查")
+ }
+
+ /** 取消定时检查 */
+ fun cancel(context: Context) {
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ val intent = Intent(context, MqttAlarmReceiver::class.java)
+ val pendingIntent = PendingIntent.getBroadcast(
+ context, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ alarmManager.cancel(pendingIntent)
+ Timber.d("MqttAlarm: 已取消定时检查")
+ }
+ }
+
+ @Inject lateinit var mqttManager: MqttManager
+
+ override fun onReceive(context: Context, intent: Intent) {
+ Timber.d("MqttAlarm: 健康检查触发, isConnected=${mqttManager.isConnected}")
+
+ // 确保前台服务在运行
+ MqttService.start(context)
+
+ // MQTT 断连则重连
+ if (!mqttManager.isConnected) {
+ Timber.w("MqttAlarm: 连接已断开,触发重连")
+ mqttManager.connect()
+ }
+
+ // 重新设置下一次闹钟(setExactAndAllowWhileIdle 是一次性的)
+ schedule(context)
+ }
+}
diff --git a/app/src/main/java/com/xiaoqu/watch/service/MqttService.kt b/app/src/main/java/com/xiaoqu/watch/service/MqttService.kt
new file mode 100644
index 0000000..bbb5421
--- /dev/null
+++ b/app/src/main/java/com/xiaoqu/watch/service/MqttService.kt
@@ -0,0 +1,145 @@
+package com.xiaoqu.watch.service
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import android.os.PowerManager
+import com.xiaoqu.watch.R
+import com.xiaoqu.watch.service.manager.MqttManager
+import dagger.hilt.android.AndroidEntryPoint
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * MQTT 前台服务
+ *
+ * 保持 MQTT 连接在息屏/Doze 模式下不被系统杀死。
+ * 通过 startForegroundService() 启动,显示常驻通知。
+ * 持有 PARTIAL_WAKE_LOCK 防止 CPU 休眠断连。
+ */
+@AndroidEntryPoint
+class MqttService : Service() {
+
+ companion object {
+ /** 通知渠道 ID */
+ private const val CHANNEL_ID = "mqtt_service"
+ /** 前台通知 ID */
+ private const val NOTIFICATION_ID = 1
+
+ /** 启动服务 */
+ fun start(context: Context) {
+ val intent = Intent(context, MqttService::class.java)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ }
+
+ /** 停止服务 */
+ fun stop(context: Context) {
+ context.stopService(Intent(context, MqttService::class.java))
+ }
+ }
+
+ @Inject lateinit var mqttManager: MqttManager
+
+ /** 保持 CPU 唤醒,防止息屏后 MQTT 心跳丢失 */
+ private var wakeLock: PowerManager.WakeLock? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ Timber.d("MqttService: onCreate")
+
+ // 创建通知渠道
+ createNotificationChannel()
+
+ // 启动前台服务(必须在 5 秒内调用,否则 ANR)
+ startForeground(NOTIFICATION_ID, buildNotification())
+
+ // 获取 PARTIAL_WAKE_LOCK 保持 CPU 运行
+ acquireWakeLock()
+
+ // 连接 MQTT
+ mqttManager.connect()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Timber.d("MqttService: onStartCommand")
+ // 如果连接断了,重新连接
+ if (!mqttManager.isConnected) {
+ mqttManager.connect()
+ }
+ // 被杀后自动重启
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ Timber.d("MqttService: onDestroy")
+ releaseWakeLock()
+ mqttManager.disconnect()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ /** 创建通知渠道(Android 8.0+) */
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "消息连接",
+ NotificationManager.IMPORTANCE_LOW // 低优先级,不发声不振动
+ ).apply {
+ description = "保持消息连接在线"
+ setShowBadge(false)
+ }
+ val nm = getSystemService(NotificationManager::class.java)
+ nm.createNotificationChannel(channel)
+ }
+ }
+
+ /** 构建前台通知(常驻,低优先级) */
+ private fun buildNotification(): Notification {
+ val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Notification.Builder(this, CHANNEL_ID)
+ } else {
+ @Suppress("DEPRECATION")
+ Notification.Builder(this)
+ }
+ return builder
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setContentTitle("小趣智清洁")
+ .setContentText("消息连接中")
+ .setOngoing(true)
+ .build()
+ }
+
+ /** 获取 WakeLock 保持 CPU 唤醒 */
+ private fun acquireWakeLock() {
+ val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
+ wakeLock = pm.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ "xqwatch:mqtt_service"
+ ).apply {
+ acquire() // 持续持有,直到服务销毁时释放
+ }
+ Timber.d("MqttService: WakeLock acquired")
+ }
+
+ /** 释放 WakeLock */
+ private fun releaseWakeLock() {
+ wakeLock?.let {
+ if (it.isHeld) {
+ it.release()
+ Timber.d("MqttService: WakeLock released")
+ }
+ }
+ wakeLock = null
+ }
+}
diff --git a/app/src/main/java/com/xiaoqu/watch/ui/common/SplashFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/common/SplashFragment.kt
index e5ccd18..ea82b15 100644
--- a/app/src/main/java/com/xiaoqu/watch/ui/common/SplashFragment.kt
+++ b/app/src/main/java/com/xiaoqu/watch/ui/common/SplashFragment.kt
@@ -13,6 +13,8 @@ import com.xiaoqu.watch.data.prefs.UserPrefs
import com.xiaoqu.watch.network.ApiResult
import com.xiaoqu.watch.network.api.CommonApi
import com.xiaoqu.watch.network.safeApiCall
+import com.xiaoqu.watch.service.MqttAlarmReceiver
+import com.xiaoqu.watch.service.MqttService
import com.xiaoqu.watch.service.manager.MqttManager
import com.xiaoqu.watch.util.DeviceUtil
import dagger.hilt.android.AndroidEntryPoint
@@ -47,10 +49,13 @@ class SplashFragment : Fragment() {
// 1. 初始化设备信息(首次启动时写入 SP)
initDevicePrefs()
- // 2. 连接 MQTT(需要 IMEI,所以必须在 initDevicePrefs 之后)
- mqttManager.connect()
+ // 2. 启动 MQTT 前台服务(保持息屏后连接存活)
+ MqttService.start(requireContext())
- // 3. 检查绑定状态并导航
+ // 3. 启动连接健康检查定时器(Doze 模式下定期检查)
+ MqttAlarmReceiver.schedule(requireContext())
+
+ // 4. 检查绑定状态并导航
checkBindStatus()
}