diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/MetricEvent.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/MetricEventUiModel.kt similarity index 79% rename from FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/MetricEvent.kt rename to FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/MetricEventUiModel.kt index 0b885c73..a8984891 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/MetricEvent.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/MetricEventUiModel.kt @@ -3,16 +3,16 @@ package io.github.openflocon.flocondesktop.features.performance import kotlinx.serialization.Serializable @Serializable -data class MetricEvent( +data class MetricEventUiModel( val timestamp: String, - val ramMb: String, + val ramMb: String?, val fps: String, val jankPercentage: String, val battery: String, val screenshotPath: String?, ) -fun previewMetricsEvent() = MetricEvent( +fun previewMetricsEvent() = MetricEventUiModel( timestamp = "10:55:38.123", ramMb = "150", fps = "60.0", diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/Navigation.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/Navigation.kt index 090309bd..bd071159 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/Navigation.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/Navigation.kt @@ -18,7 +18,7 @@ internal sealed interface PerformanceRoutes : FloconRoute { } @Serializable - data class Detail(val event: MetricEvent) : PerformanceRoutes, WindowRoute { + data class Detail(val event: MetricEventUiModel) : PerformanceRoutes, WindowRoute { override val singleTopKey = null } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/PerformanceViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/PerformanceViewModel.kt index bc80039d..836c1191 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/PerformanceViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/PerformanceViewModel.kt @@ -2,7 +2,9 @@ package io.github.openflocon.flocondesktop.features.performance import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import io.github.openflocon.domain.common.ByteFormatter import io.github.openflocon.domain.common.DispatcherProvider +import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase import io.github.openflocon.domain.performance.usecase.FetchPerformanceMetricsUseCase import io.github.openflocon.domain.performance.usecase.GetAdbDevicesUseCase import io.github.openflocon.domain.performance.usecase.GetDeviceRefreshRateUseCase @@ -10,10 +12,7 @@ import io.github.openflocon.navigation.MainFloconNavigationState import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -24,6 +23,7 @@ import java.time.format.DateTimeFormatter class PerformanceViewModel( private val fetchPerformanceMetricsUseCase: FetchPerformanceMetricsUseCase, + private val getCurrentDeviceIdAndPackageNameUseCase: GetCurrentDeviceIdAndPackageNameUseCase, private val getAdbDevicesUseCase: GetAdbDevicesUseCase, private val getDeviceRefreshRateUseCase: GetDeviceRefreshRateUseCase, private val navigationState: MainFloconNavigationState, @@ -42,7 +42,7 @@ class PerformanceViewModel( private val _intervalMs = MutableStateFlow(1000L) val intervalMs = _intervalMs.asStateFlow() - private val _metrics = MutableStateFlow>(emptyList()) + private val _metrics = MutableStateFlow>(emptyList()) val metrics = _metrics.asStateFlow() private val _isMonitoring = MutableStateFlow(false) @@ -62,6 +62,11 @@ class PerformanceViewModel( _selectedDevice.value = deviceList.first() } } + viewModelScope.launch(dispatcherProvider.viewModel) { + getCurrentDeviceIdAndPackageNameUseCase()?.let { (_, packageName) -> + _packageName.value = packageName + } + } } fun onDeviceSelected(deviceId: String) { @@ -95,7 +100,7 @@ class PerformanceViewModel( if (deviceSerial != null) { refreshRate = getDeviceRefreshRateUseCase(deviceSerial) } - + while (isActive) { fetchMetrics() delay(_intervalMs.value) @@ -124,10 +129,13 @@ class PerformanceViewModel( lastFrameCount = domainModel.totalFrames lastFetchTime = domainModel.timestamp - val event = MetricEvent( - timestamp = LocalDateTime.ofInstant(Instant.ofEpochMilli(domainModel.timestamp), ZoneId.systemDefault()) + val event = MetricEventUiModel( + timestamp = LocalDateTime.ofInstant( + Instant.ofEpochMilli(domainModel.timestamp), + ZoneId.systemDefault() + ) .format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")), - ramMb = domainModel.ramMb, + ramMb = domainModel.ramMb?.let { ByteFormatter.formatBytes(it) }, fps = if (domainModel.fps > 0) String.format("%.1f", domainModel.fps) else "0", jankPercentage = String.format("%.1f%%", domainModel.jankPercentage), battery = domainModel.battery, @@ -137,7 +145,7 @@ class PerformanceViewModel( _metrics.update { listOf(event) + it } } - fun onEventClicked(event: MetricEvent) { + fun onEventClicked(event: MetricEventUiModel) { navigationState.navigate(PerformanceRoutes.Detail(event)) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/MetricItemView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/MetricItemView.kt index 7f035e7a..94693102 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/MetricItemView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/MetricItemView.kt @@ -18,10 +18,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import io.github.openflocon.flocondesktop.common.ui.isInPreview -import io.github.openflocon.flocondesktop.features.performance.MetricEvent +import io.github.openflocon.flocondesktop.features.performance.MetricEventUiModel import io.github.openflocon.flocondesktop.features.performance.previewMetricsEvent import io.github.openflocon.library.designsystem.FloconTheme -import io.github.openflocon.library.designsystem.components.FloconCircularProgressIndicator import io.github.openflocon.library.designsystem.components.FloconSurface import org.jetbrains.compose.ui.tooling.preview.Preview @@ -29,8 +28,8 @@ private val imageSize = 40.dp @Composable internal fun MetricItemView( - event: MetricEvent, - onClick: (MetricEvent) -> Unit, + event: MetricEventUiModel, + onClick: (MetricEventUiModel) -> Unit, ) { val bodySmall = FloconTheme.typography.bodySmall.copy(fontSize = 11.sp) @@ -81,7 +80,7 @@ internal fun MetricItemView( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - Text("RAM: ${event.ramMb} MB", style = bodySmall) + event.ramMb?.let { Text("RAM: $it", style = bodySmall) } Text("Battery: ${event.battery}", style = bodySmall) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/PerformanceDetailScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/PerformanceDetailScreen.kt index 87156b3a..39fa8f3a 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/PerformanceDetailScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/PerformanceDetailScreen.kt @@ -21,14 +21,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import io.github.openflocon.flocondesktop.features.performance.MetricEvent +import io.github.openflocon.flocondesktop.features.performance.MetricEventUiModel import io.github.openflocon.library.designsystem.FloconTheme import org.jetbrains.compose.ui.tooling.preview.Preview @OptIn(ExperimentalMaterial3Api::class) @Composable fun PerformanceDetailScreen( - event: MetricEvent, + event: MetricEventUiModel, ) { Scaffold( topBar = { @@ -62,7 +62,7 @@ fun PerformanceDetailScreen( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - MetricText(label = "RAM Usage", value = "${event.ramMb} MB") + event.ramMb?.let { MetricText(label = "RAM Usage", value = "$it MB") } MetricText(label = "FPS", value = event.fps) MetricText(label = "Jank Percentage", value = event.jankPercentage) MetricText(label = "Battery", value = event.battery) @@ -113,7 +113,7 @@ private fun MetricText(label: String, value: String) { private fun PerformanceDetailScreenPreview() { FloconTheme { PerformanceDetailScreen( - event = MetricEvent( + event = MetricEventUiModel( timestamp = "10:55:38.123", ramMb = "150", fps = "60.0", diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/PerformanceScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/PerformanceScreen.kt index b253ff85..e3d673a8 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/PerformanceScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/performance/view/PerformanceScreen.kt @@ -2,9 +2,11 @@ package io.github.openflocon.flocondesktop.features.performance.view +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Remove @@ -12,16 +14,13 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil3.compose.AsyncImage -import io.github.openflocon.flocondesktop.features.performance.MetricEvent import io.github.openflocon.flocondesktop.features.performance.PerformanceViewModel import io.github.openflocon.library.designsystem.FloconTheme import io.github.openflocon.library.designsystem.components.* import org.koin.compose.viewmodel.koinViewModel -import java.io.File @Composable fun PerformanceScreen() { @@ -33,7 +32,7 @@ fun PerformanceScreen() { val metrics by viewModel.metrics.collectAsStateWithLifecycle() val isMonitoring by viewModel.isMonitoring.collectAsStateWithLifecycle() - FloconScaffold() { paddingValues -> + FloconScaffold { paddingValues -> Column( modifier = Modifier .fillMaxSize() @@ -54,7 +53,8 @@ fun PerformanceScreen() { onExpandedChange = { expanded = it } ) { Text( - modifier = Modifier.fillMaxWidth().menuAnchor(MenuAnchorType.PrimaryNotEditable), + modifier = Modifier.fillMaxWidth() + .menuAnchor(MenuAnchorType.PrimaryNotEditable), text = selectedDevice ?: "Select Device", //onValueChange = {}, //readOnly = true, @@ -137,17 +137,34 @@ fun PerformanceScreen() { HorizontalDivider() - LazyColumn( - modifier = Modifier.fillMaxWidth().weight(1f), - contentPadding = PaddingValues(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + Box( + modifier = Modifier.fillMaxWidth() + .weight(1f) + .clip(FloconTheme.shapes.medium) + .background(FloconTheme.colorPalette.primary), + contentAlignment = Alignment.Center ) { - items(metrics) { event -> - MetricItemView( - event = event, - onClick = viewModel::onEventClicked - ) + val listState = rememberLazyListState() + val scrollAdapter = rememberFloconScrollbarAdapter(listState) + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 6.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(metrics) { event -> + MetricItemView( + event = event, + onClick = viewModel::onEventClicked + ) + } } + FloconVerticalScrollbar( + adapter = scrollAdapter, + modifier = Modifier.fillMaxHeight() + .align(Alignment.TopEnd) + ) } } } diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/performance/model/PerformanceMetricsDomainModel.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/performance/model/PerformanceMetricsDomainModel.kt index 801208f3..01a93ec7 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/performance/model/PerformanceMetricsDomainModel.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/performance/model/PerformanceMetricsDomainModel.kt @@ -1,7 +1,7 @@ package io.github.openflocon.domain.performance.model data class PerformanceMetricsDomainModel( - val ramMb: String, + val ramMb: Long?, val fps: Double, val jankPercentage: Double, val battery: String, diff --git a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/performance/usecase/GetRamUsageUseCase.kt b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/performance/usecase/GetRamUsageUseCase.kt index 808b7f65..f71e5bb8 100644 --- a/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/performance/usecase/GetRamUsageUseCase.kt +++ b/FloconDesktop/domain/src/commonMain/kotlin/io/github/openflocon/domain/performance/usecase/GetRamUsageUseCase.kt @@ -7,19 +7,17 @@ import io.github.openflocon.domain.common.getOrNull class GetRamUsageUseCase( private val executeAdbCommandUseCase: ExecuteAdbCommandUseCase, ) { - suspend operator fun invoke(deviceSerial: String, packageName: String): String { - if (packageName.isEmpty()) return "N/A" + suspend operator fun invoke(deviceSerial: String, packageName: String): Long? { + if (packageName.isEmpty()) return null return executeAdbCommandUseCase( target = AdbCommandTargetDomainModel.DeviceSerial(deviceSerial), command = "shell dumpsys meminfo $packageName" ).mapSuccess { output -> val pssRegex = Regex("(?:TOTAL PSS:|TOTAL)\\s+(\\d+)", RegexOption.IGNORE_CASE) val pssKb = pssRegex.find(output)?.groupValues?.get(1)?.toLongOrNull() - if (pssKb != null) { - (pssKb / 1024).toString() - } else { - "N/A" + pssKb?.let { + it * 1024 } - }.getOrNull() ?: "N/A" + }.getOrNull() } }