fix: Permission

This commit is contained in:
TEYSSANDIER Raphael 2025-09-22 16:51:43 +02:00 committed by Raphael TEYSSANDIER
parent 63e6ec5a27
commit da69d59382
13 changed files with 221 additions and 131 deletions

View file

@ -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
}

View file

@ -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,
)
}
}

View file

@ -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<PermissionUiState>
)
@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()
)

View file

@ -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."
}
}

View file

@ -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"
)
}

View file

@ -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
)

View file

@ -0,0 +1,17 @@
package io.github.openflocon.flocondesktop.device.models
import androidx.compose.runtime.Immutable
@Immutable
data class CpuUiState(
val list: List<CpuItem>
)
@Immutable
data class CpuItem(
val name: String
)
fun previewCpuUiState() = CpuUiState(
list = emptyList()
)

View file

@ -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()
)

View file

@ -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 = ""
)

View file

@ -0,0 +1,17 @@
package io.github.openflocon.flocondesktop.device.models
import androidx.compose.runtime.Immutable
@Immutable
data class MemoryUiState(
val list: List<MemoryItem>
)
@Immutable
data class MemoryItem(
val name: String
)
fun previewMemoryUiState() = MemoryUiState(
list = emptyList()
)

View file

@ -0,0 +1,18 @@
package io.github.openflocon.flocondesktop.device.models
import androidx.compose.runtime.Immutable
@Immutable
data class PermissionUiState(
val list: List<PermissionItem>
)
@Immutable
data class PermissionItem(
val name: String,
val granted: Boolean
)
fun previewPermissionUiState() = PermissionUiState(
list = emptyList()
)

View file

@ -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
) {

View file

@ -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
)
}
}
}