fix: 息屏后MQTT断连导致无法收到通知
添加前台服务+WakeLock保活MQTT连接,请求电池优化白名单绕过Doze限制, AlarmManager每5分钟健康检查断连自动重连。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 更新 =====
|
||||
|
||||
/** 设置更新弹窗按钮回调 */
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
145
app/src/main/java/com/xiaoqu/watch/service/MqttService.kt
Normal file
145
app/src/main/java/com/xiaoqu/watch/service/MqttService.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user