Skip to content

Commit 0570fb1

Browse files
feature: Add battery
1 parent 12d199e commit 0570fb1

File tree

7 files changed

+405
-16
lines changed

7 files changed

+405
-16
lines changed

FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceScreen.kt

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
2323
import io.github.openflocon.domain.device.models.DeviceId
2424
import io.github.openflocon.flocondesktop.common.ui.window.FloconWindow
2525
import io.github.openflocon.flocondesktop.common.ui.window.createFloconWindowState
26+
import io.github.openflocon.flocondesktop.device.models.BatteryUiState
2627
import io.github.openflocon.flocondesktop.device.models.DeviceUiState
2728
import io.github.openflocon.flocondesktop.device.models.previewDeviceUiState
29+
import io.github.openflocon.flocondesktop.device.pages.BatteryPage
2830
import io.github.openflocon.flocondesktop.device.pages.CpuPage
2931
import io.github.openflocon.flocondesktop.device.pages.InfoPage
32+
import io.github.openflocon.flocondesktop.device.pages.MemoryPage
3033
import io.github.openflocon.flocondesktop.device.pages.PermissionPage
3134
import io.github.openflocon.library.designsystem.FloconTheme
3235
import io.github.openflocon.library.designsystem.components.FloconHorizontalDivider
@@ -63,7 +66,7 @@ private fun Content(
6366
onCloseRequest: () -> Unit,
6467
onAction: (DeviceAction) -> Unit
6568
) {
66-
val pagerState = rememberPagerState { 3 }
69+
val pagerState = rememberPagerState { 5 }
6770

6871
LaunchedEffect(uiState.contentState.selectedIndex) {
6972
pagerState.animateScrollToPage(uiState.contentState.selectedIndex)
@@ -104,11 +107,23 @@ private fun Content(
104107
selectedContentColor = FloconTheme.colorPalette.onSurface
105108
)
106109
FloconTab(
107-
text = "Permission",
110+
text = "Memory",
108111
selected = uiState.contentState.selectedIndex == 2,
109112
onClick = { onAction(DeviceAction.SelectTab(2)) },
110113
selectedContentColor = FloconTheme.colorPalette.onSurface
111114
)
115+
FloconTab(
116+
text = "Permission",
117+
selected = uiState.contentState.selectedIndex == 3,
118+
onClick = { onAction(DeviceAction.SelectTab(3)) },
119+
selectedContentColor = FloconTheme.colorPalette.onSurface
120+
)
121+
FloconTab(
122+
text = "Battery",
123+
selected = uiState.contentState.selectedIndex == 4,
124+
onClick = { onAction(DeviceAction.SelectTab(4)) },
125+
selectedContentColor = FloconTheme.colorPalette.onSurface
126+
)
112127
}
113128
FloconHorizontalDivider(
114129
modifier = Modifier.fillMaxWidth(),
@@ -132,11 +147,15 @@ private fun Content(
132147
state = uiState.cpuState,
133148
onAction = onAction
134149
)
150+
2 -> MemoryPage(state = uiState.memoryState)
135151

136-
2 -> PermissionPage(
152+
3 -> PermissionPage(
137153
state = uiState.permissionState,
138154
onAction = onAction
139155
)
156+
4 -> BatteryPage(
157+
state = uiState.batteryState
158+
)
140159

141160
else -> Unit
142161
}

FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/DeviceViewModel.kt

Lines changed: 137 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import io.github.openflocon.domain.adb.usecase.GetDeviceSerialUseCase
66
import io.github.openflocon.domain.adb.usecase.SendCommandUseCase
77
import io.github.openflocon.domain.common.getOrNull
88
import io.github.openflocon.domain.device.usecase.GetCurrentDeviceIdAndPackageNameUseCase
9+
import io.github.openflocon.flocondesktop.device.models.BatteryUiState
910
import io.github.openflocon.flocondesktop.device.models.ContentUiState
1011
import io.github.openflocon.flocondesktop.device.models.CpuItem
1112
import io.github.openflocon.flocondesktop.device.models.CpuUiState
1213
import io.github.openflocon.flocondesktop.device.models.DeviceUiState
1314
import io.github.openflocon.flocondesktop.device.models.InfoUiState
15+
import io.github.openflocon.flocondesktop.device.models.MemoryItem
1416
import io.github.openflocon.flocondesktop.device.models.MemoryUiState
1517
import io.github.openflocon.flocondesktop.device.models.PermissionItem
1618
import io.github.openflocon.flocondesktop.device.models.PermissionUiState
@@ -40,6 +42,28 @@ internal class DeviceViewModel(
4042
battery = ""
4143
)
4244
)
45+
private val batteryState = MutableStateFlow(
46+
BatteryUiState(
47+
acPowered = false,
48+
usbPowered = false,
49+
wirelessPowered = false,
50+
dockPowered = false,
51+
maxChargingCurrent = 0,
52+
maxChargingVoltage = 0,
53+
chargeCounter = 0,
54+
status = 0,
55+
health = 0,
56+
present = false,
57+
level = 0,
58+
scale = 0,
59+
voltage = 0,
60+
temperature = 0,
61+
technology = "",
62+
chargingState = 0,
63+
chargingPolicy = 0,
64+
capacityLevel = 0
65+
)
66+
)
4367
private val memoryState = MutableStateFlow(MemoryUiState(emptyList()))
4468
private val cpuState = MutableStateFlow(CpuUiState(emptyList()))
4569
private val permissionState = MutableStateFlow(PermissionUiState(emptyList()))
@@ -49,14 +73,16 @@ internal class DeviceViewModel(
4973
infoState,
5074
memoryState,
5175
cpuState,
52-
permissionState
53-
) { content, info, memory, cpu, permission ->
76+
permissionState,
77+
batteryState
78+
) { states ->
5479
DeviceUiState(
55-
contentState = content,
56-
infoState = info,
57-
memoryState = memory,
58-
cpuState = cpu,
59-
permissionState = permission
80+
contentState = states[0] as ContentUiState,
81+
infoState = states[1] as InfoUiState,
82+
memoryState = states[2] as MemoryUiState,
83+
cpuState = states[3] as CpuUiState,
84+
permissionState = states[4] as PermissionUiState,
85+
batteryState = states[5] as BatteryUiState
6086
)
6187
}
6288
.stateIn(
@@ -67,7 +93,8 @@ internal class DeviceViewModel(
6793
infoState = infoState.value,
6894
memoryState = memoryState.value,
6995
cpuState = cpuState.value,
70-
permissionState = permissionState.value
96+
permissionState = permissionState.value,
97+
batteryState = batteryState.value
7198
)
7299
)
73100

@@ -106,10 +133,12 @@ internal class DeviceViewModel(
106133

107134
private fun onRefresh() {
108135
refreshCpu()
136+
refreshMemory()
137+
refreshBattery()
109138
deviceInfo()
110139
fetchPermission()
111140
viewModelScope.launch {
112-
// battery = sendCommand("shell", "dumpsys", "battery"),
141+
// battery = sendCommand("shell", "dumpsys", "battery"),
113142
// mem = sendCommand("shell", "dumpsys", "meminfo")
114143
}
115144
}
@@ -203,9 +232,107 @@ internal class DeviceViewModel(
203232
}
204233
}
205234

235+
private fun refreshMemory() {
236+
viewModelScope.launch(Dispatchers.IO) {
237+
val output = sendCommand("shell", "dumpsys", "meminfo")
238+
val regex = MEM_REGEX.toRegex()
239+
val items = output.lineSequence()
240+
.map { it.trim() }
241+
.mapNotNull { regex.find(it) }
242+
.mapNotNull {
243+
try {
244+
MemoryItem(
245+
memoryUsage = formatMemoryUsage(
246+
memoryUsageKB = it.groupValues[1].replace(",", "").toDoubleOrNull()
247+
),
248+
processName = it.groupValues[2],
249+
pid = it.groupValues[3].toIntOrNull() ?: return@mapNotNull null
250+
)
251+
} catch (e: NumberFormatException) {
252+
// Handle parsing errors gracefully (e.g., log the error)
253+
null
254+
}
255+
}
256+
.toList()
257+
258+
memoryState.update { it.copy(list = items) }
259+
}
260+
}
261+
262+
private fun formatMemoryUsage(memoryUsageKB: Double?): String {
263+
if (memoryUsageKB == null) {
264+
return "N/A" // Or handle null case as appropriate
265+
}
266+
267+
val memoryUsage = memoryUsageKB * 1024 // Convert KB to bytes
268+
269+
return when {
270+
memoryUsage < 1024 -> String.format("%.2f B", memoryUsage)
271+
memoryUsage < 1024 * 1024 -> String.format("%.2f KB", memoryUsage / 1024)
272+
memoryUsage < 1024 * 1024 * 1024 -> String.format("%.2f MB", memoryUsage / (1024 * 1024))
273+
else -> String.format("%.2f GB", memoryUsage / (1024 * 1024 * 1024))
274+
}
275+
}
276+
277+
private fun refreshBattery() {
278+
viewModelScope.launch {
279+
val batteryInfo = sendCommand("shell", "dumpsys", "battery")
280+
281+
batteryState.update {
282+
it.copy(
283+
acPowered = AC_POWERED_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toBoolean() ?: false,
284+
usbPowered = USB_POWERED_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toBoolean() ?: false,
285+
wirelessPowered = WIRELESS_POWERED_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toBoolean()
286+
?: false,
287+
dockPowered = DOCK_POWERED_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toBoolean() ?: false,
288+
maxChargingCurrent = MAX_CHARGING_CURRENT_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
289+
maxChargingVoltage = MAX_CHARGING_VOLTAGE_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
290+
chargeCounter = CHARGE_COUNTER_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
291+
status = STATUS_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
292+
health = HEALTH_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
293+
present = PRESENT_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toBoolean() ?: false,
294+
level = LEVEL_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
295+
scale = SCALE_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
296+
voltage = VOLTAGE_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
297+
temperature = TEMPERATURE_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
298+
technology = TECHNOLOGY_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1),
299+
chargingState = CHARGING_STATE_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
300+
chargingPolicy = CHARGING_POLICY_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull(),
301+
capacityLevel = CAPACITY_LEVEL_REGEX.toRegex().find(batteryInfo)?.groupValues?.get(1)?.toIntOrNull()
302+
)
303+
}
304+
}
305+
}
306+
206307
companion object {
207308
private const val PERMISSION_PREFIX = "android.permission."
208-
private const val CPU_REGEX = """(\d+(?:\.\d+)?)%\s+([^:]+):\s+(\d+(?:\.\d+)?)%\s+user\s+\+\s+(\d+(?:\.\d+)?)%\s+kernel\s+/ faults:\s+(\d+)\s+minor\s+(\d+)\s+major"""
309+
310+
// CPU
311+
private const val CPU_REGEX =
312+
"""(\d+(?:\.\d+)?)%\s+([^:]+):\s+(\d+(?:\.\d+)?)%\s+user\s+\+\s+(\d+(?:\.\d+)?)%\s+kernel\s+/ faults:\s+(\d+)\s+minor\s+(\d+)\s+major"""
313+
314+
// MEM
315+
private const val MEM_REGEX = """([\d,]+)K:\s+([a-zA-Z0-9._:-]+)\s+$${"pid"}\s+(\d+)(?:\s+/\s+([a-zA-Z\s]+))?$"""
316+
317+
// Battery
318+
private const val AC_POWERED_REGEX = """AC powered:\s+(true|false)"""
319+
private const val USB_POWERED_REGEX = """USB powered:\s+(true|false)"""
320+
private const val WIRELESS_POWERED_REGEX = """Wireless powered:\s+(true|false)"""
321+
private const val DOCK_POWERED_REGEX = """Dock powered:\s+(true|false)"""
322+
private const val MAX_CHARGING_CURRENT_REGEX = """Max charging current:\s+(\d+)"""
323+
private const val MAX_CHARGING_VOLTAGE_REGEX = """Max charging voltage:\s+(\d+)"""
324+
private const val CHARGE_COUNTER_REGEX = """Charge counter:\s+(\d+)"""
325+
private const val STATUS_REGEX = """status:\s+(\d+)"""
326+
private const val HEALTH_REGEX = """health:\s+(\d+)"""
327+
private const val PRESENT_REGEX = """present:\s+(true|false)"""
328+
private const val LEVEL_REGEX = """level:\s+(\d+)"""
329+
private const val SCALE_REGEX = """scale:\s+(\d+)"""
330+
private const val VOLTAGE_REGEX = """voltage:\s+(\d+)"""
331+
private const val TEMPERATURE_REGEX = """temperature:\s+(\d+)"""
332+
private const val TECHNOLOGY_REGEX = """technology:\s+([a-zA-Z-]+)"""
333+
private const val CHARGING_STATE_REGEX = """Charging state:\s+(\d+)"""
334+
private const val CHARGING_POLICY_REGEX = """Charging policy:\s+(\d+)"""
335+
private const val CAPACITY_LEVEL_REGEX = """Capacity level:\s+(\d+)"""
209336
}
210337

