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