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
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import io.github.openflocon.domain.network.usecase.ResetCurrentDeviceHttpRequest
import io.github.openflocon.domain.network.usecase.badquality.ObserveAllNetworkBadQualitiesUseCase
import io.github.openflocon.domain.network.usecase.mocks.ObserveNetworkMocksUseCase
import io.github.openflocon.domain.network.usecase.mocks.ObserveNetworkWebsocketIdsUseCase
import io.github.openflocon.domain.network.usecase.ReplayNetworkCallUseCase
import io.github.openflocon.flocondesktop.common.utils.stateInWhileSubscribed
import io.github.openflocon.flocondesktop.core.data.settings.usecase.ObserveNetworkSettingsUseCase
import io.github.openflocon.flocondesktop.core.data.settings.usecase.SaveNetworkSettingsUseCase
Expand Down Expand Up @@ -95,6 +96,7 @@ class NetworkViewModel(
private val getNetworkRequestsUseCase: GetNetworkRequestsUseCase by inject()
private val feedbackDisplayer: FeedbackDisplayer by inject()
private val exportNetworkCallsToCsv: ExportNetworkCallsToCsvUseCase by inject()
private val replayNetworkCallUseCase: ReplayNetworkCallUseCase by inject()

private val contentState = MutableStateFlow(
ContentUiState(
Expand Down Expand Up @@ -236,6 +238,7 @@ class NetworkViewModel(
is NetworkAction.OpenBadNetworkQuality -> openBadNetworkQuality()
is NetworkAction.CloseBadNetworkQuality -> closeBadNetworkQuality()
is NetworkAction.CopyCUrl -> onCopyCUrl(action)
is NetworkAction.Replay -> onReplay(action)
is NetworkAction.CopyUrl -> onCopyUrl(action)
is NetworkAction.Remove -> onRemove(action)
is NetworkAction.RemoveLinesAbove -> onRemoveLinesAbove(action)
Expand Down Expand Up @@ -411,6 +414,12 @@ class NetworkViewModel(
}
}

private fun onReplay(action: NetworkAction.Replay) {
viewModelScope.launch(dispatcherProvider.viewModel) {
replayNetworkCallUseCase(action.item.uuid)
}
}

private fun onCopyUrl(action: NetworkAction.CopyUrl) {
viewModelScope.launch(dispatcherProvider.viewModel) {
val domainModel = observeNetworkRequestsByIdUseCase(action.item.uuid).firstOrNull()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ fun FloconNetworkCallDomainModel.toUi(
method = getMethodUi(this),
status = getStatusUi(this),
isMocked = request.isMocked,
isReplayed = isReplayed,
isFromOldAppInstance = deviceIdAndPackageName?.appInstance?.let { it != appInstance } ?: false
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ sealed interface NetworkAction {

data class CopyCUrl(val item: NetworkItemViewState) : NetworkAction

data class Replay(val item: NetworkItemViewState) : NetworkAction

data class Remove(val item: NetworkItemViewState) : NetworkAction

data class RemoveLinesAbove(val item: NetworkItemViewState) : NetworkAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ data class NetworkItemViewState(
val status: NetworkStatusUi,
val method: NetworkMethodUi,
val isMocked: Boolean,
val isReplayed: Boolean,
val isFromOldAppInstance: Boolean,
) {

Expand Down Expand Up @@ -83,6 +84,7 @@ fun previewNetworkItemViewState(): NetworkItemViewState = NetworkItemViewState(
),
isMocked = false,
isFromOldAppInstance = false,
isReplayed = false,
)

fun previewNetworkItemViewStateError(): NetworkItemViewState = NetworkItemViewState(
Expand All @@ -99,6 +101,7 @@ fun previewNetworkItemViewStateError(): NetworkItemViewState = NetworkItemViewSt
),
isMocked = false,
isFromOldAppInstance = false,
isReplayed = false,
)

fun previewGraphQlItemViewState(): NetworkItemViewState = NetworkItemViewState(
Expand All @@ -115,4 +118,5 @@ fun previewGraphQlItemViewState(): NetworkItemViewState = NetworkItemViewState(
),
isMocked = false,
isFromOldAppInstance = false,
isReplayed = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import io.github.openflocon.library.designsystem.common.buildMenu
import io.github.openflocon.library.designsystem.components.FloconSurface
import org.jetbrains.compose.ui.tooling.preview.Preview

private val replayColor = Color(0xFF242D44)

@Composable
fun NetworkItemView(
state: NetworkItemViewState,
Expand Down Expand Up @@ -84,7 +86,9 @@ fun NetworkItemView(
} else Modifier
)
.then(
if (state.isMocked) {
if(state.isReplayed) {
Modifier.background(replayColor)
} else if (state.isMocked) {
Modifier.background(FloconTheme.colorPalette.accent)
} else Modifier,
)
Expand Down Expand Up @@ -211,6 +215,9 @@ private fun contextualActions(
item(
label = "Create Mock",
onClick = { onActionCallback(NetworkAction.CreateMock(state)) })
item(
label = "Replay",
onClick = { onActionCallback(NetworkAction.Replay(state)) })
}
separator()
subMenu(label = "Filter") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.github.openflocon.data.core.network.datasource

import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel

interface NetworkReplayDataSource {
suspend fun replay(request: FloconNetworkCallDomainModel): FloconNetworkCallDomainModel
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.github.openflocon.data.core.network.datasource.NetworkLocalWebsocketDa
import io.github.openflocon.data.core.network.datasource.NetworkMocksLocalDataSource
import io.github.openflocon.data.core.network.datasource.NetworkQualityLocalDataSource
import io.github.openflocon.data.core.network.datasource.NetworkRemoteDataSource
import io.github.openflocon.data.core.network.datasource.NetworkReplayDataSource
import io.github.openflocon.domain.Protocol
import io.github.openflocon.domain.common.DispatcherProvider
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
Expand Down Expand Up @@ -36,6 +37,7 @@ class NetworkRepositoryImpl(
private val networkQualityLocalDataSource: NetworkQualityLocalDataSource,
private val networkImageRepository: NetworkImageRepository,
private val networkRemoteDataSource: NetworkRemoteDataSource,
private val networkReplayDataSource: NetworkReplayDataSource,
) : NetworkRepository,
NetworkMocksRepository,
MessagesReceiverRepository,
Expand Down Expand Up @@ -447,4 +449,16 @@ class NetworkRepositoryImpl(
}
}

override suspend fun replayRequest(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
request: FloconNetworkCallDomainModel
) {
withContext(dispatcherProvider.data) {
val replayedRequest = networkReplayDataSource.replay(request)
networkLocalDataSource.save(
deviceIdAndPackageName = deviceIdAndPackageName,
call = replayedRequest
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fun FloconNetworkCallEntity.toDomainModel(): FloconNetworkCallDomainModel {
appInstance = appInstance,
request = toRequestDomainModel(),
response = response?.toDomainModel(),
isReplayed = isReplayed,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fun FloconNetworkCallDomainModel.toEntity(
deviceId = deviceIdAndPackageName.deviceId,
packageName = deviceIdAndPackageName.packageName,
appInstance = deviceIdAndPackageName.appInstance,
isReplayed = isReplayed,
type = when (this.request.specificInfos) {
is FloconNetworkCallDomainModel.Request.SpecificInfos.Http -> FloconNetworkCallType.HTTP
is FloconNetworkCallDomainModel.Request.SpecificInfos.GraphQl -> FloconNetworkCallType.GRAPHQL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ data class FloconNetworkCallEntity(
val appInstance: Long, // the start time of the mobile app

val type: FloconNetworkCallType,
val isReplayed: Boolean,

@Embedded(prefix = "request_")
val request: FloconNetworkRequestEmbedded,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.flocon.data.remote.network

import com.flocon.data.remote.network.datasource.NetworkRemoteDataSourceImpl
import com.flocon.data.remote.network.datasource.NetworkReplayDataSourceImpl
import io.github.openflocon.data.core.network.datasource.NetworkRemoteDataSource
import io.github.openflocon.data.core.network.datasource.NetworkReplayDataSource
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module

internal val networkModule = module {
singleOf(::NetworkRemoteDataSourceImpl) bind NetworkRemoteDataSource::class
singleOf(::NetworkReplayDataSourceImpl) bind NetworkReplayDataSource::class
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
@file:OptIn(ExperimentalUuidApi::class)

package com.flocon.data.remote.network.datasource

import io.github.openflocon.data.core.network.datasource.NetworkReplayDataSource
import io.github.openflocon.domain.common.ByteFormatter
import io.github.openflocon.domain.common.time.formatDuration
import io.github.openflocon.domain.common.time.formatTimestamp
import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel
import io.github.openflocon.domain.network.models.FloconNetworkCallDomainModel.Response.Success.SpecificInfos
import io.ktor.client.HttpClient
import io.ktor.client.request.request
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpMethod
import io.ktor.http.content.TextContent
import io.ktor.util.toMap
import kotlin.time.Clock
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

internal class NetworkReplayDataSourceImpl : NetworkReplayDataSource {

private val client = HttpClient()

override suspend fun replay(request: FloconNetworkCallDomainModel): FloconNetworkCallDomainModel {
val startTime = Clock.System.now().toEpochMilliseconds()
val response = try {
val result = client.request(request.request.url) {
method = HttpMethod.parse(request.request.method)
request.request.headers.forEach { (key, value) ->
headers.append(key, value)
}
request.request.body?.let {
setBody(TextContent(it, io.ktor.http.ContentType.Application.Json)) // Assuming JSON for now
}
}
val endTime = Clock.System.now().toEpochMilliseconds()
val duration = (endTime - startTime).toDouble()
val body = result.bodyAsText()
val size = body.length.toLong()

FloconNetworkCallDomainModel.Response.Success(
headers = result.headers.toMap().mapValues { it.value.joinToString(",") },
body = body,
specificInfos = SpecificInfos.Http(
httpCode = result.status.value
),
durationMs = duration,
durationFormatted = formatDuration(duration),
byteSize = size,
byteSizeFormatted = ByteFormatter.formatBytes(size),
isImage = false, // TODO: check content type
statusFormatted = result.status.value.toString(),
contentType = result.headers["Content-Type"]
)
} catch (e: Exception) {
val endTime = Clock.System.now().toEpochMilliseconds()
val duration = (endTime - startTime).toDouble()
FloconNetworkCallDomainModel.Response.Failure(
issue = e.message ?: "Unknown error",
durationMs = duration,
durationFormatted = formatDuration(duration),
statusFormatted = "Error"
)
}

return request.copy(
callId = Uuid.random().toString(),
request = request.request.copy(
startTime = startTime,
startTimeFormatted = formatTimestamp(startTime),
),
response = response,
isReplayed = true,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.github.openflocon.domain.network.usecase.ObserveNetworkRequestsUseCase
import io.github.openflocon.domain.network.usecase.RemoveHttpRequestsBeforeUseCase
import io.github.openflocon.domain.network.usecase.RemoveNetworkRequestUseCase
import io.github.openflocon.domain.network.usecase.RemoveOldSessionsNetworkRequestUseCase
import io.github.openflocon.domain.network.usecase.ReplayNetworkCallUseCase
import io.github.openflocon.domain.network.usecase.ResetCurrentDeviceHttpRequestsUseCase
import io.github.openflocon.domain.network.usecase.UpdateNetworkFilterUseCase
import io.github.openflocon.domain.network.usecase.badquality.DeleteBadQualityUseCase
Expand Down Expand Up @@ -43,6 +44,7 @@ internal val networkModule = module {
factoryOf(::ExportNetworkCallsToCsvUseCase)
factoryOf(::DecodeJwtTokenUseCase)
factoryOf(::RemoveOldSessionsNetworkRequestUseCase)
factoryOf(::ReplayNetworkCallUseCase)
// filters
factoryOf(::GetNetworkFilterUseCase)
factoryOf(::ObserveNetworkFilterUseCase)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ data class FloconNetworkCallDomainModel(
val appInstance: AppInstance,
val request: Request,
val response: Response?,
val isReplayed: Boolean = false,
) {

data class Request(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,9 @@ interface NetworkRepository {
suspend fun deleteOldRequests(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel
)

suspend fun replayRequest(
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
request: FloconNetworkCallDomainModel
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.openflocon.domain.network.usecase

import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase
import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceIdAndPackageNameUseCase
import io.github.openflocon.domain.network.repository.NetworkRepository
import kotlinx.coroutines.flow.firstOrNull

class ReplayNetworkCallUseCase(
private val networkRepository: NetworkRepository,
private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase,
) {
suspend operator fun invoke(callId: String) {
val deviceIdAndPackageName = getCurrentDeviceIdAndPackageNameUseCase() ?: return
val call = networkRepository.observeRequest(
deviceIdAndPackageName = deviceIdAndPackageName,
requestId = callId
).firstOrNull() ?: return

networkRepository.replayRequest(
deviceIdAndPackageName = deviceIdAndPackageName,
request = call
)
}
}