Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ dependencies {
implementation(libs.okhttp)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging)
implementation(libs.retrofit)

testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.awan.app.core.data.mcp.di

import com.awan.app.core.data.mcp.repository.McpRepositoryImpl
import com.awan.app.core.data.mcp.remote.McpRemoteDataSource
import com.awan.app.core.data.mcp.remote.McpRemoteDataSourceImpl
import com.awan.app.core.domain.mcp.repository.McpRepository
import dagger.Binds
import dagger.Module
Expand All @@ -10,7 +12,13 @@ import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class McpDataModule {
internal abstract class McpDataModule {

@Binds
@Singleton
abstract fun bindMcpRemoteDataSource(
impl: McpRemoteDataSourceImpl,
): McpRemoteDataSource

@Binds
@Singleton
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.awan.app.core.data.mcp.remote

import com.awan.app.core.common.result.Result
import com.awan.app.core.network.dto.mcp.ApiKeyResponseDto
import com.awan.app.core.network.dto.mcp.ApiKeySummaryDto

internal interface McpRemoteDataSource {
suspend fun getApiKeys(): Result<List<ApiKeySummaryDto>>
suspend fun createApiKey(name: String): Result<ApiKeyResponseDto>
suspend fun revokeApiKey(id: String): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.awan.app.core.data.mcp.remote

import com.awan.app.core.common.dispatcher.AwanDispatchers
import com.awan.app.core.common.dispatcher.Dispatcher
import com.awan.app.core.common.result.Result
import com.awan.app.core.network.api.McpApiService
import com.awan.app.core.network.dto.mcp.ApiKeyResponseDto
import com.awan.app.core.network.dto.mcp.ApiKeySummaryDto
import com.awan.app.core.network.dto.mcp.CreateApiKeyRequestDto
import com.awan.app.core.network.error.safeApiCall
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.serialization.json.Json

internal class McpRemoteDataSourceImpl @Inject constructor(
private val apiService: McpApiService,
private val json: Json,
@Dispatcher(AwanDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
) : McpRemoteDataSource {

override suspend fun getApiKeys(): Result<List<ApiKeySummaryDto>> =
safeApiCall(ioDispatcher, json) { apiService.getApiKeys() }

override suspend fun createApiKey(name: String): Result<ApiKeyResponseDto> =
safeApiCall(ioDispatcher, json) { apiService.createApiKey(CreateApiKeyRequestDto(name)) }

override suspend fun revokeApiKey(id: String): Result<Unit> =
safeApiCall(ioDispatcher, json) { apiService.revokeApiKey(id) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import com.awan.app.core.common.error.AppError
import com.awan.app.core.common.result.Result
import com.awan.app.core.data.mcp.mapper.toDomain
import com.awan.app.core.data.mcp.mapper.toEntity
import com.awan.app.core.data.mcp.remote.McpRemoteDataSource
import com.awan.app.core.database.dao.McpTokenDao
import com.awan.app.core.domain.mcp.model.CreatedMcpToken
import com.awan.app.core.domain.mcp.model.McpConnectionDetails
import com.awan.app.core.domain.mcp.model.McpToken
import com.awan.app.core.domain.mcp.repository.McpRepository
import com.awan.app.core.domain.network.NetworkConnectivityMonitor
import com.awan.app.core.network.BuildConfig
import com.awan.app.core.network.api.McpApiService
import com.awan.app.core.network.dto.mcp.CreateApiKeyRequestDto
import com.awan.app.core.network.error.safeApiCall
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
Expand All @@ -24,13 +24,10 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import retrofit2.HttpException
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class McpRepositoryImpl @Inject constructor(
private val mcpApiService: McpApiService,
internal class McpRepositoryImpl @Inject constructor(
private val remoteDataSource: McpRemoteDataSource,
private val mcpTokenDao: McpTokenDao,
private val connectivityMonitor: NetworkConnectivityMonitor,
@Dispatcher(AwanDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
Expand All @@ -42,19 +39,18 @@ class McpRepositoryImpl @Inject constructor(
McpConnectionDetails(
mcpUrl = "${BuildConfig.AWAN_BASE_URL.trimEnd('/')}/v1/mcp",
clientId = "awan-android-client",
)
)
),
),
)
}.flowOn(ioDispatcher)

override fun getMcpTokens(): Flow<Result<List<McpToken>>> = flow {
if (connectivityMonitor.isCurrentlyOnline()) {
try {
val response = mcpApiService.getApiKeys()
if (response.isSuccessful) {
val dtos = response.body().orEmpty()
val entities = dtos.map { it.toEntity() }
mcpTokenDao.replaceMcpTokens(entities)
when (val result = remoteDataSource.getApiKeys()) {
is Result.Success -> mcpTokenDao.replaceMcpTokens(result.data.map { it.toEntity() })
is Result.Error,
Result.Loading -> Unit
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Expand All @@ -64,37 +60,33 @@ class McpRepositoryImpl @Inject constructor(
emitAll(
mcpTokenDao.getMcpTokens().map { entities ->
Result.Success(entities.map { it.toDomain() })
}
},
)
}.flowOn(ioDispatcher)

override suspend fun createMcpToken(name: String): Result<CreatedMcpToken> {
if (!connectivityMonitor.isCurrentlyOnline()) {
return Result.Error(AppError.Network)
}
return safeApiCall(ioDispatcher) {
val response = mcpApiService.createApiKey(CreateApiKeyRequestDto(name = name))
if (!response.isSuccessful) throw HttpException(response)
val dto = response.body() ?: throw IllegalStateException("Empty response body")
val createdToken = dto.toDomain()
try {
mcpTokenDao.upsertMcpTokens(listOf(dto.toEntity()))
} catch (e: Exception) {
if (e is CancellationException) throw e
// Local DB cache write failure must NOT drop or cause failure of the returned raw token
if (!connectivityMonitor.isCurrentlyOnline()) return Result.Error(AppError.Network)

return when (val result = remoteDataSource.createApiKey(name)) {
is Result.Success -> {
val dto = result.data
try {
mcpTokenDao.upsertMcpTokens(listOf(dto.toEntity()))
} catch (e: Exception) {
if (e is CancellationException) throw e
// Local DB cache write failure must NOT drop or cause failure of the returned raw token
}
Result.Success(dto.toDomain())
}
createdToken
is Result.Error -> Result.Error(result.error)
Result.Loading -> Result.Loading
}
}

override suspend fun deleteMcpToken(id: String): Result<Unit> {
if (!connectivityMonitor.isCurrentlyOnline()) {
return Result.Error(AppError.Network)
}
val result = safeApiCall(ioDispatcher) {
val response = mcpApiService.revokeApiKey(id)
if (!response.isSuccessful) throw HttpException(response)
}
if (!connectivityMonitor.isCurrentlyOnline()) return Result.Error(AppError.Network)

val result = remoteDataSource.revokeApiKey(id)
if (result is Result.Success) {
try {
mcpTokenDao.deleteMcpToken(id)
Expand All @@ -106,28 +98,27 @@ class McpRepositoryImpl @Inject constructor(
}

override suspend fun regenerateMcpToken(id: String): Result<CreatedMcpToken> {
if (!connectivityMonitor.isCurrentlyOnline()) {
return Result.Error(AppError.Network)
}
val existingTokenName = mcpTokenDao.getMcpTokens().first().find { it.id == id }?.name ?: "MCP Token"
return safeApiCall(ioDispatcher) {
val createResponse = mcpApiService.createApiKey(CreateApiKeyRequestDto(name = existingTokenName))
if (!createResponse.isSuccessful) throw HttpException(createResponse)
val dto = createResponse.body() ?: throw IllegalStateException("Empty response body")
val createdToken = dto.toDomain()

val revokeResponse = mcpApiService.revokeApiKey(id)
if (!revokeResponse.isSuccessful) throw HttpException(revokeResponse)
if (!connectivityMonitor.isCurrentlyOnline()) return Result.Error(AppError.Network)

try {
mcpTokenDao.deleteMcpToken(id)
mcpTokenDao.upsertMcpTokens(listOf(dto.toEntity()))
} catch (e: Exception) {
if (e is CancellationException) throw e
// Local DB cache write failure must NOT drop or cause failure of the returned raw token
val name = mcpTokenDao.getMcpTokens().first().find { it.id == id }?.name ?: "MCP Token"
return when (val createResult = remoteDataSource.createApiKey(name)) {
is Result.Success -> when (val revokeResult = remoteDataSource.revokeApiKey(id)) {
is Result.Success -> {
val dto = createResult.data
try {
mcpTokenDao.deleteMcpToken(id)
mcpTokenDao.upsertMcpTokens(listOf(dto.toEntity()))
} catch (e: Exception) {
if (e is CancellationException) throw e
// Local DB cache write failure must NOT drop or cause failure of the returned raw token
}
Result.Success(dto.toDomain())
}
is Result.Error -> Result.Error(revokeResult.error)
Result.Loading -> Result.Loading
}
createdToken
is Result.Error -> Result.Error(createResult.error)
Result.Loading -> Result.Loading
}
}
}

Loading
Loading