Skip to content

Commit 069cd08

Browse files
committed
Add Expert Mode debug screen for raw getevent output (issue #2081)
Creates a new debug screen accessible from Settings > Debugging that shows raw getevent output using the system bridge. Displays getevent -il on open (device info) and supports recording getevent -lt events via a red record/stop FAB. Output can be copied to clipboard or saved to file. Requires Expert Mode. https://claude.ai/code/session_01QG84he6gs9tqgY9QrFhtKb
1 parent c9c2d8d commit 069cd08

7 files changed

Lines changed: 485 additions & 0 deletions

File tree

base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import io.github.sds100.keymapper.base.constraints.ChooseConstraintScreen
2727
import io.github.sds100.keymapper.base.constraints.ChooseConstraintViewModel
2828
import io.github.sds100.keymapper.base.expertmode.ExpertModeScreen
2929
import io.github.sds100.keymapper.base.expertmode.ExpertModeSetupScreen
30+
import io.github.sds100.keymapper.base.debug.GetEventScreen
3031
import io.github.sds100.keymapper.base.logging.LogScreen
3132
import io.github.sds100.keymapper.base.onboarding.HandleAccessibilityServiceDialogs
3233
import io.github.sds100.keymapper.base.onboarding.SetupAccessibilityServiceDelegateImpl
@@ -165,6 +166,14 @@ fun BaseMainNavHost(
165166
)
166167
}
167168

