package cc.unitmesh.config import android.content.Context import cc.unitmesh.agent.config.ToolConfigFile import cc.unitmesh.agent.mcp.McpServerConfig import cc.unitmesh.llm.NamedModelConfig import cc.unitmesh.yaml.YamlUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File /** * Android implementation of ConfigManager * Uses Android Context for proper file storage location * * Best Practice: Use app-specific internal storage * - Files are private to the app * - Automatically cleaned up when app is uninstalled * - No special permissions required */ actual object ConfigManager { var appContext: Context? = null /** * Initialize ConfigManager with Android Context * Should be called once in Application.onCreate() */ fun initialize(context: Context) { appContext = context.applicationContext } private fun getConfigDir(): File { val context = appContext ?: throw IllegalStateException("ConfigManager not initialized. Call ConfigManager.initialize(context) first.") // Use app-specific internal storage directory // Path: /data/data/your.package.name/files/.autodev/ return File(context.filesDir, ".autodev") } private fun getConfigFile(): File { return File(getConfigDir(), "config.yaml") } private fun getToolConfigFile(): File { return File(getConfigDir(), "mcp.json") } // JSON parser for potential JSON config support private val json = Json { prettyPrint = true ignoreUnknownKeys = true } actual suspend fun load(): AutoDevConfigWrapper = withContext(Dispatchers.IO) { try { val file = getConfigFile() if (!file.exists()) { return@withContext createEmpty() } val content = file.readText() val configFileData = parseYamlConfig(content) AutoDevConfigWrapper(configFileData) } catch (e: Exception) { println("Error loading config: ${e.message}") createEmpty() } } actual suspend fun save(configFile: ConfigFile) = withContext(Dispatchers.IO) { try { val dir = getConfigDir() dir.mkdirs() val file = getConfigFile() val yamlContent = toYaml(configFile) file.writeText(yamlContent) } catch (e: Exception) { println("Error saving config: ${e.message}") throw e } } actual suspend fun saveConfig( config: NamedModelConfig, setActive: Boolean ) { val wrapper = load() val configFile = wrapper.configFile val existingIndex = configFile.configs.indexOfFirst { it.name == config.name } val updatedConfigs = if (existingIndex >= 0) { configFile.configs.toMutableList().apply { set(existingIndex, config) } } else { configFile.configs + config } val updatedConfigFile = configFile.copy( active = if (setActive) config.name else configFile.active, configs = updatedConfigs ) save(updatedConfigFile) } actual suspend fun deleteConfig(name: String) { val wrapper = load() val configFile = wrapper.configFile val updatedConfigs = configFile.configs.filter { it.name != name } val updatedActive = if (configFile.active == name && updatedConfigs.isNotEmpty()) { updatedConfigs.first().name } else { configFile.active } save(configFile.copy(active = updatedActive, configs = updatedConfigs)) } actual suspend fun setActive(name: String) { val wrapper = load() val configFile = wrapper.configFile if (configFile.configs.none { it.name == name }) { throw IllegalArgumentException("Configuration '$name' not found") } save(configFile.copy(active = name)) } actual fun getConfigPath(): String = getConfigFile().absolutePath actual suspend fun exists(): Boolean = withContext(Dispatchers.IO) { getConfigFile().exists() } actual suspend fun saveMcpServers(mcpServers: Map) { val wrapper = load() val configFile = wrapper.configFile val updatedConfigFile = configFile.copy(mcpServers = mcpServers) save(updatedConfigFile) } actual suspend fun saveRemoteServer(remoteServer: RemoteServerConfig) { val wrapper = load() val configFile = wrapper.configFile val updatedConfigFile = configFile.copy(remoteServer = remoteServer) save(updatedConfigFile) } actual suspend fun saveLastWorkspace(name: String, path: String) { val wrapper = load() val configFile = wrapper.configFile val updatedConfigFile = configFile.copy( lastWorkspace = WorkspaceInfo(name = name, path = path) ) save(updatedConfigFile) } actual suspend fun getLastWorkspace(): WorkspaceInfo? { val wrapper = load() return wrapper.getLastWorkspace() } actual suspend fun saveIssueTracker(issueTracker: IssueTrackerConfig) { val wrapper = load() val configFile = wrapper.configFile val updatedConfigFile = configFile.copy(issueTracker = issueTracker) save(updatedConfigFile) } actual suspend fun getIssueTracker(): IssueTrackerConfig { val wrapper = load() return wrapper.getIssueTracker() } actual fun getKcefInstallDir(): String { // KCEF is not available on Android return "" } actual fun getAcpLogsDir(): String { return File(getConfigDir(), "acp-logs").absolutePath } actual suspend fun loadToolConfig(): ToolConfigFile = withContext(Dispatchers.IO) { try { val file = getToolConfigFile() if (!file.exists()) { return@withContext ToolConfigFile.default() } val content = file.readText() json.decodeFromString(content) } catch (e: Exception) { println("Error loading tool config: ${e.message}") ToolConfigFile.default() } } actual suspend fun saveToolConfig(toolConfig: ToolConfigFile) = withContext(Dispatchers.IO) { try { val dir = getConfigDir() dir.mkdirs() val file = getToolConfigFile() val jsonContent = json.encodeToString(ToolConfigFile.serializer(), toolConfig) file.writeText(jsonContent) } catch (e: Exception) { println("Error saving tool config: ${e.message}") throw e } } actual fun getToolConfigPath(): String = getToolConfigFile().absolutePath actual fun generateUniqueConfigName(baseName: String, existingNames: List): String { if (baseName !in existingNames) { return baseName } var counter = 1 var uniqueName = "$baseName-$counter" while (uniqueName in existingNames) { counter++ uniqueName = "$baseName-$counter" } return uniqueName } private fun createEmpty(): AutoDevConfigWrapper { return AutoDevConfigWrapper(ConfigFile(active = "", configs = emptyList())) } /** * Parse YAML config file using YamlUtils */ private fun parseYamlConfig(content: String): ConfigFile { // Try to parse as JSON first (for MCP config compatibility) try { if (content.trim().startsWith("{")) { return json.decodeFromString(content) } } catch (e: Exception) { // Fall through to YAML parsing } // Use YamlUtils for proper YAML parsing return YamlUtils.loadAs(content, kotlinx.serialization.serializer()) } /** * Convert ConfigFile to YAML format using YamlUtils */ private fun toYaml(configFile: ConfigFile): String { return YamlUtils.dump(configFile, kotlinx.serialization.serializer()) } }