feat: 服务消息UI交互

This commit is contained in:
yangxisong 2025-12-02 15:21:29 +08:00
parent 84961a367c
commit 9c29fee24e
15 changed files with 202 additions and 28 deletions

View File

@ -1,5 +1,6 @@
package com.yuanxuan.rokid package com.yuanxuan.rokid
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import androidx.activity.addCallback import androidx.activity.addCallback
@ -11,9 +12,15 @@ import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment
import com.yuanxuan.rokid.databinding.ActivityMainBinding import com.yuanxuan.rokid.databinding.ActivityMainBinding
import com.yuanxuan.rokid.extension.fadeIn import com.yuanxuan.rokid.extension.fadeIn
import com.yuanxuan.rokid.extension.fadeOut import com.yuanxuan.rokid.extension.fadeOut
import com.yuanxuan.rokid.keeplive.KeepLiveService
import com.yuanxuan.rokid.network.websocket.WebSocketEvent
import com.yuanxuan.rokid.ui.NoticeFragment
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@ -39,8 +46,33 @@ class MainActivity : AppCompatActivity() {
/** /**
* 拦截返回键事件防止返回到桌面 * 拦截返回键事件防止返回到桌面
*/ */
onBackPressedDispatcher.addCallback {} onBackPressedDispatcher.addCallback {
viewModel.wakeUp()
navigationToast("TestMessage")
}
/**
* 这个服务保证心跳包准时发
*/
val serviceIntent = Intent(this, KeepLiveService::class.java)
startForegroundService(serviceIntent)
/**
* 出路服务端事件
*/
lifecycleScope.launch {
viewModel.socketEvent.collect { event ->
when (event) {
is WebSocketEvent.Notice -> {
viewModel.wakeUp()
delay(500)
navigationToast(event.msg)
}
WebSocketEvent.Unknow -> {}
}
}
}
} }
@ -55,6 +87,23 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun navigationToast(toast: String) {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val options = NavOptions.Builder()
.setEnterAnim(android.R.anim.fade_in)
.setExitAnim(android.R.anim.fade_out)
.setPopEnterAnim(android.R.anim.fade_in)
.setPopExitAnim(android.R.anim.fade_out)
.build()
val bundle = Bundle().apply {
putString(NoticeFragment.TOAST_MESSAGE, toast)
}
val navController = navHostFragment.navController
navController.navigate(R.id.notice_fragment, bundle, options)
}
private fun observer() { private fun observer() {
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {

View File

@ -2,6 +2,7 @@ package com.yuanxuan.rokid
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.yuanxuan.rokid.dependencies.AppDependencies
import com.yuanxuan.rokid.dependencies.AppDependencies.deviceServiceManager import com.yuanxuan.rokid.dependencies.AppDependencies.deviceServiceManager
import com.yuanxuan.rokid.device.DeviceServiceManager import com.yuanxuan.rokid.device.DeviceServiceManager
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -37,6 +38,8 @@ class MainViewModel : ViewModel() {
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds),
) )
val socketEvent = AppDependencies.webSocketManager.socketEventFlow
fun quitInstructReceived() { fun quitInstructReceived() {
deviceServiceManager.quitInstructReceived() deviceServiceManager.quitInstructReceived()
} }
@ -47,6 +50,14 @@ class MainViewModel : ViewModel() {
} }
} }
fun wakeUp() {
deviceServiceManager.wakeup()
}
fun playTTS(tts: String) {
deviceServiceManager.playTTS(tts)
}
sealed interface KeyEvent { sealed interface KeyEvent {
data object DpadRight : KeyEvent //前滑 data object DpadRight : KeyEvent //前滑
data object DpadLeft : KeyEvent //后滑 data object DpadLeft : KeyEvent //后滑

View File

@ -1,10 +1,8 @@
package com.yuanxuan.rokid package com.yuanxuan.rokid
import android.app.Application import android.app.Application
import android.content.Intent
import com.yuanxuan.rokid.dependencies.AppDependencies import com.yuanxuan.rokid.dependencies.AppDependencies
import com.yuanxuan.rokid.dependencies.ApplicationDependencyProvider import com.yuanxuan.rokid.dependencies.ApplicationDependencyProvider
import com.yuanxuan.rokid.keeplive.KeepLiveService
import com.yuanxuan.rokid.network.websocket.WebSocketReconnectWorker import com.yuanxuan.rokid.network.websocket.WebSocketReconnectWorker
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -33,11 +31,6 @@ class RokidApplication : Application() {
Timber.e(it) Timber.e(it)
} }
} }
/**
* 这个服务保证心跳包准时发
*/
val serviceIntent = Intent(this, KeepLiveService::class.java)
startForegroundService(serviceIntent)
} }
} }

