feat: add GatewaySettingsStore and wire up dynamic connection settings (Step 7)
Implement GatewaySettingsStoreImpl with DataStore persistence, add apiKey support to WebSocketClient and ConfigApiClient, and observe settings changes in ClawDroidApp to reconnect WS on config updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
908c1e051b
commit
790cd6a411
8 changed files with 116 additions and 11 deletions
|
|
@ -87,7 +87,11 @@ dependencies {
|
|||
|
||||
implementation(libs.coroutines.android)
|
||||
|
||||
implementation(libs.datastore.preferences)
|
||||
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
implementation(libs.icons.lucide)
|
||||
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,38 @@
|
|||
package io.clawdroid
|
||||
|
||||
import android.app.Application
|
||||
import io.clawdroid.backend.api.GatewaySettingsStore
|
||||
import io.clawdroid.backend.config.configModule
|
||||
import io.clawdroid.core.data.remote.WebSocketClient
|
||||
import io.clawdroid.di.appModule
|
||||
import io.clawdroid.receiver.NotificationHelper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
|
||||
class ClawDroidApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startKoin {
|
||||
val koinApp = startKoin {
|
||||
androidContext(this@ClawDroidApp)
|
||||
modules(appModule)
|
||||
modules(appModule, configModule)
|
||||
}
|
||||
NotificationHelper.createNotificationChannel(this)
|
||||
|
||||
val koin = koinApp.koin
|
||||
val settingsStore: GatewaySettingsStore = koin.get()
|
||||
val wsClient: WebSocketClient = koin.get()
|
||||
val scope: CoroutineScope = koin.get()
|
||||
scope.launch {
|
||||
settingsStore.settings.collect { s ->
|
||||
if (wsClient.wsUrl != s.wsUrl || wsClient.apiKey != s.apiKey) {
|
||||
wsClient.disconnect()
|
||||
wsClient.wsUrl = s.wsUrl
|
||||
wsClient.apiKey = s.apiKey
|
||||
wsClient.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,15 @@ import io.clawdroid.core.domain.usecase.ObserveConnectionUseCase
|
|||
import io.clawdroid.core.domain.usecase.ObserveMessagesUseCase
|
||||
import io.clawdroid.core.domain.usecase.ObserveStatusUseCase
|
||||
import io.clawdroid.core.domain.usecase.SendMessageUseCase
|
||||
import io.clawdroid.backend.api.GatewaySettingsStore
|
||||
import io.clawdroid.feature.chat.ChatViewModel
|
||||
import io.clawdroid.feature.chat.SettingsViewModel
|
||||
import io.clawdroid.feature.chat.voice.SpeechRecognizerWrapper
|
||||
import io.clawdroid.feature.chat.voice.TextToSpeechWrapper
|
||||
import io.clawdroid.feature.chat.voice.CameraCaptureManager
|
||||
import io.clawdroid.feature.chat.voice.VoiceModeManager
|
||||
import io.clawdroid.settings.AppSettingsViewModel
|
||||
import io.clawdroid.settings.GatewaySettingsStoreImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
|
@ -44,6 +47,9 @@ val appModule = module {
|
|||
// CoroutineScope
|
||||
single { CoroutineScope(SupervisorJob() + Dispatchers.IO) }
|
||||
|
||||
// GatewaySettingsStore
|
||||
single<GatewaySettingsStore> { GatewaySettingsStoreImpl(androidContext(), get()) }
|
||||
|
||||
// Room
|
||||
single {
|
||||
Room.databaseBuilder(
|
||||
|
|
@ -72,7 +78,11 @@ val appModule = module {
|
|||
val clientId = prefs.getString("client_id", null) ?: UUID.randomUUID().toString().also {
|
||||
prefs.edit().putString("client_id", it).apply()
|
||||
}
|
||||
WebSocketClient(get(), get(), clientId)
|
||||
val gwSettings = get<GatewaySettingsStore>().settings.value
|
||||
WebSocketClient(get(), get(), clientId).apply {
|
||||
wsUrl = gwSettings.wsUrl
|
||||
apiKey = gwSettings.apiKey
|
||||
}
|
||||
}
|
||||
|
||||
// ImageFileStorage
|
||||
|
|
@ -124,4 +134,5 @@ val appModule = module {
|
|||
// ViewModel
|
||||
viewModel { ChatViewModel(get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
viewModel { SettingsViewModel(get(), get(), get()) }
|
||||
viewModel { AppSettingsViewModel(get()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package io.clawdroid.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import io.clawdroid.backend.api.GatewaySettings
|
||||
import io.clawdroid.backend.api.GatewaySettingsStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
private val Context.gatewayDataStore by preferencesDataStore(name = "gateway_settings")
|
||||
|
||||
class GatewaySettingsStoreImpl(
|
||||
private val context: Context,
|
||||
scope: CoroutineScope,
|
||||
) : GatewaySettingsStore {
|
||||
|
||||
private object Keys {
|
||||
val WS_PORT = intPreferencesKey("ws_port")
|
||||
val HTTP_PORT = intPreferencesKey("http_port")
|
||||
val API_KEY = stringPreferencesKey("api_key")
|
||||
}
|
||||
|
||||
override val settings: StateFlow<GatewaySettings> =
|
||||
context.gatewayDataStore.data.map { prefs ->
|
||||
GatewaySettings(
|
||||
wsPort = prefs[Keys.WS_PORT] ?: DEFAULT.wsPort,
|
||||
httpPort = prefs[Keys.HTTP_PORT] ?: DEFAULT.httpPort,
|
||||
apiKey = prefs[Keys.API_KEY] ?: DEFAULT.apiKey,
|
||||
)
|
||||
}.stateIn(scope, SharingStarted.Eagerly, DEFAULT)
|
||||
|
||||
companion object {
|
||||
private val DEFAULT = GatewaySettings()
|
||||
}
|
||||
|
||||
override suspend fun update(settings: GatewaySettings) {
|
||||
context.gatewayDataStore.edit { prefs ->
|
||||
prefs[Keys.WS_PORT] = settings.wsPort
|
||||
prefs[Keys.HTTP_PORT] = settings.httpPort
|
||||
prefs[Keys.API_KEY] = settings.apiKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,4 +4,7 @@ data class GatewaySettings(
|
|||
val wsPort: Int = 18793,
|
||||
val httpPort: Int = 18790,
|
||||
val apiKey: String = "",
|
||||
)
|
||||
) {
|
||||
val wsUrl: String get() = "ws://127.0.0.1:$wsPort/ws"
|
||||
val httpBaseUrl: String get() = "http://127.0.0.1:$httpPort"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package io.clawdroid.backend.config
|
||||
|
||||
import io.clawdroid.backend.api.GatewaySettingsStore
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.put
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
|
|
@ -41,8 +43,12 @@ data class SaveConfigResult(
|
|||
val error: String? = null,
|
||||
)
|
||||
|
||||
class ConfigApiClient : Closeable {
|
||||
private val baseUrl: String get() = "http://127.0.0.1:18790"
|
||||
class ConfigApiClient(private val settingsStore: GatewaySettingsStore) : Closeable {
|
||||
private val baseUrl: String
|
||||
get() = settingsStore.settings.value.httpBaseUrl
|
||||
|
||||
private val apiKey: String
|
||||
get() = settingsStore.settings.value.apiKey
|
||||
|
||||
private val client = HttpClient(OkHttp) {
|
||||
install(ContentNegotiation) {
|
||||
|
|
@ -51,17 +57,22 @@ class ConfigApiClient : Closeable {
|
|||
}
|
||||
|
||||
suspend fun getSchema(): ConfigSchema {
|
||||
return client.get("$baseUrl/api/config/schema").ensureSuccess().body()
|
||||
return client.get("$baseUrl/api/config/schema") {
|
||||
if (apiKey.isNotEmpty()) header("Authorization", "Bearer $apiKey")
|
||||
}.ensureSuccess().body()
|
||||
}
|
||||
|
||||
suspend fun getConfig(): JsonObject {
|
||||
return client.get("$baseUrl/api/config").ensureSuccess().body()
|
||||
return client.get("$baseUrl/api/config") {
|
||||
if (apiKey.isNotEmpty()) header("Authorization", "Bearer $apiKey")
|
||||
}.ensureSuccess().body()
|
||||
}
|
||||
|
||||
suspend fun saveConfig(config: JsonObject): SaveConfigResult {
|
||||
return client.put("$baseUrl/api/config") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(config)
|
||||
if (apiKey.isNotEmpty()) header("Authorization", "Bearer $apiKey")
|
||||
}.ensureSuccess().body()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ import org.koin.core.module.dsl.withOptions
|
|||
import org.koin.dsl.module
|
||||
|
||||
val configModule = module {
|
||||
single { ConfigApiClient() } withOptions { callbacks = Callbacks(onClose = { it?.close() }) }
|
||||
single { ConfigApiClient(get()) } withOptions { callbacks = Callbacks(onClose = { it?.close() }) }
|
||||
viewModel { ConfigViewModel(get()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import kotlinx.coroutines.isActive
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.URLEncoder
|
||||
|
||||
class WebSocketClient(
|
||||
private val client: HttpClient,
|
||||
|
|
@ -40,7 +41,8 @@ class WebSocketClient(
|
|||
private var connectJob: Job? = null
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
var wsUrl: String = "ws://127.0.0.1:18793/ws"
|
||||
@Volatile var wsUrl: String = "ws://127.0.0.1:18793/ws"
|
||||
@Volatile var apiKey: String = ""
|
||||
|
||||
fun connect() {
|
||||
if (connectJob?.isActive == true) return
|
||||
|
|
@ -49,7 +51,12 @@ class WebSocketClient(
|
|||
while (isActive) {
|
||||
try {
|
||||
_connectionState.value = ConnectionState.CONNECTING
|
||||
val url = "$wsUrl?client_id=$clientId&client_type=$clientType"
|
||||
val currentWsUrl = wsUrl
|
||||
val currentApiKey = apiKey
|
||||
val url = buildString {
|
||||
append("$currentWsUrl?client_id=$clientId&client_type=$clientType")
|
||||
if (currentApiKey.isNotEmpty()) append("&api_key=${URLEncoder.encode(currentApiKey, "UTF-8")}")
|
||||
}
|
||||
client.webSocket(url) {
|
||||
session = this
|
||||
_connectionState.value = ConnectionState.CONNECTED
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue