fix: 息屏后MQTT断连导致无法收到通知

添加前台服务+WakeLock保活MQTT连接,请求电池优化白名单绕过Doze限制,
AlarmManager每5分钟健康检查断连自动重连。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-05-09 13:10:04 +09:30
parent 966c9f9c0d
commit b867c33015
6 changed files with 301 additions and 5 deletions

View File

@@ -27,6 +27,12 @@
<!-- WakeLock --> <!-- WakeLock -->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- 前台服务MQTT 保活) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 电池优化白名单(绕过 Doze 限制) -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- 手机状态(获取 IMEI --> <!-- 手机状态(获取 IMEI -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
@@ -73,6 +79,12 @@
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
<!-- MQTT 前台服务(息屏保活) -->
<service
android:name=".service.MqttService"
android:enabled="true"
android:exported="false" />
<!-- 开机自启广播接收器 --> <!-- 开机自启广播接收器 -->
<receiver <receiver
android:name=".app.BootReceiver" android:name=".app.BootReceiver"
@@ -83,6 +95,12 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- MQTT 连接健康检查(定时闹钟触发) -->
<receiver
android:name=".service.MqttAlarmReceiver"
android:enabled="true"
android:exported="false" />
</application> </application>
</manifest> </manifest>

View File

@@ -3,17 +3,27 @@ package com.xiaoqu.watch.app
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.xiaoqu.watch.service.MqttAlarmReceiver
import com.xiaoqu.watch.service.MqttService
import timber.log.Timber import timber.log.Timber
/** /**
* 开机自启广播接收器 * 开机自启广播接收器
* 收到 BOOT_COMPLETED 后启动 MainActivity * 收到 BOOT_COMPLETED 后启动 MQTT 服务 + MainActivity
*/ */
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) { 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 { val launchIntent = Intent(context, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }

View File

@@ -1,7 +1,12 @@
package com.xiaoqu.watch.app package com.xiaoqu.watch.app
import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
@@ -92,6 +97,9 @@ class MainActivity : AppCompatActivity() {
// 监听 MQTT type=1 → 处理通知 + 显示横幅 // 监听 MQTT type=1 → 处理通知 + 显示横幅
observeMqttMessages() observeMqttMessages()
// 请求电池优化白名单(绕过 Doze 模式限制)
requestBatteryWhitelist()
Timber.d("MainActivity created") 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 更新 ===== // ===== OTA 更新 =====
/** 设置更新弹窗按钮回调 */ /** 设置更新弹窗按钮回调 */

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -13,6 +13,8 @@ import com.xiaoqu.watch.data.prefs.UserPrefs
import com.xiaoqu.watch.network.ApiResult import com.xiaoqu.watch.network.ApiResult
import com.xiaoqu.watch.network.api.CommonApi import com.xiaoqu.watch.network.api.CommonApi
import com.xiaoqu.watch.network.safeApiCall 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.service.manager.MqttManager
import com.xiaoqu.watch.util.DeviceUtil import com.xiaoqu.watch.util.DeviceUtil
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -47,10 +49,13 @@ class SplashFragment : Fragment() {
// 1. 初始化设备信息(首次启动时写入 SP // 1. 初始化设备信息(首次启动时写入 SP
initDevicePrefs() initDevicePrefs()
// 2. 连接 MQTT(需要 IMEI所以必须在 initDevicePrefs 之后 // 2. 启动 MQTT 前台服务(保持息屏后连接存活
mqttManager.connect() MqttService.start(requireContext())
// 3. 检查绑定状态并导航 // 3. 启动连接健康检查定时器Doze 模式下定期检查)
MqttAlarmReceiver.schedule(requireContext())
// 4. 检查绑定状态并导航
checkBindStatus() checkBindStatus()
} }