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 -->
<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 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
@@ -73,6 +79,12 @@
android:resource="@xml/file_paths" />
</provider>
<!-- MQTT 前台服务(息屏保活) -->
<service
android:name=".service.MqttService"
android:enabled="true"
android:exported="false" />
<!-- 开机自启广播接收器 -->
<receiver
android:name=".app.BootReceiver"
@@ -83,6 +95,12 @@
</intent-filter>
</receiver>
<!-- MQTT 连接健康检查(定时闹钟触发) -->
<receiver
android:name=".service.MqttAlarmReceiver"
android:enabled="true"
android:exported="false" />
</application>
</manifest>

View File

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

View File

@@ -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 更新 =====
/** 设置更新弹窗按钮回调 */

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.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()
}