View File

@ -196,6 +196,13 @@ class DeviceServiceManager(val context: Application) : ConnectivityManager.Netwo
_instructState.update { InstructState.None } _instructState.update { InstructState.None }
} }
fun wakeup() {
systemFuncServiceTodo { service ->
service.wakeUp()
}
}
fun playTTS(msg: String) { fun playTTS(msg: String) {
ttsServiceTodo { ttsServiceTodo {
ttsService?.playTtsMsg(msg) ttsService?.playTtsMsg(msg)

View File

@ -6,8 +6,8 @@ import java.net.NetworkInterface
object NetUtils { object NetUtils {
// fun getBaseUrl() = "http://${getLocalIPV4address()}.70:7070" fun getBaseUrl() = "http://${getLocalIPV4address()}.70:7070"
fun getBaseUrl() = "http://${getLocalIPV4address()}.52:8765" // fun getBaseUrl() = "http://${getLocalIPV4address()}.52:8765"
/** /**

View File

@ -0,0 +1,22 @@
package com.yuanxuan.rokid.network.bean
import com.google.gson.JsonElement
import com.google.gson.annotations.SerializedName
data class WebSocketResponse(
@SerializedName("MsgType")
val msgType: Int,
@SerializedName("Msg")
val msg: JsonElement
) {
enum class MsgType(val value: Int) {
Notice(0);
companion object {
fun fromValue(value: Int) = entries.firstOrNull { it.value == value }
}
}
}

View File

@ -1,11 +1,17 @@
package com.yuanxuan.rokid.network.websocket package com.yuanxuan.rokid.network.websocket
import com.google.gson.Gson
import com.yuanxuan.rokid.network.NetUtils import com.yuanxuan.rokid.network.NetUtils
import com.yuanxuan.rokid.network.bean.WebSocketResponse
import com.yuanxuan.rokid.network.websocket.WebSocketConnection.Companion.DEFAULT_SEND_TIMEOUT import com.yuanxuan.rokid.network.websocket.WebSocketConnection.Companion.DEFAULT_SEND_TIMEOUT
import com.yuanxuan.rokid.network.websocket.WebSocketConnection.Companion.PING_INTERVAL_TIME import com.yuanxuan.rokid.network.websocket.WebSocketConnection.Companion.PING_INTERVAL_TIME
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -13,12 +19,16 @@ import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
import timber.log.Timber import timber.log.Timber
class OkHttpWebSocketConnection() : WebSocketListener(), WebSocketConnection { class OkHttpWebSocketConnection(val scope: CoroutineScope) : WebSocketListener(),
WebSocketConnection {
private val _webSocketConnectionStateFlow: MutableStateFlow<WebSocketConnectionState> = private val _webSocketConnectionStateFlow: MutableStateFlow<WebSocketConnectionState> =
MutableStateFlow(WebSocketConnectionState.DISCONNECTED) MutableStateFlow(WebSocketConnectionState.DISCONNECTED)
val webSocketConnectionStateFlow = _webSocketConnectionStateFlow.asStateFlow() val webSocketConnectionStateFlow = _webSocketConnectionStateFlow.asStateFlow()
private val _eventFlow = MutableSharedFlow<WebSocketEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private var client: WebSocket? = null private var client: WebSocket? = null
@Synchronized @Synchronized
@ -38,6 +48,20 @@ class OkHttpWebSocketConnection() : WebSocketListener(), WebSocketConnection {
override fun onMessage(webSocket: WebSocket, text: String) { override fun onMessage(webSocket: WebSocket, text: String) {
Timber.d(text) Timber.d(text)
val response = Gson().fromJson(text, WebSocketResponse::class.java)
val msgType = WebSocketResponse.MsgType.fromValue(response.msgType)
val event = when (msgType) {
WebSocketResponse.MsgType.Notice -> {
WebSocketEvent.Notice(response.msg.asString)
}
null -> {
WebSocketEvent.Unknow
}
}
scope.launch {
_eventFlow.emit(event)
}
} }
override fun onOpen(webSocket: WebSocket, response: Response) { override fun onOpen(webSocket: WebSocket, response: Response) {

View File

@ -0,0 +1,7 @@
package com.yuanxuan.rokid.network.websocket
sealed interface WebSocketEvent {
data class Notice(val msg: String) : WebSocketEvent
data object Unknow : WebSocketEvent
}

View File

@ -7,7 +7,9 @@ import kotlinx.coroutines.launch
class WebSocketManager(context: Context, scope: CoroutineScope) { class WebSocketManager(context: Context, scope: CoroutineScope) {
private val webSocketConnection = OkHttpWebSocketConnection() private val webSocketConnection = OkHttpWebSocketConnection(scope = scope)
val socketEventFlow = webSocketConnection.eventFlow
init { init {
scope.launch { scope.launch {

View File

@ -2,23 +2,27 @@ package com.yuanxuan.rokid.network.websocket
import android.content.Context import android.content.Context
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.yuanxuan.rokid.dependencies.AppDependencies import com.yuanxuan.rokid.dependencies.AppDependencies
import kotlinx.coroutines.delay
import timber.log.Timber import timber.log.Timber
class WebSocketReconnectWorker(context: Context, workerParams: WorkerParameters) : Worker( class WebSocketReconnectWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(
context, context,
workerParams workerParams
) { ) {
override fun doWork(): Result { override suspend fun doWork(): Result {
/**
* 第一次启动给点时间连wifi重连等5s
*/
delay(5000)
Timber.d("尝试开始重连") Timber.d("尝试开始重连")
AppDependencies.deviceServiceManager.playTTS("开始重连")
AppDependencies.webSocketManager.connect(AppDependencies.deviceServiceManager.sn) AppDependencies.webSocketManager.connect(AppDependencies.deviceServiceManager.sn)
return Result.success() return Result.success()
} }

View File

@ -1,10 +0,0 @@
package com.yuanxuan.rokid.network.websocket
import com.google.gson.annotations.SerializedName
data class WebSocketResponseMessage<T>(
@SerializedName("MsgType")
val msgType: Int,
@SerializedName("Msg")
val msg: T
)

View File

@ -0,0 +1,38 @@
package com.yuanxuan.rokid.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.yuanxuan.rokid.databinding.FragmentNoticeBinding
class NoticeFragment : Fragment() {
companion object {
const val TOAST_MESSAGE = "toast_message"
}
private lateinit var binding: FragmentNoticeBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentNoticeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toast = arguments?.getString(TOAST_MESSAGE)
binding.toast.text = toast
}
override fun onPause() {
super.onPause()
findNavController().navigateUp()
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/toast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="30.dp"
android:paddingEnd="30.dp"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据测试数据" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -14,7 +14,7 @@
android:id="@+id/title" android:id="@+id/title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="#ffffff" android:textColor="@color/white"
android:textSize="10sp" android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -39,4 +39,10 @@
android:label="sn" android:label="sn"
tools:layout="@layout/fragment_sn" /> tools:layout="@layout/fragment_sn" />
<fragment
android:id="@+id/notice_fragment"
android:name="com.yuanxuan.rokid.ui.NoticeFragment"
android:label="notice"
tools:layout="@layout/fragment_notice" />
</navigation> </navigation>