refactor: 更新Android网络和配置
- 更新ApiService以支持认证 - 更新WebSocketClient以支持认证连接 - 更新WebSocketMessage模型 - 更新AppConfig配置 - 更新MainActivity以支持认证导航 - 更新AppNavigation添加认证相关路由 - 更新VoiceRecorder功能
This commit is contained in:
@@ -6,9 +6,25 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -16,7 +32,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@@ -24,10 +39,9 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import android.view.WindowManager
|
import com.huaga.life_echo.data.auth.TokenManager
|
||||||
import com.huaga.life_echo.navigation.AppNavigation
|
import com.huaga.life_echo.navigation.AppNavigation
|
||||||
import com.huaga.life_echo.navigation.Screen
|
import com.huaga.life_echo.navigation.Screen
|
||||||
import com.huaga.life_echo.ui.icons.AppIcons
|
import com.huaga.life_echo.ui.icons.AppIcons
|
||||||
@@ -37,6 +51,9 @@ import com.huaga.life_echo.ui.theme.LightPurple
|
|||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// 初始化TokenManager
|
||||||
|
TokenManager.initialize(this)
|
||||||
// 启用边缘到边缘显示
|
// 启用边缘到边缘显示
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
// 设置系统栏透明
|
// 设置系统栏透明
|
||||||
@@ -167,20 +184,4 @@ enum class AppDestinations(
|
|||||||
CHAT("聊天", AppIcons.Chat),
|
CHAT("聊天", AppIcons.Chat),
|
||||||
MEMOIR("回忆录", AppIcons.Memoir),
|
MEMOIR("回忆录", AppIcons.Memoir),
|
||||||
PROFILE("我的", AppIcons.Profile),
|
PROFILE("我的", AppIcons.Profile),
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
|
||||||
Text(
|
|
||||||
text = "Hello $name!",
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
fun GreetingPreview() {
|
|
||||||
LifeechoTheme {
|
|
||||||
Greeting("Android")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,4 @@ object AppConfig {
|
|||||||
|
|
||||||
// 生产环境应该从配置文件或环境变量读取
|
// 生产环境应该从配置文件或环境变量读取
|
||||||
// const val BASE_URL = BuildConfig.API_BASE_URL
|
// const val BASE_URL = BuildConfig.API_BASE_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.huaga.life_echo.feature.voice
|
package com.huaga.life_echo.feature.voice
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -8,8 +7,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.util.UUID
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class VoiceRecorder(private val context: Context) {
|
class VoiceRecorder(private val context: Context) {
|
||||||
private var mediaRecorder: MediaRecorder? = null
|
private var mediaRecorder: MediaRecorder? = null
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import androidx.navigation.navArgument
|
|||||||
import com.huaga.life_echo.ui.screens.ConversationListScreen
|
import com.huaga.life_echo.ui.screens.ConversationListScreen
|
||||||
import com.huaga.life_echo.ui.screens.CreateMemoryScreen
|
import com.huaga.life_echo.ui.screens.CreateMemoryScreen
|
||||||
import com.huaga.life_echo.ui.screens.ExportDataScreen
|
import com.huaga.life_echo.ui.screens.ExportDataScreen
|
||||||
|
import com.huaga.life_echo.ui.screens.LoginScreen
|
||||||
import com.huaga.life_echo.ui.screens.MyMemoirScreen
|
import com.huaga.life_echo.ui.screens.MyMemoirScreen
|
||||||
import com.huaga.life_echo.ui.screens.MyOrdersScreen
|
import com.huaga.life_echo.ui.screens.MyOrdersScreen
|
||||||
import com.huaga.life_echo.ui.screens.ProfileScreen
|
import com.huaga.life_echo.ui.screens.ProfileScreen
|
||||||
|
import com.huaga.life_echo.ui.screens.RegisterScreen
|
||||||
import com.huaga.life_echo.ui.screens.UpgradePlanScreen
|
import com.huaga.life_echo.ui.screens.UpgradePlanScreen
|
||||||
|
|
||||||
sealed class Screen(val route: String) {
|
sealed class Screen(val route: String) {
|
||||||
@@ -21,6 +23,8 @@ sealed class Screen(val route: String) {
|
|||||||
}
|
}
|
||||||
object MyMemoir : Screen("my_memoir")
|
object MyMemoir : Screen("my_memoir")
|
||||||
object Profile : Screen("profile")
|
object Profile : Screen("profile")
|
||||||
|
object Login : Screen("login")
|
||||||
|
object Register : Screen("register")
|
||||||
object UpgradePlan : Screen("upgrade_plan")
|
object UpgradePlan : Screen("upgrade_plan")
|
||||||
object MyOrders : Screen("my_orders")
|
object MyOrders : Screen("my_orders")
|
||||||
object ExportData : Screen("export_data")
|
object ExportData : Screen("export_data")
|
||||||
@@ -64,6 +68,26 @@ fun AppNavigation(navController: NavHostController) {
|
|||||||
composable(Screen.ExportData.route) {
|
composable(Screen.ExportData.route) {
|
||||||
ExportDataScreen(navController = navController)
|
ExportDataScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
composable(Screen.Login.route) {
|
||||||
|
LoginScreen(
|
||||||
|
onLoginSuccess = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onNavigateToRegister = {
|
||||||
|
navController.navigate(Screen.Register.route)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable(Screen.Register.route) {
|
||||||
|
RegisterScreen(
|
||||||
|
onRegisterSuccess = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onNavigateBack = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.huaga.life_echo.network
|
package com.huaga.life_echo.network
|
||||||
|
|
||||||
|
import com.huaga.life_echo.data.auth.TokenManager
|
||||||
|
import com.huaga.life_echo.network.interceptors.AuthInterceptorPlugin
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.engine.android.*
|
import io.ktor.client.engine.android.*
|
||||||
@@ -30,7 +32,10 @@ data class BookDto(
|
|||||||
val totalWords: Int
|
val totalWords: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
class ApiService {
|
class ApiService(
|
||||||
|
tokenManager: TokenManager? = null,
|
||||||
|
authService: AuthService? = null
|
||||||
|
) {
|
||||||
private val client = HttpClient(Android) {
|
private val client = HttpClient(Android) {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json {
|
json(Json {
|
||||||
@@ -40,15 +45,24 @@ class ApiService {
|
|||||||
install(Logging) {
|
install(Logging) {
|
||||||
level = LogLevel.INFO
|
level = LogLevel.INFO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果提供了tokenManager和authService,安装认证拦截器
|
||||||
|
if (tokenManager != null && authService != null) {
|
||||||
|
install(AuthInterceptorPlugin) {
|
||||||
|
this.tokenManager = tokenManager
|
||||||
|
this.authService = authService
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BASE_URL = com.huaga.life_echo.config.AppConfig.BASE_URL
|
private const val BASE_URL = com.huaga.life_echo.config.AppConfig.BASE_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getChapters(): List<ChapterDto> {
|
suspend fun getChapters(userId: String = "default_user"): List<ChapterDto> {
|
||||||
return client.get("$BASE_URL/api/chapters") {
|
return client.get("$BASE_URL/api/chapters") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
|
parameter("user_id", userId)
|
||||||
}.body()
|
}.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +72,10 @@ class ApiService {
|
|||||||
}.body()
|
}.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun exportPdf(bookId: String): ByteArray {
|
suspend fun exportPdf(bookId: String, userId: String = "default_user"): ByteArray {
|
||||||
return client.post("$BASE_URL/api/books/export-pdf") {
|
return client.post("$BASE_URL/api/books/export-pdf") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(mapOf("book_id" to bookId))
|
setBody(mapOf("book_id" to bookId, "user_id" to userId))
|
||||||
}.body()
|
}.body()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class WebSocketClient {
|
|||||||
private var reconnectJob: Job? = null
|
private var reconnectJob: Job? = null
|
||||||
private var isConnected = false
|
private var isConnected = false
|
||||||
private var currentConversationId: String? = null
|
private var currentConversationId: String? = null
|
||||||
|
private var currentToken: String? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BASE_URL = com.huaga.life_echo.config.AppConfig.WS_BASE_URL
|
private const val BASE_URL = com.huaga.life_echo.config.AppConfig.WS_BASE_URL
|
||||||
@@ -41,9 +42,15 @@ class WebSocketClient {
|
|||||||
|
|
||||||
suspend fun connect(
|
suspend fun connect(
|
||||||
conversationId: String,
|
conversationId: String,
|
||||||
|
token: String? = null,
|
||||||
onMessage: (WebSocketMessage) -> Unit
|
onMessage: (WebSocketMessage) -> Unit
|
||||||
) {
|
) {
|
||||||
val url = "$BASE_URL/ws/conversation/$conversationId"
|
val baseUrl = "$BASE_URL/ws/conversation/$conversationId"
|
||||||
|
val url = if (token != null) {
|
||||||
|
"$baseUrl?token=$token"
|
||||||
|
} else {
|
||||||
|
baseUrl
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session = client.webSocketSession {
|
session = client.webSocketSession {
|
||||||
@@ -53,6 +60,7 @@ class WebSocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentConversationId = conversationId
|
currentConversationId = conversationId
|
||||||
|
currentToken = token
|
||||||
isConnected = true
|
isConnected = true
|
||||||
|
|
||||||
// 启动消息接收协程
|
// 启动消息接收协程
|
||||||
@@ -112,6 +120,14 @@ class WebSocketClient {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun sendTextMessage(text: String, conversationId: String) {
|
||||||
|
sendMessage(WebSocketMessage(
|
||||||
|
type = MessageType.text,
|
||||||
|
conversation_id = conversationId,
|
||||||
|
data = mapOf("text" to text)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun sendEndConversation(conversationId: String) {
|
suspend fun sendEndConversation(conversationId: String) {
|
||||||
sendMessage(WebSocketMessage(
|
sendMessage(WebSocketMessage(
|
||||||
type = MessageType.end_conversation,
|
type = MessageType.end_conversation,
|
||||||
@@ -139,8 +155,9 @@ class WebSocketClient {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
val conversationId = currentConversationId
|
val conversationId = currentConversationId
|
||||||
|
val token = currentToken
|
||||||
if (conversationId != null) {
|
if (conversationId != null) {
|
||||||
connect(conversationId, onMessage)
|
connect(conversationId, token, onMessage)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
reconnectWithBackoff(onMessage, attempt + 1)
|
reconnectWithBackoff(onMessage, attempt + 1)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable
|
|||||||
enum class MessageType {
|
enum class MessageType {
|
||||||
connect,
|
connect,
|
||||||
audio_chunk,
|
audio_chunk,
|
||||||
|
text, // 文本消息
|
||||||
transcript,
|
transcript,
|
||||||
agent_response,
|
agent_response,
|
||||||
tts_audio,
|
tts_audio,
|
||||||
|
|||||||
Reference in New Issue
Block a user