211338
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.github.openflocon.flocondesktop.device.models
2+
3+
import androidx.compose.runtime.Immutable
4+
5+
@Immutable
6+
data class BatteryUiState(
7+
val acPowered: Boolean?,
8+
val usbPowered: Boolean?,
9+
val wirelessPowered: Boolean?,
10+
val dockPowered: Boolean?,
11+
val maxChargingCurrent: Int?,
12+
val maxChargingVoltage: Int?,
13+
val chargeCounter: Int?,
14+
val status: Int?,
15+
val health: Int?,
16+
val present: Boolean?,
17+
val level: Int?,
18+
val scale: Int?,
19+
val voltage: Int?,
20+
val temperature: Int?,
21+
val technology: String?,
22+
val chargingState: Int?,
23+
val chargingPolicy: Int?,
24+
val capacityLevel: Int?
25+
)
26+
27+
fun previewBatteryUiState() = BatteryUiState(
28+
acPowered = true,
29+
usbPowered = false,
30+
wirelessPowered = true,
31+
dockPowered = false,
32+
maxChargingCurrent = 100,
33+
maxChargingVoltage = 100,
34+
chargeCounter = 100,
35+
status = 100,
36+
health = 100,
37+
present = true,
38+
level = 100,
39+
scale = 100,
40+
voltage = 100,
41+
temperature = 100,
42+
technology = "Technology",
43+
chargingState = 100,
44+
chargingPolicy = 100,
45+
capacityLevel = 100
46+
)

FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/DeviceUiState.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ data class DeviceUiState(
88
val infoState: InfoUiState,
99
val cpuState: CpuUiState,
1010
val memoryState: MemoryUiState,
11-
val permissionState: PermissionUiState
11+
val permissionState: PermissionUiState,
12+
val batteryState: BatteryUiState
1213
)
1314

1415
internal fun previewDeviceUiState() = DeviceUiState(
1516
contentState = previewContentUiState(),
1617
cpuState = previewCpuUiState(),
1718
memoryState = previewMemoryUiState(),
1819
infoState = previewInfoUiState(),
19-
permissionState = previewPermissionUiState()
20+
permissionState = previewPermissionUiState(),
21+
batteryState = previewBatteryUiState()
2022
)

FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/device/models/MemoryUiState.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ data class MemoryUiState(
99

1010
@Immutable
1111
data class MemoryItem(
12-
val name: String
12+
val memoryUsage: String,
13+
val processName: String,
14+
val pid: Int
1315
)
1416

1517
fun previewMemoryUiState() = MemoryUiState(

0 commit comments

Comments
 (0)