diff --git a/app/src/main/java/com/xiaoqu/watch/service/manager/MqttManager.kt b/app/src/main/java/com/xiaoqu/watch/service/manager/MqttManager.kt index 268a8f1..2c4c268 100644 --- a/app/src/main/java/com/xiaoqu/watch/service/manager/MqttManager.kt +++ b/app/src/main/java/com/xiaoqu/watch/service/manager/MqttManager.kt @@ -36,12 +36,18 @@ class MqttManager @Inject constructor( /** MQTT 异步客户端 */ private var client: MqttAsyncClient? = null + /** 是否正在连接中(防止重复创建 client) */ + private var connecting = false + /** 是否已连接 */ val isConnected: Boolean get() = client?.isConnected == true /** * 连接 MQTT Broker * 使用 DevicePrefs.imei 作为 clientId,TCP 协议连接 + * + * 关键:不重复创建 client。如果 client 已存在(连接中/已连接/自动重连中), + * 直接复用,避免覆盖正在连接的实例导致连接丢失。 */ fun connect() { val imei = devicePrefs.imei @@ -56,7 +62,21 @@ class MqttManager @Inject constructor( return } + // 正在连接中,不重复发起 + if (connecting) { + Timber.d("MQTT: 正在连接中,跳过") + return + } + + // client 已存在(可能在自动重连中),不重新创建 + if (client != null) { + Timber.d("MQTT: client 已存在(可能在自动重连中),跳过") + return + } + try { + connecting = true + // 构建 TCP 连接地址 val serverUri = "tcp://${EnvConfig.mqttHost}:${MqttConfig.PORT}" Timber.d("MQTT: 连接 $serverUri, clientId=$imei") @@ -82,6 +102,7 @@ class MqttManager @Inject constructor( } catch (e: Exception) { Timber.e(e, "MQTT: 连接异常") + connecting = false } } @@ -89,6 +110,8 @@ class MqttManager @Inject constructor( fun disconnect() { try { client?.disconnect() + client = null + connecting = false Timber.d("MQTT: 已断开") } catch (e: Exception) { Timber.w(e, "MQTT: 断开异常") @@ -98,12 +121,16 @@ class MqttManager @Inject constructor( /** 连接回调:成功后订阅 3 个 Topic */ private val connectCallback = object : IMqttActionListener { override fun onSuccess(asyncActionToken: IMqttToken?) { + connecting = false Timber.d("MQTT: 连接成功") emitEvent(AppEvent.MqttConnected) subscribeTopics() } override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { + connecting = false + // 连接失败,清除 client 以允许下次重新创建 + client = null Timber.e(exception, "MQTT: 连接失败") } } diff --git a/app/src/main/java/com/xiaoqu/watch/ui/bind/BindFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/bind/BindFragment.kt index 46a0add..8c06bc7 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/bind/BindFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/bind/BindFragment.kt @@ -16,6 +16,7 @@ import com.xiaoqu.watch.event.AppEvent import com.xiaoqu.watch.event.EventBus import com.xiaoqu.watch.ui.common.BaseFragment import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -163,17 +164,39 @@ class BindFragment : BaseFragment() { binding.loadingWrap.visibility = View.VISIBLE } - /** 显示 MQTT 连接状态(调试用,确认扫码时连接是否正常) */ + /** + * 显示 MQTT 连接状态(调试用) + * 如果已连接直接显示;否则等待 MqttConnected 事件(最多 8 秒超时) + */ private fun showMqttStatus() { - val connected = mqttManager.isConnected - val imei = devicePrefs.imei - val msg = if (connected) "MQTT已连接 (IMEI=$imei)" else "MQTT未连接!" - android.widget.Toast.makeText(requireContext(), msg, android.widget.Toast.LENGTH_LONG).show() - Timber.d("绑定页: $msg") + if (mqttManager.isConnected) { + // 已连接,直接显示 + showMqttToast(true) + return + } - // 未连接则尝试重连 - if (!connected) { + // MQTT 还在连接中(Splash 刚发起异步连接),等待连接成功事件 + viewLifecycleOwner.lifecycleScope.launch { + // 先尝试重连(防止 Splash 连接失败的情况) mqttManager.connect() + + // 等待 MqttConnected 事件,最多 8 秒 + val connected = kotlinx.coroutines.withTimeoutOrNull(8000L) { + eventBus.events.first { it is AppEvent.MqttConnected } + true + } ?: false + + if (isAdded) { + showMqttToast(connected) + } } } + + /** 显示 MQTT 连接状态 Toast */ + private fun showMqttToast(connected: Boolean) { + val imei = devicePrefs.imei + val msg = if (connected) "MQTT已连接 (IMEI=$imei)" else "MQTT未连接! 请检查网络" + android.widget.Toast.makeText(requireContext(), msg, android.widget.Toast.LENGTH_LONG).show() + Timber.d("绑定页: $msg") + } } 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 130b84d..6bfa9dd 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 @@ -10,13 +10,18 @@ import androidx.navigation.fragment.findNavController import com.xiaoqu.watch.R import com.xiaoqu.watch.data.prefs.DevicePrefs import com.xiaoqu.watch.data.prefs.UserPrefs +import com.xiaoqu.watch.event.AppEvent +import com.xiaoqu.watch.event.EventBus import com.xiaoqu.watch.network.ApiResult import com.xiaoqu.watch.network.api.CommonApi import com.xiaoqu.watch.network.safeApiCall import com.xiaoqu.watch.service.manager.MqttManager import com.xiaoqu.watch.util.DeviceUtil import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber import javax.inject.Inject @@ -32,6 +37,7 @@ class SplashFragment : Fragment() { @Inject lateinit var userPrefs: UserPrefs @Inject lateinit var mqttManager: MqttManager @Inject lateinit var commonApi: CommonApi + @Inject lateinit var eventBus: EventBus override fun onCreateView( inflater: LayoutInflater, @@ -47,11 +53,29 @@ class SplashFragment : Fragment() { // 1. 初始化设备信息(首次启动时写入 SP) initDevicePrefs() - // 2. 连接 MQTT(定制 ROM + Launcher 应用,系统不会杀进程) - mqttManager.connect() + // 2. 连接 MQTT + 检查绑定状态 并行执行,等 MQTT 就绪后再导航 + viewLifecycleOwner.lifecycleScope.launch { + // 并行:MQTT 连接 + API 绑定检查 + mqttManager.connect() - // 3. 检查绑定状态并导航 - checkBindStatus() + // 等待 MQTT 连接成功(最多 10 秒),同时检查绑定状态 + val mqttReady = async { + if (mqttManager.isConnected) return@async true + withTimeoutOrNull(10_000L) { + eventBus.events.first { it is AppEvent.MqttConnected } + true + } ?: false + } + + val bindResult = async { checkBindStatusAsync() } + + // 等待两者都完成 + val mqttOk = mqttReady.await() + Timber.d("Splash: MQTT 就绪=$mqttOk") + + // 根据绑定结果导航 + navigateByResult(bindResult.await()) + } } /** 首次启动时初始化设备信息到 device_prefs */ @@ -69,61 +93,54 @@ class SplashFragment : Fragment() { } /** - * 检查绑定状态: + * 检查绑定状态(挂起函数,返回是否已绑定) * 1. 调用 API 获取最新绑定状态 - * 2. 有数据 → 已绑定 → 更新 UserPrefs → 导航到首页 - * 3. 无数据 → 未绑定 → 导航到绑定页 + * 2. 有数据 → 已绑定 → 更新 UserPrefs → 返回 true + * 3. 无数据 → 未绑定 → 返回 false * 4. 网络异常 → 用本地 isBound 兜底 */ - private fun checkBindStatus() { - viewLifecycleOwner.lifecycleScope.launch { - val imei = devicePrefs.imei + private suspend fun checkBindStatusAsync(): Boolean { + val imei = devicePrefs.imei - // 调用 API 检查绑定状态 - val result = safeApiCall { commonApi.getWatchByImei(imei) } + // 调用 API 检查绑定状态 + val result = safeApiCall { commonApi.getWatchByImei(imei) } - when (result) { - is ApiResult.Success -> { - // API 返回数据 → 已绑定,存入 UserPrefs - val data = result.data - if (data != null && data.userId > 0) { - Timber.d("Splash: API 返回已绑定 userId=${data.userId}") - userPrefs.saveUser( - userId = data.userId, - mobile = data.mobile, - userName = data.userName, - headUrl = data.headUrl - ) - navigateToHome() - } else { - // 返回数据无效,视为未绑定 - Timber.d("Splash: API 返回数据无效,视为未绑定") - navigateToBind() - } + return when (result) { + is ApiResult.Success -> { + val data = result.data + if (data != null && data.userId > 0) { + Timber.d("Splash: API 返回已绑定 userId=${data.userId}") + userPrefs.saveUser( + userId = data.userId, + mobile = data.mobile, + userName = data.userName, + headUrl = data.headUrl + ) + true + } else { + Timber.d("Splash: API 返回数据无效,视为未绑定") + false } - is ApiResult.Error -> { - if (result.code == 1) { - // code=1 未绑定 - Timber.d("Splash: API 返回未绑定") - navigateToBind() - } else { - // 其他错误,用本地状态兜底 - Timber.w("Splash: API 错误 code=${result.code}, 用本地状态兜底") - navigateByLocalState() - } - } - is ApiResult.NetworkError -> { - // 网络异常,用本地状态兜底 - Timber.w("Splash: 网络异常, 用本地状态兜底") - navigateByLocalState() + } + is ApiResult.Error -> { + if (result.code == 1) { + Timber.d("Splash: API 返回未绑定") + false + } else { + Timber.w("Splash: API 错误 code=${result.code}, 用本地状态兜底") + userPrefs.isBound } } + is ApiResult.NetworkError -> { + Timber.w("Splash: 网络异常, 用本地状态兜底") + userPrefs.isBound + } } } - /** 根据本地 UserPrefs 绑定状态导航(离线兜底) */ - private fun navigateByLocalState() { - if (userPrefs.isBound) { + /** 根据绑定结果导航 */ + private fun navigateByResult(isBound: Boolean) { + if (isBound) { navigateToHome() } else { navigateToBind()