diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceAction.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceAction.kt index 63ec2162..0d545456 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceAction.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceAction.kt @@ -6,6 +6,6 @@ internal sealed interface DeviceAction { data object Refresh : DeviceAction - data class ChangePermission(val permissions: Permissions, val granted: Boolean) : DeviceAction + data class ChangePermission(val permission: String, val granted: Boolean) : DeviceAction } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceScreen.kt index dc091e14..51156404 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceScreen.kt @@ -20,11 +20,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.openflocon.domain.device.models.DeviceId import io.github.openflocon.flocondesktop.common.ui.window.FloconWindow import io.github.openflocon.flocondesktop.common.ui.window.createFloconWindowState +import io.github.openflocon.flocondesktop.device.models.DeviceUiState +import io.github.openflocon.flocondesktop.device.models.previewDeviceUiState import io.github.openflocon.library.designsystem.FloconTheme import io.github.openflocon.library.designsystem.components.FloconCheckbox import io.github.openflocon.library.designsystem.components.FloconHorizontalDivider @@ -63,12 +66,12 @@ private fun Content( ) { val pagerState = rememberPagerState { 2 } - LaunchedEffect(uiState.selectedIndex) { - pagerState.animateScrollToPage(uiState.selectedIndex) + LaunchedEffect(uiState.contentState.selectedIndex) { + pagerState.animateScrollToPage(uiState.contentState.selectedIndex) } FloconWindow( - title = "Device", + title = "Device - ${uiState.infoState.model}", onCloseRequest = onCloseRequest, state = createFloconWindowState() ) { @@ -86,18 +89,18 @@ private fun Content( onAction = onAction ) FloconScrollableTabRow( - selectedTabIndex = uiState.selectedIndex, + selectedTabIndex = uiState.contentState.selectedIndex, modifier = Modifier.fillMaxWidth() ) { FloconTab( text = "Info", - selected = true, + selected = uiState.contentState.selectedIndex == 0, onClick = { onAction(DeviceAction.SelectTab(0)) }, selectedContentColor = FloconTheme.colorPalette.onSurface ) FloconTab( text = "Permission", - selected = true, + selected = uiState.contentState.selectedIndex == 1, onClick = { onAction(DeviceAction.SelectTab(1)) }, selectedContentColor = FloconTheme.colorPalette.onSurface ) @@ -112,8 +115,8 @@ private fun Content( HorizontalPager( state = pagerState, userScrollEnabled = false, - contentPadding = PaddingValues(16.dp), - pageSpacing = 16.dp, + contentPadding = PaddingValues(8.dp), + pageSpacing = 8.dp, modifier = Modifier .fillMaxSize() .padding(it) @@ -142,7 +145,7 @@ private fun Header( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "${uiState.model} (${uiState.serialNumber})", + text = "${uiState.infoState.model} (${uiState.infoState.serialNumber})", style = FloconTheme.typography.headlineSmall, modifier = Modifier .weight(1f) @@ -160,15 +163,16 @@ private fun InfoPage( uiState: DeviceUiState ) { Column( - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxSize() ) { - FloconTextValue("Brand", uiState.brand) - FloconTextValue("CPU", uiState.cpu) - FloconTextValue("Memory", uiState.mem) - FloconTextValue("Battery", uiState.battery) - FloconTextValue("Serial number", uiState.serialNumber) - FloconTextValue("Version - Release", uiState.versionRelease) - FloconTextValue("Version - Sdk", uiState.versionSdk) + FloconTextValue("Brand", uiState.infoState.brand) +// FloconTextValue("CPU", uiState.cpu) +// FloconTextValue("Memory", uiState.mem) + FloconTextValue("Battery", uiState.infoState.battery) + FloconTextValue("Serial number", uiState.infoState.serialNumber) + FloconTextValue("Version - Release", uiState.infoState.versionRelease) + FloconTextValue("Version - Sdk", uiState.infoState.versionSdk) } } @@ -181,13 +185,15 @@ private fun PermissionPage( modifier = Modifier.fillMaxSize() ) { items( - items = uiState.permissions, + items = uiState.permissionState.list, key = { it.name } ) { Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() + .clip(FloconTheme.shapes.medium) .clickable(onClick = { // onAction( // DeviceAction.ChangePermission( @@ -196,18 +202,17 @@ private fun PermissionPage( // ) // ) }) + .padding(4.dp) ) { Text( text = it.name, + style = FloconTheme.typography.labelSmall, modifier = Modifier.weight(1f) ) - Text( - text = it.status - ) FloconCheckbox( checked = it.granted, - uncheckedColor = FloconTheme.colorPalette.secondary, - onCheckedChange = {} + onCheckedChange = null, + uncheckedColor = FloconTheme.colorPalette.primary, ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceUiState.kt deleted file mode 100644 index 70840cd8..00000000 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceUiState.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.github.openflocon.flocondesktop.device - -import androidx.compose.runtime.Immutable - -@Immutable -data class DeviceUiState( - val selectedIndex: Int, - - val model: String, - val brand: String, - val versionRelease: String, - val versionSdk: String, - val serialNumber: String, - val battery: String, - val cpu: String, - val mem: String, - - val permissions: List -) - -@Immutable -data class PermissionUiState( - val name: String, - val status: String, - val granted: Boolean -) - -internal fun previewDeviceUiState() = DeviceUiState( - selectedIndex = 0, - - model = "", - brand = "", - versionRelease = "", - versionSdk = "", - serialNumber = "", - battery = "", - cpu = "", - mem = "", - permissions = emptyList() -) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceViewModel.kt index a8e7d4d0..94a08d70 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceViewModel.kt @@ -6,9 +6,18 @@ import io.github.openflocon.domain.adb.usecase.GetDeviceSerialUseCase import io.github.openflocon.domain.adb.usecase.SendCommandUseCase import io.github.openflocon.domain.common.getOrNull import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase +import io.github.openflocon.flocondesktop.device.models.ContentUiState +import io.github.openflocon.flocondesktop.device.models.CpuUiState +import io.github.openflocon.flocondesktop.device.models.DeviceUiState +import io.github.openflocon.flocondesktop.device.models.InfoUiState +import io.github.openflocon.flocondesktop.device.models.MemoryUiState +import io.github.openflocon.flocondesktop.device.models.PermissionItem +import io.github.openflocon.flocondesktop.device.models.PermissionUiState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -19,21 +28,47 @@ internal class DeviceViewModel( val currentDeviceAppsUseCase: GetCurrentDeviceIdAndPackageNameUseCase ) : ViewModel() { - private val _uiState = MutableStateFlow( - DeviceUiState( - selectedIndex = 0, + private val contentState = MutableStateFlow(ContentUiState(selectedIndex = 0)) + private val infoState = MutableStateFlow( + InfoUiState( model = "", brand = "", versionRelease = "", versionSdk = "", serialNumber = "", - battery = "", - cpu = "", - mem = "", - permissions = emptyList() + battery = "" ) ) - val uiState = _uiState.asStateFlow() + private val memoryState = MutableStateFlow(MemoryUiState(emptyList())) + private val cpuState = MutableStateFlow(CpuUiState(emptyList())) + private val permissionState = MutableStateFlow(PermissionUiState(emptyList())) + + val uiState = combine( + contentState, + infoState, + memoryState, + cpuState, + permissionState + ) { content, info, memory, cpu, permission -> + DeviceUiState( + contentState = content, + infoState = info, + memoryState = memory, + cpuState = cpu, + permissionState = permission + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = DeviceUiState( + contentState = contentState.value, + infoState = infoState.value, + memoryState = memoryState.value, + cpuState = cpuState.value, + permissionState = permissionState.value + ) + ) private var deviceSerial: String = "" @@ -58,32 +93,32 @@ internal class DeviceViewModel( private fun onChangePermission(action: DeviceAction.ChangePermission) { viewModelScope.launch { if (action.granted) { - revokePermission(action.permissions) + revokePermission(action.permission) } else { - grantPermission(action.permissions) + grantPermission(action.permission) } } } private fun onSelect(action: DeviceAction.SelectTab) { - _uiState.update { it.copy(selectedIndex = action.index) } + contentState.update { it.copy(selectedIndex = action.index) } } private fun onRefresh() { viewModelScope.launch { - _uiState.update { state -> - state.copy( - cpu = main(sendCommand("shell", "dumpsys", "cpuinfo")), - battery = sendCommand("shell", "dumpsys", "battery"), - mem = sendCommand("shell", "dumpsys", "meminfo") - ) - } +// _uiState.update { state -> +// state.copy( +// cpu = main(sendCommand("shell", "dumpsys", "cpuinfo")), +// battery = sendCommand("shell", "dumpsys", "battery"), +// mem = sendCommand("shell", "dumpsys", "meminfo") +// ) +// } } } private fun deviceInfo() { viewModelScope.launch { - _uiState.update { state -> + infoState.update { state -> state.copy( model = sendCommand("shell", "getprop", "ro.product.model"), brand = sendCommand("shell", "getprop", "ro.product.brand"), @@ -98,42 +133,37 @@ internal class DeviceViewModel( private fun fetchPermission() { viewModelScope.launch { val packageName = currentDeviceAppsUseCase()?.packageName ?: return@launch - val command = sendCommand("shell", "cmd", "appops", "get", packageName) + val command = sendCommand("shell", "dumpsys", "package", packageName) val permissions = command.lines() + .dropWhile { !it.contains("install permissions:") } + .drop(1) + .takeWhile { it.contains("granted=") } + .map { it.trim() } + .filter { it.startsWith(PERMISSION_PREFIX) } .mapNotNull { line -> val list = line.split(":") - PermissionUiState( - name = list.getOrNull(0) ?: return@mapNotNull null, - status = list.getOrNull(1) ?: return@mapNotNull null, - granted = false + PermissionItem( + name = list.getOrNull(0)?.removePrefix(PERMISSION_PREFIX) ?: return@mapNotNull null, + granted = list.getOrNull(1)?.contains("granted=true") ?: return@mapNotNull null, ) } - .sortedBy(PermissionUiState::name) + .sortedBy(PermissionItem::name) -// val granted = sendCommand("shell", "dumpsys", "package", packageName, "|", "grep", "permission") -// val permissions = Permissions.entries -// .map { permissions -> -// PermissionUiState( -// permissions = permissions, -// granted = granted.contains(permissions.value) -// ) -// } - - _uiState.update { it.copy(permissions = permissions) } + permissionState.update { it.copy(list = permissions) } } } - private suspend fun grantPermission(permissions: Permissions) { + private suspend fun grantPermission(permission: String) { val packageName = currentDeviceAppsUseCase() ?: return - sendCommand("shell", "pm", "grant", packageName.packageName, permissions.value) + sendCommand("shell", "pm", "grant", packageName.packageName, "${PERMISSION_PREFIX}$permission") } - private suspend fun revokePermission(permissions: Permissions) { + private suspend fun revokePermission(permission: String) { val packageName = currentDeviceAppsUseCase() ?: return - sendCommand("shell", "pm", "revoke", packageName.packageName, permissions.value) + sendCommand("shell", "pm", "revoke", packageName.packageName, "$PERMISSION_PREFIX$permission") } private suspend fun sendCommand(vararg args: String): String { @@ -247,4 +277,8 @@ internal class DeviceViewModel( return "" } + companion object { + private const val PERMISSION_PREFIX = "android.permission." + } + } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/Permissions.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/Permissions.kt deleted file mode 100644 index 1257e259..00000000 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/Permissions.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.openflocon.flocondesktop.device - -enum class Permissions( - val label: String, - val value: String, -) { - BACKGROUND_LOCATION( - label = "Background location", - value = "android.permission.ACCESS_BACKGROUND_LOCATION" - ), - ACCESS_COARSE_LOCATION( - label = "Approximate location", - value = "android.permission.ACCESS_COARSE_LOCATION" - ), - ACCESS_FINE_LOCATION( - label = "Precise location", - value = "android.permission.ACCESS_FINE_LOCATION" - ) -} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/ContentUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/ContentUiState.kt new file mode 100644 index 00000000..ef09a8d3 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/ContentUiState.kt @@ -0,0 +1,12 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class ContentUiState( + val selectedIndex: Int +) + +fun previewContentUiState() = ContentUiState( + selectedIndex = 0 +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/CpuUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/CpuUiState.kt new file mode 100644 index 00000000..090eeb6c --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/CpuUiState.kt @@ -0,0 +1,17 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class CpuUiState( + val list: List +) + +@Immutable +data class CpuItem( + val name: String +) + +fun previewCpuUiState() = CpuUiState( + list = emptyList() +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/DeviceUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/DeviceUiState.kt new file mode 100644 index 00000000..3893c07d --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/DeviceUiState.kt @@ -0,0 +1,20 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class DeviceUiState( + val contentState: ContentUiState, + val infoState: InfoUiState, + val cpuState: CpuUiState, + val memoryState: MemoryUiState, + val permissionState: PermissionUiState +) + +internal fun previewDeviceUiState() = DeviceUiState( + contentState = previewContentUiState(), + cpuState = previewCpuUiState(), + memoryState = previewMemoryUiState(), + infoState = previewInfoUiState(), + permissionState = previewPermissionUiState() +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/InfoUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/InfoUiState.kt new file mode 100644 index 00000000..72c80782 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/InfoUiState.kt @@ -0,0 +1,22 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class InfoUiState( + val model: String, + val brand: String, + val versionRelease: String, + val versionSdk: String, + val serialNumber: String, + val battery: String +) + +fun previewInfoUiState() = InfoUiState( + model = "", + brand = "", + versionRelease = "", + versionSdk = "", + serialNumber = "", + battery = "" +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/MemoryUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/MemoryUiState.kt new file mode 100644 index 00000000..755ab2a2 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/MemoryUiState.kt @@ -0,0 +1,17 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class MemoryUiState( + val list: List +) + +@Immutable +data class MemoryItem( + val name: String +) + +fun previewMemoryUiState() = MemoryUiState( + list = emptyList() +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/PermissionUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/PermissionUiState.kt new file mode 100644 index 00000000..b1196494 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/PermissionUiState.kt @@ -0,0 +1,18 @@ +package io.github.openflocon.flocondesktop.device.models + +import androidx.compose.runtime.Immutable + +@Immutable +data class PermissionUiState( + val list: List +) + +@Immutable +data class PermissionItem( + val name: String, + val granted: Boolean +) + +fun previewPermissionUiState() = PermissionUiState( + list = emptyList() +) diff --git a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconCheckbox.kt b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconCheckbox.kt index 5354a6ad..72cb1c01 100644 --- a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconCheckbox.kt +++ b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconCheckbox.kt @@ -10,7 +10,7 @@ import io.github.openflocon.library.designsystem.FloconTheme @Composable fun FloconCheckbox( checked: Boolean, - onCheckedChange: (Boolean) -> Unit, + onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, uncheckedColor: Color = FloconTheme.colorPalette.primary ) { diff --git a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTextValue.kt b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTextValue.kt index 13f7655b..c160dab4 100644 --- a/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTextValue.kt +++ b/FloconDesktop/library/designsystem/src/commonMain/kotlin/io/github/openflocon/library/designsystem/components/FloconTextValue.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -16,25 +16,29 @@ import io.github.openflocon.library.designsystem.FloconTheme @Composable fun FloconTextValue( label: String, - value: String + value: String, + modifier: Modifier = Modifier ) { Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(2.dp) + modifier = modifier.padding(2.dp) ) { Text( text = label, style = FloconTheme.typography.labelSmall, modifier = Modifier.weight(1f) ) - Text( - text = value, - style = FloconTheme.typography.labelSmall, + SelectionContainer( modifier = Modifier.weight(1f) .clip(FloconTheme.shapes.small) .background(FloconTheme.colorPalette.primary) .padding(4.dp) - ) + ) { + Text( + text = value, + style = FloconTheme.typography.labelSmall + ) + } } }