From 1b61abb38040e75d784334d0ca4bb9468067363e Mon Sep 17 00:00:00 2001 From: dongliang Date: Mon, 27 Apr 2026 16:23:37 +0930 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=BE=E5=A4=87=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E4=B8=8E=E9=85=8D=E5=AF=B9=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增: - SplashFragment 启动分发(初始化+绑定检查+导航到Home或Bind) - BindFragment 二维码配对页面(ZXing生成QR码+MQTT绑定监听) - WatchBindInfo 绑定信息数据类 修改: - nav_main.xml startDestination改为SplashFragment,添加导航action - HomeFragment 移除初始化逻辑到Splash,添加MQTT解绑处理 - CommonApi getWatchByImei返回类型改为WatchBindInfo Co-Authored-By: Claude Opus 4.6 (1M context) --- .../xiaoqu/watch/data/device/WatchBindInfo.kt | 14 ++ .../com/xiaoqu/watch/network/api/CommonApi.kt | 5 +- .../com/xiaoqu/watch/ui/bind/BindFragment.kt | 143 ++++++++++++++++++ .../xiaoqu/watch/ui/common/SplashFragment.kt | 136 +++++++++++++++++ .../com/xiaoqu/watch/ui/home/HomeFragment.kt | 30 +--- app/src/main/res/layout/fragment_bind.xml | 44 +++++- app/src/main/res/layout/fragment_splash.xml | 6 + app/src/main/res/navigation/nav_main.xml | 43 +++++- 8 files changed, 389 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/xiaoqu/watch/data/device/WatchBindInfo.kt create mode 100644 app/src/main/java/com/xiaoqu/watch/ui/common/SplashFragment.kt create mode 100644 app/src/main/res/layout/fragment_splash.xml diff --git a/app/src/main/java/com/xiaoqu/watch/data/device/WatchBindInfo.kt b/app/src/main/java/com/xiaoqu/watch/data/device/WatchBindInfo.kt new file mode 100644 index 0000000..76b2c72 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/data/device/WatchBindInfo.kt @@ -0,0 +1,14 @@ +package com.xiaoqu.watch.data.device + +import com.google.gson.annotations.SerializedName + +/** + * 设备绑定信息数据类 + * 对应 getWatchByImei API 返回和 MQTT messageType=2 消息中的用户信息 + */ +data class WatchBindInfo( + @SerializedName("userId") val userId: Long = 0, + @SerializedName("mobile") val mobile: String = "", + @SerializedName("userName") val userName: String = "", + @SerializedName("headUrl") val headUrl: String = "" +) diff --git a/app/src/main/java/com/xiaoqu/watch/network/api/CommonApi.kt b/app/src/main/java/com/xiaoqu/watch/network/api/CommonApi.kt index cb4087b..306b07f 100644 --- a/app/src/main/java/com/xiaoqu/watch/network/api/CommonApi.kt +++ b/app/src/main/java/com/xiaoqu/watch/network/api/CommonApi.kt @@ -1,5 +1,6 @@ package com.xiaoqu.watch.network.api +import com.xiaoqu.watch.data.device.WatchBindInfo import com.xiaoqu.watch.network.ApiResponse import retrofit2.http.Body import retrofit2.http.GET @@ -16,9 +17,9 @@ interface CommonApi { @POST("watch/bindWatchConfirm") suspend fun bindWatchConfirm(@Body params: Map): ApiResponse - /** 根据 IMEI 查询手表信息 */ + /** 根据 IMEI 查询手表绑定信息(返回用户数据则已绑定,否则未绑定) */ @GET("watch/getWatchByImei") - suspend fun getWatchByImei(@Query("imei") imei: String): ApiResponse + suspend fun getWatchByImei(@Query("imei") imei: String): ApiResponse /** 检查版本更新 */ @GET("newAppVersion/queryWatch") 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 729b2f6..0e3055f 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 @@ -1,15 +1,158 @@ package com.xiaoqu.watch.ui.bind +import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.google.gson.Gson +import com.google.zxing.BarcodeFormat +import com.google.zxing.MultiFormatWriter +import com.google.zxing.common.BitMatrix +import android.graphics.Bitmap +import com.xiaoqu.watch.R +import com.xiaoqu.watch.data.device.WatchBindInfo +import com.xiaoqu.watch.data.prefs.DevicePrefs +import com.xiaoqu.watch.data.prefs.UserPrefs import com.xiaoqu.watch.databinding.FragmentBindBinding +import com.xiaoqu.watch.event.AppEvent +import com.xiaoqu.watch.event.EventBus +import com.xiaoqu.watch.network.api.CommonApi +import com.xiaoqu.watch.network.safeApiCall import com.xiaoqu.watch.ui.common.BaseFragment import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject +/** + * 设备绑定页面 + * 显示二维码供手机 APP 扫码绑定 + * 监听 MQTT messageType=2 绑定成功消息 + */ @AndroidEntryPoint class BindFragment : BaseFragment() { + @Inject lateinit var devicePrefs: DevicePrefs + @Inject lateinit var userPrefs: UserPrefs + @Inject lateinit var eventBus: EventBus + @Inject lateinit var commonApi: CommonApi + @Inject lateinit var gson: Gson + override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBindBinding { return FragmentBindBinding.inflate(inflater, container, false) } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // 生成并显示二维码 + generateQrCode() + + // 监听 MQTT 绑定消息 + observeBindEvent() + } + + /** + * 生成二维码 + * 编码设备信息 JSON:{bluetoothName, imei, serial, power} + */ + private fun generateQrCode() { + try { + // 构建设备信息 JSON(与旧版 codeScanPair.vue 一致) + val qrData = gson.toJson(mapOf( + "bluetoothName" to devicePrefs.bluetoothName, + "imei" to devicePrefs.imei, + "serial" to devicePrefs.serial, + "power" to 100 // TODO: 接入实时电量 + )) + Timber.d("绑定: QR 数据=$qrData") + + // 使用 ZXing 生成二维码 Bitmap + val size = 500 // 生成 500×500 像素,ImageView 会缩放 + val bitMatrix: BitMatrix = MultiFormatWriter().encode( + qrData, BarcodeFormat.QR_CODE, size, size + ) + val bitmap = bitMatrixToBitmap(bitMatrix) + binding.ivQrCode.setImageBitmap(bitmap) + + } catch (e: Exception) { + Timber.e(e, "绑定: 二维码生成失败") + } + } + + /** + * 将 ZXing BitMatrix 转换为 Android Bitmap + * 黑色像素用黑色,白色像素用白色 + */ + private fun bitMatrixToBitmap(matrix: BitMatrix): Bitmap { + val width = matrix.width + val height = matrix.height + val pixels = IntArray(width * height) + for (y in 0 until height) { + for (x in 0 until width) { + pixels[y * width + x] = if (matrix[x, y]) { + 0xFF000000.toInt() // 黑色 + } else { + 0xFFFFFFFF.toInt() // 白色 + } + } + } + return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888) + } + + /** + * 监听 MQTT 绑定成功消息(messageType=2) + * 收到后存储用户信息 → 调用 API 确认 → 导航到首页 + */ + private fun observeBindEvent() { + viewLifecycleOwner.lifecycleScope.launch { + eventBus.events.collect { event -> + if (event is AppEvent.MqttMessageReceived && event.type == 2) { + handleBindSuccess(event.rawJson) + } + } + } + } + + /** + * 处理绑定成功 + * 1. 解析用户信息并存入 UserPrefs + * 2. 调用 bindWatchConfirm API 确认 + * 3. 导航到首页 + */ + private fun handleBindSuccess(rawJson: String) { + try { + Timber.d("绑定: 收到绑定消息 $rawJson") + + // 解析用户信息 + val bindInfo = gson.fromJson(rawJson, WatchBindInfo::class.java) + + // 存入 UserPrefs + userPrefs.saveUser( + userId = bindInfo.userId, + mobile = bindInfo.mobile, + userName = bindInfo.userName, + headUrl = bindInfo.headUrl + ) + + // 调用 API 确认绑定 + viewLifecycleOwner.lifecycleScope.launch { + val params = mapOf( + "imei" to devicePrefs.imei, + "userId" to bindInfo.userId + ) + safeApiCall { commonApi.bindWatchConfirm(params) } + Timber.d("绑定: 确认 API 已调用") + } + + // 导航到首页 + Timber.d("绑定: 绑定成功,导航到首页") + findNavController().navigate(R.id.action_bind_to_home) + + } catch (e: Exception) { + Timber.e(e, "绑定: 处理绑定消息异常") + } + } } 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 new file mode 100644 index 0000000..742c358 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/ui/common/SplashFragment.kt @@ -0,0 +1,136 @@ +package com.xiaoqu.watch.ui.common + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +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.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.launch +import timber.log.Timber +import javax.inject.Inject + +/** + * 启动分发页面 + * 职责:初始化设备信息 → 连接 MQTT → 检查绑定状态 → 导航到首页或绑定页 + * 纯逻辑,无 UI(黑色背景,用户看不到此页面) + */ +@AndroidEntryPoint +class SplashFragment : Fragment() { + + @Inject lateinit var devicePrefs: DevicePrefs + @Inject lateinit var userPrefs: UserPrefs + @Inject lateinit var mqttManager: MqttManager + @Inject lateinit var commonApi: CommonApi + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.fragment_splash, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // 1. 初始化设备信息(首次启动时写入 SP) + initDevicePrefs() + + // 2. 连接 MQTT(需要 IMEI,所以必须在 initDevicePrefs 之后) + mqttManager.connect() + + // 3. 检查绑定状态并导航 + checkBindStatus() + } + + /** 首次启动时初始化设备信息到 device_prefs */ + private fun initDevicePrefs() { + if (!devicePrefs.isInitialized) { + val info = DeviceUtil.getDeviceInfo(requireContext()) + devicePrefs.saveDeviceInfo( + imei = info.imei, serial = info.serial, + bluetoothName = info.bluetoothName, bluetoothMac = info.bluetoothMac, + brand = info.brand, model = info.model, + osVersion = info.osVersion, totalMemory = info.totalMemory + ) + Timber.d("Splash: 设备信息已初始化") + } + } + + /** + * 检查绑定状态: + * 1. 调用 API 获取最新绑定状态 + * 2. 有数据 → 已绑定 → 更新 UserPrefs → 导航到首页 + * 3. 无数据 → 未绑定 → 导航到绑定页 + * 4. 网络异常 → 用本地 isBound 兜底 + */ + private fun checkBindStatus() { + viewLifecycleOwner.lifecycleScope.launch { + val imei = devicePrefs.imei + + // 调用 API 检查绑定状态 + val result = safeApiCall { commonApi.getWatchByImei(imei) } + + when (result) { + is ApiResult.Success -> { + // API 返回数据 → 已绑定 + Timber.d("Splash: API 返回已绑定") + // TODO: 解析返回数据更新 UserPrefs(等确认实际字段后完善) + navigateToHome() + } + 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() + } + } + } + } + + /** 根据本地 UserPrefs 绑定状态导航(离线兜底) */ + private fun navigateByLocalState() { + if (userPrefs.isBound) { + navigateToHome() + } else { + navigateToBind() + } + } + + /** 导航到首页(popUpTo splash 防止返回) */ + private fun navigateToHome() { + Timber.d("Splash: → HomeFragment") + findNavController().navigate( + R.id.action_splash_to_home + ) + } + + /** 导航到绑定页(popUpTo splash 防止返回) */ + private fun navigateToBind() { + Timber.d("Splash: → BindFragment") + findNavController().navigate( + R.id.action_splash_to_bind + ) + } +} diff --git a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt index c8237a4..3c13bdb 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/home/HomeFragment.kt @@ -16,12 +16,10 @@ import com.xiaoqu.watch.device.sensor.VibrationController import com.xiaoqu.watch.device.sensor.VibrationDefaults import com.xiaoqu.watch.event.AppEvent import com.xiaoqu.watch.event.EventBus -import com.xiaoqu.watch.service.manager.MqttManager import com.xiaoqu.watch.ui.common.BaseFragment import com.xiaoqu.watch.ui.widget.NavBarHelper import com.xiaoqu.watch.ui.widget.QuTipDialog import com.xiaoqu.watch.util.DateUtil -import com.xiaoqu.watch.util.DeviceUtil import com.xiaoqu.watch.util.NetworkUtil import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay @@ -43,7 +41,6 @@ class HomeFragment : BaseFragment() { @Inject lateinit var nfcController: NfcController @Inject lateinit var vibrationController: VibrationController @Inject lateinit var eventBus: EventBus - @Inject lateinit var mqttManager: MqttManager /** 提示弹窗 */ private lateinit var tipDialog: QuTipDialog @@ -62,15 +59,9 @@ class HomeFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // 初始化设备信息 - initDevicePrefs() - - // 设置 NavBar 为首页模式 + // 设置 NavBar 为首页模式(initDevicePrefs 和 MQTT 已在 SplashFragment 完成) NavBarHelper.setupHomePage(binding.root) - // 连接 MQTT(必须在 initDevicePrefs 之后,确保 IMEI 可用) - mqttManager.connect() - // 初始化弹窗 val dialogContainer = requireActivity().findViewById(R.id.dialog_container) tipDialog = QuTipDialog(dialogContainer) @@ -97,19 +88,6 @@ class HomeFragment : BaseFragment() { } } - /** 首次启动时初始化设备信息到 device_prefs */ - private fun initDevicePrefs() { - if (!devicePrefs.isInitialized) { - val info = DeviceUtil.getDeviceInfo(requireContext()) - devicePrefs.saveDeviceInfo( - imei = info.imei, serial = info.serial, - bluetoothName = info.bluetoothName, bluetoothMac = info.bluetoothMac, - brand = info.brand, model = info.model, - osVersion = info.osVersion, totalMemory = info.totalMemory - ) - } - } - /** 更新状态信息显示(try-catch 保护,防止系统关机时 DeadSystemException) */ private fun updateStatus() { try { @@ -224,6 +202,12 @@ class HomeFragment : BaseFragment() { is AppEvent.MqttDisconnected -> updateStatus() is AppEvent.MqttMessageReceived -> { Timber.d("MQTT消息: type=${event.type}") + // messageType=3: 解绑 → 清除用户数据,导航到绑定页 + if (event.type == 3) { + userPrefs.clear() + findMainNavController().navigate(R.id.action_home_to_bind) + return@collect + } updateStatus() } else -> {} // 其他事件不处理 diff --git a/app/src/main/res/layout/fragment_bind.xml b/app/src/main/res/layout/fragment_bind.xml index e41e04c..46d4f5b 100644 --- a/app/src/main/res/layout/fragment_bind.xml +++ b/app/src/main/res/layout/fragment_bind.xml @@ -1,9 +1,45 @@ - + + android:background="@color/background" + android:gravity="center" + android:orientation="vertical" + android:padding="@dimen/safe_area_left"> - + + - + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml new file mode 100644 index 0000000..8bd72b4 --- /dev/null +++ b/app/src/main/res/layout/fragment_splash.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/navigation/nav_main.xml b/app/src/main/res/navigation/nav_main.xml index 4028bb9..cce4614 100644 --- a/app/src/main/res/navigation/nav_main.xml +++ b/app/src/main/res/navigation/nav_main.xml @@ -2,19 +2,56 @@ + app:startDestination="@id/splashFragment"> + + + + + + + + + + + android:label="首页"> + + + + + android:label="设备绑定"> + + + +