169+
composable<NavDestination.GetEvent> {
170+
GetEventScreen(
171+
modifier = Modifier.fillMaxSize(),
172+
viewModel = hiltViewModel(),
173+
onBackClick = { navController.popBackStack() },
174+
)
175+
}
176+
168177
composable<NavDestination.ChooseSetting> {
169178
ChooseSettingScreen(
170179
modifier = Modifier.fillMaxSize(),
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
package io.github.sds100.keymapper.base.debug
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.calculateEndPadding
7+
import androidx.compose.foundation.layout.calculateStartPadding
8+
import androidx.compose.foundation.layout.displayCutoutPadding
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.height
12+
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.foundation.layout.width
14+
import androidx.compose.foundation.rememberScrollState
15+
import androidx.compose.foundation.text.selection.SelectionContainer
16+
import androidx.compose.foundation.verticalScroll
17+
import androidx.compose.material.icons.Icons
18+
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
19+
import androidx.compose.material.icons.outlined.ContentCopy
20+
import androidx.compose.material.icons.outlined.Delete
21+
import androidx.compose.material.icons.outlined.Share
22+
import androidx.compose.material.icons.rounded.FiberManualRecord
23+
import androidx.compose.material.icons.rounded.Stop
24+
import androidx.compose.material3.BottomAppBar
25+
import androidx.compose.material3.ButtonDefaults
26+
import androidx.compose.material3.ElevatedCard
27+
import androidx.compose.material3.ExperimentalMaterial3Api
28+
import androidx.compose.material3.FloatingActionButton
29+
import androidx.compose.material3.FloatingActionButtonDefaults
30+
import androidx.compose.material3.Icon
31+
import androidx.compose.material3.IconButton
32+
import androidx.compose.material3.LinearProgressIndicator
33+
import androidx.compose.material3.MaterialTheme
34+
import androidx.compose.material3.OutlinedButton
35+
import androidx.compose.material3.Scaffold
36+
import androidx.compose.material3.Surface
37+
import androidx.compose.material3.Text
38+
import androidx.compose.material3.TopAppBar
39+
import androidx.compose.runtime.Composable
40+
import androidx.compose.runtime.LaunchedEffect
41+
import androidx.compose.ui.Modifier
42+
import androidx.compose.ui.platform.LocalLayoutDirection
43+
import androidx.compose.ui.res.stringResource
44+
import androidx.compose.ui.text.font.FontFamily
45+
import androidx.compose.ui.tooling.preview.Preview
46+
import androidx.compose.ui.unit.dp
47+
import androidx.hilt.navigation.compose.hiltViewModel
48+
import io.github.sds100.keymapper.base.R
49+
import io.github.sds100.keymapper.base.compose.KeyMapperTheme
50+
import io.github.sds100.keymapper.base.utils.ExpertModeStatus
51+
52+
@Composable
53+
fun GetEventScreen(
54+
modifier: Modifier = Modifier,
55+
viewModel: GetEventViewModel = hiltViewModel(),
56+
onBackClick: () -> Unit,
57+
) {
58+
GetEventScreen(
59+
modifier = modifier,
60+
state = viewModel.state,
61+
onBackClick = onBackClick,
62+
onToggleRecordClick = viewModel::onToggleRecordClick,
63+
onClearClick = viewModel::onClearClick,
64+
onCopyToClipboardClick = viewModel::onCopyToClipboardClick,
65+
onSaveToFileClick = viewModel::onSaveToFileClick,
66+
onSetupExpertModeClick = viewModel::onSetupExpertModeClick,
67+
)
68+
}
69+
70+
@OptIn(ExperimentalMaterial3Api::class)
71+
@Composable
72+
private fun GetEventScreen(
73+
modifier: Modifier = Modifier,
74+
state: GetEventViewModel.State,
75+
onBackClick: () -> Unit = {},
76+
onToggleRecordClick: () -> Unit = {},
77+
onClearClick: () -> Unit = {},
78+
onCopyToClipboardClick: () -> Unit = {},
79+
onSaveToFileClick: () -> Unit = {},
80+
onSetupExpertModeClick: () -> Unit = {},
81+
) {
82+
val hasOutput = state.output.isNotEmpty()
83+
84+
Scaffold(
85+
modifier = modifier.displayCutoutPadding(),
86+
topBar = {
87+
TopAppBar(
88+
title = { Text(stringResource(R.string.title_debug_getevent)) },
89+
)
90+
},
91+
bottomBar = {
92+
BottomAppBar(
93+
floatingActionButton = {
94+
if (state.expertModeStatus == ExpertModeStatus.ENABLED) {
95+
val containerColor = if (state.isRecording) {
96+
MaterialTheme.colorScheme.errorContainer
97+
} else {
98+
MaterialTheme.colorScheme.primaryContainer
99+
}
100+
val contentColor = if (state.isRecording) {
101+
MaterialTheme.colorScheme.onErrorContainer
102+
} else {
103+
MaterialTheme.colorScheme.onPrimaryContainer
104+
}
105+
FloatingActionButton(
106+
onClick = onToggleRecordClick,
107+
containerColor = containerColor,
108+
contentColor = contentColor,
109+
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
110+
) {
111+
if (state.isRecording) {
112+
Icon(
113+
imageVector = Icons.Rounded.Stop,
114+
contentDescription = stringResource(R.string.debug_getevent_stop_recording),
115+
)
116+
} else {
117+
Icon(
118+
imageVector = Icons.Rounded.FiberManualRecord,
119+
contentDescription = stringResource(R.string.debug_getevent_start_recording),
120+
)
121+
}
122+
}
123+
}
124+
},
125+
actions = {
126+
IconButton(onClick = onBackClick) {
127+
Icon(
128+
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
129+
contentDescription = stringResource(R.string.action_go_back),
130+
)
131+
}
132+
Spacer(modifier = Modifier.weight(1f))
133+
IconButton(
134+
onClick = onCopyToClipboardClick,
135+
enabled = hasOutput,
136+
) {
137+
Icon(
138+
imageVector = Icons.Outlined.ContentCopy,
139+
contentDescription = stringResource(R.string.debug_getevent_copy),
140+
)
141+
}
142+
IconButton(
143+
onClick = onSaveToFileClick,
144+
enabled = hasOutput,
145+
) {
146+
Icon(
147+
imageVector = Icons.Outlined.Share,
148+
contentDescription = stringResource(R.string.debug_getevent_save),
149+
)
150+
}
151+
IconButton(
152+
onClick = onClearClick,
153+
enabled = hasOutput,
154+
) {
155+
Icon(
156+
imageVector = Icons.Outlined.Delete,
157+
contentDescription = stringResource(R.string.debug_getevent_clear),
158+
)
159+
}
160+
},
161+
)
162+
},
163+
) { innerPadding ->
164+
val layoutDirection = LocalLayoutDirection.current
165+
val startPadding = innerPadding.calculateStartPadding(layoutDirection)
166+
val endPadding = innerPadding.calculateEndPadding(layoutDirection)
167+
168+
Surface(
169+
modifier = Modifier
170+
.fillMaxSize()
171+
.padding(
172+
top = innerPadding.calculateTopPadding(),
173+
bottom = innerPadding.calculateBottomPadding(),
174+
start = startPadding,
175+
end = endPadding,
176+
),
177+
) {
178+
Content(
179+
modifier = Modifier.fillMaxSize(),
180+
state = state,
181+
onSetupExpertModeClick = onSetupExpertModeClick,
182+
)
183+
}
184+
}
185+
}
186+
187+
@Composable
188+
private fun Content(
189+
modifier: Modifier = Modifier,
190+
state: GetEventViewModel.State,
191+
onSetupExpertModeClick: () -> Unit,
192+
) {
193+
val scrollState = rememberScrollState()
194+
195+
LaunchedEffect(state.output) {
196+
if (state.output.isNotEmpty()) {
197+
scrollState.animateScrollTo(scrollState.maxValue)
198+
}
199+
}
200+
201+
Column(
202+
modifier = modifier
203+
.verticalScroll(scrollState)
204+
.padding(16.dp),
205+
verticalArrangement = Arrangement.spacedBy(16.dp),
206+
) {
207+
if (state.expertModeStatus != ExpertModeStatus.ENABLED) {
208+
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
209+
Column(
210+
modifier = Modifier.padding(16.dp),
211+
verticalArrangement = Arrangement.spacedBy(8.dp),
212+
) {
213+
Text(
214+
text = stringResource(R.string.debug_getevent_expert_mode_required),
215+
style = MaterialTheme.typography.bodyMedium,
216+
)
217+
OutlinedButton(
218+
onClick = onSetupExpertModeClick,
219+
colors = ButtonDefaults.outlinedButtonColors(),
220+
) {
221+
Text(stringResource(R.string.action_shell_command_setup_expert_mode))
222+
}
223+
}
224+
}
225+
}
226+
227+
if (state.isLoadingDeviceInfo) {
228+
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
229+
}
230+
231+
if (state.isRecording) {
232+
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
233+
}
234+
235+
if (state.output.isNotEmpty()) {
236+
SelectionContainer {
237+
Text(
238+
text = state.output,
239+
style = MaterialTheme.typography.bodySmall.copy(
240+
fontFamily = FontFamily.Monospace,
241+
),
242+
)
243+
}
244+
}
245+
246+
Spacer(modifier = Modifier.height(8.dp))
247+
}
248+
}
249+
250+
@Preview
251+
@Composable
252+
private fun PreviewWithOutput() {
253+
KeyMapperTheme {
254+
GetEventScreen(
255+
state = GetEventViewModel.State(
256+
output = "add device 1: /dev/input/event0\n name: \"gpio-keys\"\n\nadd device 2: /dev/input/event1\n name: \"sec_touchscreen\"",
257+
expertModeStatus = ExpertModeStatus.ENABLED,
258+
),
259+
)
260+
}
261+
}
262+
263+
@Preview
264+
@Composable
265+
private fun PreviewRecording() {
266+
KeyMapperTheme {
267+
GetEventScreen(
268+
state = GetEventViewModel.State(
269+
output = "add device 1: /dev/input/event0\n name: \"gpio-keys\"",
270+
isRecording = true,
271+
expertModeStatus = ExpertModeStatus.ENABLED,
272+
),
273+
)
274+
}
275+
}
276+
277+
@Preview
278+
@Composable
279+
private fun PreviewExpertModeDisabled() {
280+
KeyMapperTheme {
281+
GetEventScreen(
282+
state = GetEventViewModel.State(
283+
expertModeStatus = ExpertModeStatus.DISABLED,
284+
),
285+
)
286+
}
287+
}

0 commit comments

Comments
 (0)