From 069cd08f45a79bd3dfdec5b4e4a6ef4980126c4e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 12:29:41 +0000 Subject: [PATCH 1/7] 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 --- .../sds100/keymapper/base/BaseMainNavHost.kt | 9 + .../keymapper/base/debug/GetEventScreen.kt | 287 ++++++++++++++++++ .../keymapper/base/debug/GetEventViewModel.kt | 157 ++++++++++ .../keymapper/base/settings/SettingsScreen.kt | 9 + .../base/settings/SettingsViewModel.kt | 6 + .../base/utils/navigation/NavDestination.kt | 6 + base/src/main/res/values/strings.xml | 11 + 7 files changed, 485 insertions(+) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt index 0b22cc858e..acc0092714 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt @@ -27,6 +27,7 @@ import io.github.sds100.keymapper.base.constraints.ChooseConstraintScreen import io.github.sds100.keymapper.base.constraints.ChooseConstraintViewModel import io.github.sds100.keymapper.base.expertmode.ExpertModeScreen import io.github.sds100.keymapper.base.expertmode.ExpertModeSetupScreen +import io.github.sds100.keymapper.base.debug.GetEventScreen import io.github.sds100.keymapper.base.logging.LogScreen import io.github.sds100.keymapper.base.onboarding.HandleAccessibilityServiceDialogs import io.github.sds100.keymapper.base.onboarding.SetupAccessibilityServiceDelegateImpl @@ -165,6 +166,14 @@ fun BaseMainNavHost( ) } + composable { + GetEventScreen( + modifier = Modifier.fillMaxSize(), + viewModel = hiltViewModel(), + onBackClick = { navController.popBackStack() }, + ) + } + composable { ChooseSettingScreen( modifier = Modifier.fillMaxSize(), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt new file mode 100644 index 0000000000..fc675cd7c0 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt @@ -0,0 +1,287 @@ +package io.github.sds100.keymapper.base.debug + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material.icons.rounded.FiberManualRecord +import androidx.compose.material.icons.rounded.Stop +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ExpertModeStatus + +@Composable +fun GetEventScreen( + modifier: Modifier = Modifier, + viewModel: GetEventViewModel = hiltViewModel(), + onBackClick: () -> Unit, +) { + GetEventScreen( + modifier = modifier, + state = viewModel.state, + onBackClick = onBackClick, + onToggleRecordClick = viewModel::onToggleRecordClick, + onClearClick = viewModel::onClearClick, + onCopyToClipboardClick = viewModel::onCopyToClipboardClick, + onSaveToFileClick = viewModel::onSaveToFileClick, + onSetupExpertModeClick = viewModel::onSetupExpertModeClick, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun GetEventScreen( + modifier: Modifier = Modifier, + state: GetEventViewModel.State, + onBackClick: () -> Unit = {}, + onToggleRecordClick: () -> Unit = {}, + onClearClick: () -> Unit = {}, + onCopyToClipboardClick: () -> Unit = {}, + onSaveToFileClick: () -> Unit = {}, + onSetupExpertModeClick: () -> Unit = {}, +) { + val hasOutput = state.output.isNotEmpty() + + Scaffold( + modifier = modifier.displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.title_debug_getevent)) }, + ) + }, + bottomBar = { + BottomAppBar( + floatingActionButton = { + if (state.expertModeStatus == ExpertModeStatus.ENABLED) { + val containerColor = if (state.isRecording) { + MaterialTheme.colorScheme.errorContainer + } else { + MaterialTheme.colorScheme.primaryContainer + } + val contentColor = if (state.isRecording) { + MaterialTheme.colorScheme.onErrorContainer + } else { + MaterialTheme.colorScheme.onPrimaryContainer + } + FloatingActionButton( + onClick = onToggleRecordClick, + containerColor = containerColor, + contentColor = contentColor, + elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(), + ) { + if (state.isRecording) { + Icon( + imageVector = Icons.Rounded.Stop, + contentDescription = stringResource(R.string.debug_getevent_stop_recording), + ) + } else { + Icon( + imageVector = Icons.Rounded.FiberManualRecord, + contentDescription = stringResource(R.string.debug_getevent_start_recording), + ) + } + } + } + }, + actions = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.action_go_back), + ) + } + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = onCopyToClipboardClick, + enabled = hasOutput, + ) { + Icon( + imageVector = Icons.Outlined.ContentCopy, + contentDescription = stringResource(R.string.debug_getevent_copy), + ) + } + IconButton( + onClick = onSaveToFileClick, + enabled = hasOutput, + ) { + Icon( + imageVector = Icons.Outlined.Share, + contentDescription = stringResource(R.string.debug_getevent_save), + ) + } + IconButton( + onClick = onClearClick, + enabled = hasOutput, + ) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = stringResource(R.string.debug_getevent_clear), + ) + } + }, + ) + }, + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + val startPadding = innerPadding.calculateStartPadding(layoutDirection) + val endPadding = innerPadding.calculateEndPadding(layoutDirection) + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding(), + start = startPadding, + end = endPadding, + ), + ) { + Content( + modifier = Modifier.fillMaxSize(), + state = state, + onSetupExpertModeClick = onSetupExpertModeClick, + ) + } + } +} + +@Composable +private fun Content( + modifier: Modifier = Modifier, + state: GetEventViewModel.State, + onSetupExpertModeClick: () -> Unit, +) { + val scrollState = rememberScrollState() + + LaunchedEffect(state.output) { + if (state.output.isNotEmpty()) { + scrollState.animateScrollTo(scrollState.maxValue) + } + } + + Column( + modifier = modifier + .verticalScroll(scrollState) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (state.expertModeStatus != ExpertModeStatus.ENABLED) { + ElevatedCard(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(R.string.debug_getevent_expert_mode_required), + style = MaterialTheme.typography.bodyMedium, + ) + OutlinedButton( + onClick = onSetupExpertModeClick, + colors = ButtonDefaults.outlinedButtonColors(), + ) { + Text(stringResource(R.string.action_shell_command_setup_expert_mode)) + } + } + } + } + + if (state.isLoadingDeviceInfo) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + + if (state.isRecording) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + + if (state.output.isNotEmpty()) { + SelectionContainer { + Text( + text = state.output, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + ), + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Preview +@Composable +private fun PreviewWithOutput() { + KeyMapperTheme { + GetEventScreen( + state = GetEventViewModel.State( + output = "add device 1: /dev/input/event0\n name: \"gpio-keys\"\n\nadd device 2: /dev/input/event1\n name: \"sec_touchscreen\"", + expertModeStatus = ExpertModeStatus.ENABLED, + ), + ) + } +} + +@Preview +@Composable +private fun PreviewRecording() { + KeyMapperTheme { + GetEventScreen( + state = GetEventViewModel.State( + output = "add device 1: /dev/input/event0\n name: \"gpio-keys\"", + isRecording = true, + expertModeStatus = ExpertModeStatus.ENABLED, + ), + ) + } +} + +@Preview +@Composable +private fun PreviewExpertModeDisabled() { + KeyMapperTheme { + GetEventScreen( + state = GetEventViewModel.State( + expertModeStatus = ExpertModeStatus.DISABLED, + ), + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt new file mode 100644 index 0000000000..89acde9931 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt @@ -0,0 +1,157 @@ +package io.github.sds100.keymapper.base.debug + +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.net.toUri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.base.utils.ExpertModeStatus +import io.github.sds100.keymapper.base.utils.ShareUtils +import io.github.sds100.keymapper.base.utils.getFullMessage +import io.github.sds100.keymapper.base.utils.navigation.NavDestination +import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider +import io.github.sds100.keymapper.base.utils.navigation.navigate +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.base.actions.ExecuteShellCommandUseCase +import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.models.ShellExecutionMode +import io.github.sds100.keymapper.common.utils.handle +import io.github.sds100.keymapper.system.clipboard.ClipboardAdapter +import io.github.sds100.keymapper.system.files.FileAdapter +import io.github.sds100.keymapper.system.files.FileUtils +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +@HiltViewModel +class GetEventViewModel @Inject constructor( + private val executeShellCommandUseCase: ExecuteShellCommandUseCase, + private val navigationProvider: NavigationProvider, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val clipboardAdapter: ClipboardAdapter, + private val fileAdapter: FileAdapter, + private val buildConfigProvider: BuildConfigProvider, + @ApplicationContext private val context: Context, + resourceProvider: ResourceProvider, +) : ViewModel(), NavigationProvider by navigationProvider, ResourceProvider by resourceProvider { + + data class State( + val output: String = "", + val isLoadingDeviceInfo: Boolean = false, + val isRecording: Boolean = false, + val expertModeStatus: ExpertModeStatus = ExpertModeStatus.DISABLED, + ) + + var state: State by mutableStateOf(State()) + private set + + init { + viewModelScope.launch { + systemBridgeConnectionManager.connectionState.map { connectionState -> + when (connectionState) { + is SystemBridgeConnectionState.Connected -> ExpertModeStatus.ENABLED + is SystemBridgeConnectionState.Disconnected -> ExpertModeStatus.DISABLED + } + }.collect { status -> + val wasDisabled = state.expertModeStatus != ExpertModeStatus.ENABLED + state = state.copy(expertModeStatus = status) + if (status == ExpertModeStatus.ENABLED && wasDisabled && state.output.isEmpty()) { + loadDeviceInfo() + } + } + } + } + + private fun loadDeviceInfo() { + viewModelScope.launch { + state = state.copy(isLoadingDeviceInfo = true) + val result = executeShellCommandUseCase.execute( + command = "getevent -il", + executionMode = ShellExecutionMode.ADB, + timeoutMillis = 30_000L, + ) + val output = result.handle( + onSuccess = { it.stdout }, + onError = { "Error: ${it.getFullMessage(this@GetEventViewModel)}" }, + ) + state = state.copy(output = output, isLoadingDeviceInfo = false) + } + } + + fun onToggleRecordClick() { + if (state.isRecording) { + stopRecording() + } else { + startRecording() + } + } + + private fun startRecording() { + state = state.copy(isRecording = true) + viewModelScope.launch { + val result = executeShellCommandUseCase.execute( + command = "getevent -lt", + executionMode = ShellExecutionMode.ADB, + timeoutMillis = 300_000L, + ) + val newOutput = result.handle( + onSuccess = { it.stdout }, + onError = { "" }, + ) + if (newOutput.isNotEmpty()) { + state = state.copy( + output = state.output + "\n--- Recording ---\n" + newOutput, + ) + } + state = state.copy(isRecording = false) + } + } + + private fun stopRecording() { + viewModelScope.launch { + executeShellCommandUseCase.execute( + command = "pkill -x getevent || true", + executionMode = ShellExecutionMode.ADB, + timeoutMillis = 5_000L, + ) + } + } + + fun onClearClick() { + state = state.copy(output = "") + } + + fun onCopyToClipboardClick() { + clipboardAdapter.copy("getevent output", state.output) + } + + fun onSaveToFileClick() { + viewModelScope.launch(Dispatchers.IO) { + val fileName = "getevent/key_mapper_getevent_${FileUtils.createFileDate()}.txt" + val file = fileAdapter.getPrivateFile(fileName) + file.createFile() + file.outputStream()?.bufferedWriter()?.use { it.write(state.output) } + val publicUri = fileAdapter.getPublicUriForPrivateFile(file).toUri() + ShareUtils.shareFile(context, publicUri, buildConfigProvider.packageName) + } + } + + fun onBackClick() { + viewModelScope.launch { + navigationProvider.popBackStack() + } + } + + fun onSetupExpertModeClick() { + viewModelScope.launch { + navigationProvider.navigate("getevent_setup_expert_mode", NavDestination.ExpertModeSetup) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt index 5d79cd6838..b4435f13e2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -152,6 +152,7 @@ fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) onForceVibrateToggled = viewModel::onForceVibrateToggled, onLoggingToggled = viewModel::onLoggingToggled, onViewLogClick = viewModel::onViewLogClick, + onGetEventClick = viewModel::onGetEventClick, onHideHomeScreenAlertsToggled = viewModel::onHideHomeScreenAlertsToggled, onShowDeviceDescriptorsToggled = viewModel::onShowDeviceDescriptorsToggled, onAutomaticBackupClick = { @@ -235,6 +236,7 @@ private fun Content( onForceVibrateToggled: (Boolean) -> Unit = { }, onLoggingToggled: (Boolean) -> Unit = { }, onViewLogClick: () -> Unit = { }, + onGetEventClick: () -> Unit = { }, onShareLogcatClick: () -> Unit = { }, onHideHomeScreenAlertsToggled: (Boolean) -> Unit = { }, onShowDeviceDescriptorsToggled: (Boolean) -> Unit = { }, @@ -398,6 +400,13 @@ private fun Content( onClick = onShareLogcatClick, ) + OptionPageButton( + title = stringResource(R.string.title_pref_get_event_debug), + text = stringResource(R.string.summary_pref_get_event_debug), + icon = Icons.Rounded.Keyboard, + onClick = onGetEventClick, + ) + Spacer(modifier = Modifier.height(8.dp)) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 7628eb1edc..1a2ef5dbda 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -291,6 +291,12 @@ class SettingsViewModel @Inject constructor( } } + fun onGetEventClick() { + viewModelScope.launch { + navigate("get_event_debug", NavDestination.GetEvent) + } + } + fun onShareLogcatClick() { viewModelScope.launch { if (shareLogcatUseCase.isPermissionGranted()) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt index f13888bccd..6aae4a0db8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt @@ -45,6 +45,7 @@ abstract class NavDestination(val isCompose: Boolean = false) { const val ID_EXPERT_MODE = "expert_mode" const val ID_LOG = "log" const val ID_ADVANCED_TRIGGERS = "advanced_triggers" + const val ID_GET_EVENT = "get_event" } @Serializable @@ -211,4 +212,9 @@ abstract class NavDestination(val isCompose: Boolean = false) { data object AdvancedTriggers : NavDestination(isCompose = true) { override val id: String = ID_ADVANCED_TRIGGERS } + + @Serializable + data object GetEvent : NavDestination(isCompose = true) { + override val id: String = ID_GET_EVENT + } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 9bb7dc5d2f..002e8da6e4 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -635,6 +635,17 @@ Share the entire system log Sharing logcat failed + Expert Mode debug + View raw input device info and events + Expert Mode Debug + Expert Mode is required to use getevent. Start Expert Mode and then return to this screen. + Record + Stop Recording + Clear output + Loading device info\u2026 + Copy to clipboard + Save to file + Report issue Delete sound files From 297c8ef6c9c9dad110813ee234d60b0d7e2ab014 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 08:24:27 +0000 Subject: [PATCH 2/7] Fix ktlint violations in GetEventViewModel and GetEventScreen - Move ExecuteShellCommandUseCase import before base.utils.* (alphabetical order) - Remove unused `width` import from GetEventScreen https://claude.ai/code/session_01QG84he6gs9tqgY9QrFhtKb --- .../io/github/sds100/keymapper/base/debug/GetEventScreen.kt | 1 - .../io/github/sds100/keymapper/base/debug/GetEventViewModel.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt index fc675cd7c0..f4d18632be 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt index 89acde9931..42ca58ae17 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.base.actions.ExecuteShellCommandUseCase import io.github.sds100.keymapper.base.utils.ExpertModeStatus import io.github.sds100.keymapper.base.utils.ShareUtils import io.github.sds100.keymapper.base.utils.getFullMessage @@ -16,7 +17,6 @@ import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.base.actions.ExecuteShellCommandUseCase import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.models.ShellExecutionMode import io.github.sds100.keymapper.common.utils.handle From ddb95d447322978f984785ccddd11bfe20281afc Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 14 Apr 2026 10:32:50 +0200 Subject: [PATCH 3/7] #2081 fix padding and text style --- .../keymapper/base/debug/GetEventScreen.kt | 72 +++++++++++++++---- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt index fc675cd7c0..d904202a6d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.debug +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -10,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll @@ -44,6 +44,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme @@ -111,12 +112,16 @@ private fun GetEventScreen( if (state.isRecording) { Icon( imageVector = Icons.Rounded.Stop, - contentDescription = stringResource(R.string.debug_getevent_stop_recording), + contentDescription = stringResource( + R.string.debug_getevent_stop_recording, + ), ) } else { Icon( imageVector = Icons.Rounded.FiberManualRecord, - contentDescription = stringResource(R.string.debug_getevent_start_recording), + contentDescription = stringResource( + R.string.debug_getevent_start_recording, + ), ) } } @@ -198,14 +203,13 @@ private fun Content( } } - Column( - modifier = modifier - .verticalScroll(scrollState) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { + Column(modifier = modifier) { if (state.expertModeStatus != ExpertModeStatus.ENABLED) { - ElevatedCard(modifier = Modifier.fillMaxWidth()) { + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -225,20 +229,36 @@ private fun Content( } if (state.isLoadingDeviceInfo) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) } if (state.isRecording) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) } if (state.output.isNotEmpty()) { - SelectionContainer { + SelectionContainer( + modifier = Modifier + .horizontalScroll(rememberScrollState()) + .verticalScroll(rememberScrollState()), + ) { Text( + modifier = Modifier.padding(horizontal = 16.dp), text = state.output, + softWrap = false, style = MaterialTheme.typography.bodySmall.copy( + fontSize = 10.sp, fontFamily = FontFamily.Monospace, ), + lineHeight = 13.sp, ) } } @@ -253,7 +273,31 @@ private fun PreviewWithOutput() { KeyMapperTheme { GetEventScreen( state = GetEventViewModel.State( - output = "add device 1: /dev/input/event0\n name: \"gpio-keys\"\n\nadd device 2: /dev/input/event1\n name: \"sec_touchscreen\"", + output = """add device 1: /dev/input/event0 + bus: 0019 + vendor 0001 + product 0001 + version 0100 + name: "gpio-keys" + location: "gpio-keys/input0" + id: "" + version: 1.0.1 + events: + KEY (0001): KEY_POWER + input props: + +add device 2: /dev/input/event1 + bus: 0006 + vendor 0000 + product 0000 + version 0000 + name: "virtio_input_multi_touch_1" + location: "virtio10/input0" + id: "" + version: 1.0.1 + events: + KEY (0001): BTN_TOOL_RUBBER BTN_STYLUS + ABS (0003): ABS_X : value 0, min 0, max 32767, fuzz 0, flat 0, resolution 0"""", expertModeStatus = ExpertModeStatus.ENABLED, ), ) From 4f1648dd592ae4722a822f713cf04ef2dadd79d3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 5 May 2026 10:25:32 +0200 Subject: [PATCH 4/7] #2081 create tabs for info and events --- .../sds100/keymapper/base/BaseMainNavHost.kt | 2 +- .../keymapper/base/debug/GetEventScreen.kt | 176 ++++++++++++------ .../keymapper/base/debug/GetEventViewModel.kt | 31 +-- base/src/main/res/values/strings.xml | 2 + 4 files changed, 143 insertions(+), 68 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt index acc0092714..1927e46269 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt @@ -25,9 +25,9 @@ import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementScreen import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementViewModel import io.github.sds100.keymapper.base.constraints.ChooseConstraintScreen import io.github.sds100.keymapper.base.constraints.ChooseConstraintViewModel +import io.github.sds100.keymapper.base.debug.GetEventScreen import io.github.sds100.keymapper.base.expertmode.ExpertModeScreen import io.github.sds100.keymapper.base.expertmode.ExpertModeSetupScreen -import io.github.sds100.keymapper.base.debug.GetEventScreen import io.github.sds100.keymapper.base.logging.LogScreen import io.github.sds100.keymapper.base.onboarding.HandleAccessibilityServiceDialogs import io.github.sds100.keymapper.base.onboarding.SetupAccessibilityServiceDelegateImpl diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt index d904202a6d..bfe767f706 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt @@ -9,8 +9,9 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll @@ -32,12 +33,15 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface +import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource @@ -49,6 +53,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ExpertModeStatus +import kotlinx.coroutines.launch @Composable fun GetEventScreen( @@ -80,7 +85,9 @@ private fun GetEventScreen( onSaveToFileClick: () -> Unit = {}, onSetupExpertModeClick: () -> Unit = {}, ) { - val hasOutput = state.output.isNotEmpty() + val hasOutput = state.recordingOutput.isNotEmpty() + val pagerState = rememberPagerState(pageCount = { 2 }) + val scope = rememberCoroutineScope() Scaffold( modifier = modifier.displayCutoutPadding(), @@ -104,7 +111,12 @@ private fun GetEventScreen( MaterialTheme.colorScheme.onPrimaryContainer } FloatingActionButton( - onClick = onToggleRecordClick, + onClick = { + if (!state.isRecording) { + scope.launch { pagerState.animateScrollToPage(1) } + } + onToggleRecordClick() + }, containerColor = containerColor, contentColor = contentColor, elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(), @@ -180,79 +192,132 @@ private fun GetEventScreen( end = endPadding, ), ) { - Content( - modifier = Modifier.fillMaxSize(), - state = state, - onSetupExpertModeClick = onSetupExpertModeClick, - ) + Column(modifier = Modifier.fillMaxSize()) { + if (state.expertModeStatus != ExpertModeStatus.ENABLED) { + ExpertModeSetupCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + onSetupExpertModeClick = onSetupExpertModeClick, + ) + } else { + PrimaryTabRow( + selectedTabIndex = pagerState.targetPage, + modifier = Modifier.fillMaxWidth(), + contentColor = MaterialTheme.colorScheme.onSurface, + ) { + Tab( + selected = pagerState.targetPage == 0, + onClick = { scope.launch { pagerState.animateScrollToPage(0) } }, + text = { Text(stringResource(R.string.debug_getevent_tab_info)) }, + ) + Tab( + selected = pagerState.targetPage == 1, + onClick = { scope.launch { pagerState.animateScrollToPage(1) } }, + text = { Text(stringResource(R.string.debug_getevent_tab_events)) }, + ) + } + + HorizontalPager( + modifier = Modifier.fillMaxSize(), + state = pagerState, + ) { pageIndex -> + when (pageIndex) { + 0 -> InfoContent( + modifier = Modifier.fillMaxSize(), + state = state, + ) + + 1 -> EventsContent( + modifier = Modifier.fillMaxSize(), + state = state, + ) + } + } + } + } } } } @Composable -private fun Content( +private fun ExpertModeSetupCard( modifier: Modifier = Modifier, - state: GetEventViewModel.State, onSetupExpertModeClick: () -> Unit, ) { - val scrollState = rememberScrollState() - - LaunchedEffect(state.output) { - if (state.output.isNotEmpty()) { - scrollState.animateScrollTo(scrollState.maxValue) + ElevatedCard(modifier = modifier) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(R.string.debug_getevent_expert_mode_required), + style = MaterialTheme.typography.bodyMedium, + ) + OutlinedButton( + onClick = onSetupExpertModeClick, + colors = ButtonDefaults.outlinedButtonColors(), + ) { + Text(stringResource(R.string.action_shell_command_setup_expert_mode)) + } } } +} +@Composable +private fun InfoContent(modifier: Modifier = Modifier, state: GetEventViewModel.State) { Column(modifier = modifier) { - if (state.expertModeStatus != ExpertModeStatus.ENABLED) { - ElevatedCard( + if (state.isLoadingDeviceInfo) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + + if (state.deviceInfoOutput.isNotEmpty()) { + SelectionContainer( modifier = Modifier - .fillMaxWidth() - .padding(16.dp), + .weight(1f) + .horizontalScroll(rememberScrollState()) + .verticalScroll(rememberScrollState()), ) { - Column( + Text( modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource(R.string.debug_getevent_expert_mode_required), - style = MaterialTheme.typography.bodyMedium, - ) - OutlinedButton( - onClick = onSetupExpertModeClick, - colors = ButtonDefaults.outlinedButtonColors(), - ) { - Text(stringResource(R.string.action_shell_command_setup_expert_mode)) - } - } + text = state.deviceInfoOutput, + softWrap = false, + style = MaterialTheme.typography.bodySmall.copy( + fontSize = 10.sp, + fontFamily = FontFamily.Monospace, + ), + lineHeight = 13.sp, + ) } } + } +} - if (state.isLoadingDeviceInfo) { - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) +@Composable +private fun EventsContent(modifier: Modifier = Modifier, state: GetEventViewModel.State) { + val verticalScrollState = rememberScrollState() + + LaunchedEffect(state.recordingOutput) { + if (state.recordingOutput.isNotEmpty()) { + verticalScrollState.animateScrollTo(verticalScrollState.maxValue) } + } + Column(modifier = modifier) { if (state.isRecording) { - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } - if (state.output.isNotEmpty()) { + if (state.recordingOutput.isNotEmpty()) { SelectionContainer( modifier = Modifier + .weight(1f) .horizontalScroll(rememberScrollState()) - .verticalScroll(rememberScrollState()), + .verticalScroll(verticalScrollState), ) { Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = state.output, + modifier = Modifier.padding(16.dp), + text = state.recordingOutput, softWrap = false, style = MaterialTheme.typography.bodySmall.copy( fontSize = 10.sp, @@ -262,18 +327,16 @@ private fun Content( ) } } - - Spacer(modifier = Modifier.height(8.dp)) } } @Preview @Composable -private fun PreviewWithOutput() { +private fun PreviewInfoTab() { KeyMapperTheme { GetEventScreen( state = GetEventViewModel.State( - output = """add device 1: /dev/input/event0 + deviceInfoOutput = """add device 1: /dev/input/event0 bus: 0019 vendor 0001 product 0001 @@ -283,7 +346,7 @@ private fun PreviewWithOutput() { id: "" version: 1.0.1 events: - KEY (0001): KEY_POWER + KEY (0001): KEY_POWER input props: add device 2: /dev/input/event1 @@ -296,8 +359,8 @@ add device 2: /dev/input/event1 id: "" version: 1.0.1 events: - KEY (0001): BTN_TOOL_RUBBER BTN_STYLUS - ABS (0003): ABS_X : value 0, min 0, max 32767, fuzz 0, flat 0, resolution 0"""", + KEY (0001): BTN_TOOL_RUBBER BTN_STYLUS + ABS (0003): ABS_X : value 0, min 0, max 32767, fuzz 0, flat 0, resolution 0""", expertModeStatus = ExpertModeStatus.ENABLED, ), ) @@ -310,7 +373,8 @@ private fun PreviewRecording() { KeyMapperTheme { GetEventScreen( state = GetEventViewModel.State( - output = "add device 1: /dev/input/event0\n name: \"gpio-keys\"", + recordingOutput = """/dev/input/event1: EV_KEY KEY_VOLUMEDOWN DOWN +/dev/input/event1: EV_SYN SYN_REPORT 00""", isRecording = true, expertModeStatus = ExpertModeStatus.ENABLED, ), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt index 42ca58ae17..dc5928ca0c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt @@ -20,11 +20,11 @@ import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.models.ShellExecutionMode import io.github.sds100.keymapper.common.utils.handle +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import io.github.sds100.keymapper.system.clipboard.ClipboardAdapter import io.github.sds100.keymapper.system.files.FileAdapter import io.github.sds100.keymapper.system.files.FileUtils -import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager -import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map @@ -40,10 +40,13 @@ class GetEventViewModel @Inject constructor( private val buildConfigProvider: BuildConfigProvider, @ApplicationContext private val context: Context, resourceProvider: ResourceProvider, -) : ViewModel(), NavigationProvider by navigationProvider, ResourceProvider by resourceProvider { +) : ViewModel(), + NavigationProvider by navigationProvider, + ResourceProvider by resourceProvider { data class State( - val output: String = "", + val deviceInfoOutput: String = "", + val recordingOutput: String = "", val isLoadingDeviceInfo: Boolean = false, val isRecording: Boolean = false, val expertModeStatus: ExpertModeStatus = ExpertModeStatus.DISABLED, @@ -62,7 +65,10 @@ class GetEventViewModel @Inject constructor( }.collect { status -> val wasDisabled = state.expertModeStatus != ExpertModeStatus.ENABLED state = state.copy(expertModeStatus = status) - if (status == ExpertModeStatus.ENABLED && wasDisabled && state.output.isEmpty()) { + if (status == ExpertModeStatus.ENABLED && + wasDisabled && + state.deviceInfoOutput.isEmpty() + ) { loadDeviceInfo() } } @@ -81,7 +87,7 @@ class GetEventViewModel @Inject constructor( onSuccess = { it.stdout }, onError = { "Error: ${it.getFullMessage(this@GetEventViewModel)}" }, ) - state = state.copy(output = output, isLoadingDeviceInfo = false) + state = state.copy(deviceInfoOutput = output, isLoadingDeviceInfo = false) } } @@ -107,7 +113,7 @@ class GetEventViewModel @Inject constructor( ) if (newOutput.isNotEmpty()) { state = state.copy( - output = state.output + "\n--- Recording ---\n" + newOutput, + recordingOutput = state.recordingOutput + newOutput, ) } state = state.copy(isRecording = false) @@ -125,11 +131,11 @@ class GetEventViewModel @Inject constructor( } fun onClearClick() { - state = state.copy(output = "") + state = state.copy(recordingOutput = "") } fun onCopyToClipboardClick() { - clipboardAdapter.copy("getevent output", state.output) + clipboardAdapter.copy("getevent output", state.recordingOutput) } fun onSaveToFileClick() { @@ -137,7 +143,7 @@ class GetEventViewModel @Inject constructor( val fileName = "getevent/key_mapper_getevent_${FileUtils.createFileDate()}.txt" val file = fileAdapter.getPrivateFile(fileName) file.createFile() - file.outputStream()?.bufferedWriter()?.use { it.write(state.output) } + file.outputStream()?.bufferedWriter()?.use { it.write(state.recordingOutput) } val publicUri = fileAdapter.getPublicUriForPrivateFile(file).toUri() ShareUtils.shareFile(context, publicUri, buildConfigProvider.packageName) } @@ -151,7 +157,10 @@ class GetEventViewModel @Inject constructor( fun onSetupExpertModeClick() { viewModelScope.launch { - navigationProvider.navigate("getevent_setup_expert_mode", NavDestination.ExpertModeSetup) + navigationProvider.navigate( + "getevent_setup_expert_mode", + NavDestination.ExpertModeSetup, + ) } } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 3fa56c873e..c8d598751e 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -645,6 +645,8 @@ Loading device info\u2026 Copy to clipboard Save to file + Info + Events Report issue From 0968f4d35f851d02365e8204b8af9515a884d78c Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 5 May 2026 12:43:24 +0200 Subject: [PATCH 5/7] #2081 show refresh button for getevent info --- .../keymapper/base/debug/GetEventScreen.kt | 200 ++++++++++++++---- .../keymapper/base/debug/GetEventViewModel.kt | 22 +- base/src/main/res/values/strings.xml | 1 + 3 files changed, 177 insertions(+), 46 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt index bfe767f706..f448f8eec3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt @@ -1,5 +1,10 @@ package io.github.sds100.keymapper.base.debug +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,7 +23,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.outlined.ContentCopy -import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Share import androidx.compose.material.icons.rounded.FiberManualRecord import androidx.compose.material.icons.rounded.Stop @@ -66,13 +71,29 @@ fun GetEventScreen( state = viewModel.state, onBackClick = onBackClick, onToggleRecordClick = viewModel::onToggleRecordClick, - onClearClick = viewModel::onClearClick, - onCopyToClipboardClick = viewModel::onCopyToClipboardClick, - onSaveToFileClick = viewModel::onSaveToFileClick, + onRefreshDeviceInfoClick = viewModel::onRefreshDeviceInfoClick, + onCopyToClipboardClick = { tab -> viewModel.onCopyToClipboardClick(tab.toOutputTab()) }, + onSaveToFileClick = { tab -> viewModel.onSaveToFileClick(tab.toOutputTab()) }, onSetupExpertModeClick = viewModel::onSetupExpertModeClick, ) } +private enum class GetEventTab { + INFO, + EVENTS, +} + +private enum class RefreshButtonState { + REFRESH_INFO, + START_RECORDING, + STOP, +} + +private fun GetEventTab.toOutputTab(): GetEventViewModel.OutputTab = when (this) { + GetEventTab.INFO -> GetEventViewModel.OutputTab.INFO + GetEventTab.EVENTS -> GetEventViewModel.OutputTab.EVENTS +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun GetEventScreen( @@ -80,14 +101,23 @@ private fun GetEventScreen( state: GetEventViewModel.State, onBackClick: () -> Unit = {}, onToggleRecordClick: () -> Unit = {}, - onClearClick: () -> Unit = {}, - onCopyToClipboardClick: () -> Unit = {}, - onSaveToFileClick: () -> Unit = {}, + onRefreshDeviceInfoClick: () -> Unit = {}, + onCopyToClipboardClick: (GetEventTab) -> Unit = {}, + onSaveToFileClick: (GetEventTab) -> Unit = {}, onSetupExpertModeClick: () -> Unit = {}, ) { - val hasOutput = state.recordingOutput.isNotEmpty() val pagerState = rememberPagerState(pageCount = { 2 }) val scope = rememberCoroutineScope() + val selectedTab = if (pagerState.currentPage == 0) GetEventTab.INFO else GetEventTab.EVENTS + val hasOutputForSelectedTab = when (selectedTab) { + GetEventTab.INFO -> state.deviceInfoOutput.isNotEmpty() + GetEventTab.EVENTS -> state.recordingOutput.isNotEmpty() + } + val refreshButtonState = when { + selectedTab == GetEventTab.INFO -> RefreshButtonState.REFRESH_INFO + state.isRecording -> RefreshButtonState.STOP + else -> RefreshButtonState.START_RECORDING + } Scaffold( modifier = modifier.displayCutoutPadding(), @@ -100,41 +130,67 @@ private fun GetEventScreen( BottomAppBar( floatingActionButton = { if (state.expertModeStatus == ExpertModeStatus.ENABLED) { - val containerColor = if (state.isRecording) { + val containerColor = if (refreshButtonState == RefreshButtonState.STOP) { MaterialTheme.colorScheme.errorContainer } else { MaterialTheme.colorScheme.primaryContainer } - val contentColor = if (state.isRecording) { + val contentColor = if (refreshButtonState == RefreshButtonState.STOP) { MaterialTheme.colorScheme.onErrorContainer } else { MaterialTheme.colorScheme.onPrimaryContainer } FloatingActionButton( onClick = { - if (!state.isRecording) { - scope.launch { pagerState.animateScrollToPage(1) } + if (selectedTab == GetEventTab.INFO) { + onRefreshDeviceInfoClick() + } else { + if (!state.isRecording) { + scope.launch { pagerState.animateScrollToPage(1) } + } + onToggleRecordClick() } - onToggleRecordClick() }, containerColor = containerColor, contentColor = contentColor, elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(), ) { - if (state.isRecording) { - Icon( - imageVector = Icons.Rounded.Stop, - contentDescription = stringResource( - R.string.debug_getevent_stop_recording, - ), - ) - } else { - Icon( - imageVector = Icons.Rounded.FiberManualRecord, - contentDescription = stringResource( - R.string.debug_getevent_start_recording, - ), - ) + AnimatedContent( + targetState = refreshButtonState, + transitionSpec = { + fadeIn(animationSpec = tween(200)) togetherWith + fadeOut(animationSpec = tween(200)) + }, + label = "refresh_button_state", + ) { buttonState -> + when (buttonState) { + RefreshButtonState.REFRESH_INFO -> { + Icon( + imageVector = Icons.Outlined.Refresh, + contentDescription = stringResource( + R.string.debug_getevent_refresh, + ), + ) + } + + RefreshButtonState.START_RECORDING -> { + Icon( + imageVector = Icons.Rounded.FiberManualRecord, + contentDescription = stringResource( + R.string.debug_getevent_start_recording, + ), + ) + } + + RefreshButtonState.STOP -> { + Icon( + imageVector = Icons.Rounded.Stop, + contentDescription = stringResource( + R.string.debug_getevent_stop_recording, + ), + ) + } + } } } } @@ -148,8 +204,8 @@ private fun GetEventScreen( } Spacer(modifier = Modifier.weight(1f)) IconButton( - onClick = onCopyToClipboardClick, - enabled = hasOutput, + onClick = { onCopyToClipboardClick(selectedTab) }, + enabled = hasOutputForSelectedTab, ) { Icon( imageVector = Icons.Outlined.ContentCopy, @@ -157,23 +213,14 @@ private fun GetEventScreen( ) } IconButton( - onClick = onSaveToFileClick, - enabled = hasOutput, + onClick = { onSaveToFileClick(selectedTab) }, + enabled = hasOutputForSelectedTab, ) { Icon( imageVector = Icons.Outlined.Share, contentDescription = stringResource(R.string.debug_getevent_save), ) } - IconButton( - onClick = onClearClick, - enabled = hasOutput, - ) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = stringResource(R.string.debug_getevent_clear), - ) - } }, ) }, @@ -367,6 +414,31 @@ add device 2: /dev/input/event1 } } +@Preview +@Composable +private fun PreviewInfoTabLoading() { + KeyMapperTheme { + GetEventScreen( + state = GetEventViewModel.State( + isLoadingDeviceInfo = true, + expertModeStatus = ExpertModeStatus.ENABLED, + ), + ) + } +} + +@Preview +@Composable +private fun PreviewInfoTabEmptyOutput() { + KeyMapperTheme { + GetEventScreen( + state = GetEventViewModel.State( + expertModeStatus = ExpertModeStatus.ENABLED, + ), + ) + } +} + @Preview @Composable private fun PreviewRecording() { @@ -382,6 +454,54 @@ private fun PreviewRecording() { } } +@Preview +@Composable +private fun PreviewEventsContentEmptyIdle() { + KeyMapperTheme { + GetEventScreen( + state = GetEventViewModel.State( + isRecording = false, + expertModeStatus = ExpertModeStatus.ENABLED, + ), + ) + } +} + +@Preview +@Composable +private fun PreviewEventsContentOutputIdle() { + KeyMapperTheme { + GetEventScreen( + state = GetEventViewModel.State( + recordingOutput = """/dev/input/event2: EV_KEY KEY_VOLUMEUP DOWN +/dev/input/event2: EV_SYN SYN_REPORT 00""", + isRecording = false, + expertModeStatus = ExpertModeStatus.ENABLED, + ), + ) + } +} + +@Preview +@Composable +private fun PreviewInfoContentOutputAndLoading() { + KeyMapperTheme { + GetEventScreen( + state = GetEventViewModel.State( + deviceInfoOutput = """add device 3: /dev/input/event2 + bus: 0019 + vendor 0001 + product 0001 + version 0100 + name: "gpio-keys-2" + location: "gpio-keys/input1\"""", + isLoadingDeviceInfo = true, + expertModeStatus = ExpertModeStatus.ENABLED, + ), + ) + } +} + @Preview @Composable private fun PreviewExpertModeDisabled() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt index dc5928ca0c..a5dc0fe931 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt @@ -44,6 +44,11 @@ class GetEventViewModel @Inject constructor( NavigationProvider by navigationProvider, ResourceProvider by resourceProvider { + enum class OutputTab { + INFO, + EVENTS, + } + data class State( val deviceInfoOutput: String = "", val recordingOutput: String = "", @@ -69,13 +74,13 @@ class GetEventViewModel @Inject constructor( wasDisabled && state.deviceInfoOutput.isEmpty() ) { - loadDeviceInfo() + onRefreshDeviceInfoClick() } } } } - private fun loadDeviceInfo() { + fun onRefreshDeviceInfoClick() { viewModelScope.launch { state = state.copy(isLoadingDeviceInfo = true) val result = executeShellCommandUseCase.execute( @@ -134,21 +139,26 @@ class GetEventViewModel @Inject constructor( state = state.copy(recordingOutput = "") } - fun onCopyToClipboardClick() { - clipboardAdapter.copy("getevent output", state.recordingOutput) + fun onCopyToClipboardClick(tab: OutputTab) { + clipboardAdapter.copy("getevent output", getOutputForTab(tab)) } - fun onSaveToFileClick() { + fun onSaveToFileClick(tab: OutputTab) { viewModelScope.launch(Dispatchers.IO) { val fileName = "getevent/key_mapper_getevent_${FileUtils.createFileDate()}.txt" val file = fileAdapter.getPrivateFile(fileName) file.createFile() - file.outputStream()?.bufferedWriter()?.use { it.write(state.recordingOutput) } + file.outputStream()?.bufferedWriter()?.use { it.write(getOutputForTab(tab)) } val publicUri = fileAdapter.getPublicUriForPrivateFile(file).toUri() ShareUtils.shareFile(context, publicUri, buildConfigProvider.packageName) } } + private fun getOutputForTab(tab: OutputTab): String = when (tab) { + OutputTab.INFO -> state.deviceInfoOutput + OutputTab.EVENTS -> state.recordingOutput + } + fun onBackClick() { viewModelScope.launch { navigationProvider.popBackStack() diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index c8d598751e..99a6abd93d 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -642,6 +642,7 @@ Record Stop Recording Clear output + Refresh Loading device info\u2026 Copy to clipboard Save to file From dc6a3262cb6835dbee0cdbae08f7aa02a2ea6b28 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 5 May 2026 12:51:47 +0200 Subject: [PATCH 6/7] #2081 add getevent files to provider --- base/src/main/res/xml/provider_paths.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/base/src/main/res/xml/provider_paths.xml b/base/src/main/res/xml/provider_paths.xml index 81fcea4ee0..72b9380231 100644 --- a/base/src/main/res/xml/provider_paths.xml +++ b/base/src/main/res/xml/provider_paths.xml @@ -4,6 +4,10 @@ name="backups" path="backups" /> + + From 6532c66498ee75801f5fccb3fc03bc6bb5df2214 Mon Sep 17 00:00:00 2001 From: sds100 Date: Tue, 5 May 2026 13:09:20 +0200 Subject: [PATCH 7/7] #2081 save getevent output and show it even if system bridge disabled --- .../keymapper/base/BaseViewModelHiltModule.kt | 6 + .../base/debug/GetEventOutputUseCase.kt | 110 ++++++++++ .../keymapper/base/debug/GetEventScreen.kt | 204 +++++++++--------- .../keymapper/base/debug/GetEventViewModel.kt | 86 ++------ base/src/main/res/values/strings.xml | 1 + .../io/github/sds100/keymapper/data/Keys.kt | 2 + 6 files changed, 242 insertions(+), 167 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventOutputUseCase.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt index 75ebbc90c7..6c5a9240bf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt @@ -25,6 +25,8 @@ import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCaseImpl import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCase import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCaseImpl import io.github.sds100.keymapper.base.constraints.DisplayConstraintUseCase +import io.github.sds100.keymapper.base.debug.GetEventOutputUseCase +import io.github.sds100.keymapper.base.debug.GetEventOutputUseCaseImpl import io.github.sds100.keymapper.base.expertmode.ExpertModeSetupDelegateImpl import io.github.sds100.keymapper.base.expertmode.SystemBridgeSetupDelegate import io.github.sds100.keymapper.base.expertmode.SystemBridgeSetupUseCase @@ -185,6 +187,10 @@ abstract class BaseViewModelHiltModule { @ViewModelScoped abstract fun bindShareLogcatUseCase(impl: ShareLogcatUseCaseImpl): ShareLogcatUseCase + @Binds + @ViewModelScoped + abstract fun bindGetEventOutputUseCase(impl: GetEventOutputUseCaseImpl): GetEventOutputUseCase + @Binds @ViewModelScoped abstract fun bindOnboardingTipDelegate(impl: OnboardingTipDelegateImpl): OnboardingTipDelegate diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventOutputUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventOutputUseCase.kt new file mode 100644 index 0000000000..86054205a9 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventOutputUseCase.kt @@ -0,0 +1,110 @@ +package io.github.sds100.keymapper.base.debug + +import android.content.Context +import androidx.core.net.toUri +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.base.actions.ExecuteShellCommandUseCase +import io.github.sds100.keymapper.base.utils.ShareUtils +import io.github.sds100.keymapper.base.utils.getFullMessage +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.models.ShellExecutionMode +import io.github.sds100.keymapper.common.utils.handle +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.system.clipboard.ClipboardAdapter +import io.github.sds100.keymapper.system.files.FileAdapter +import io.github.sds100.keymapper.system.files.FileUtils +import io.github.sds100.keymapper.system.files.IFile +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +interface GetEventOutputUseCase { + val deviceInfoOutput: Flow + val eventsOutput: Flow + + suspend fun refreshDeviceInfo() + suspend fun recordEvents() + suspend fun stopRecording() + fun copyOutput(output: String) + suspend fun shareOutput(output: String) +} + +class GetEventOutputUseCaseImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val executeShellCommandUseCase: ExecuteShellCommandUseCase, + private val preferenceRepository: PreferenceRepository, + private val clipboardAdapter: ClipboardAdapter, + private val fileAdapter: FileAdapter, + private val buildConfigProvider: BuildConfigProvider, + private val resourceProvider: ResourceProvider, +) : GetEventOutputUseCase { + + companion object { + private const val MAX_COPY_OUTPUT_LENGTH = 150_000 + } + + override val deviceInfoOutput: Flow = preferenceRepository + .get(Keys.getEventDeviceInfoOutput) + .map { it.orEmpty() } + + override val eventsOutput: Flow = preferenceRepository + .get(Keys.getEventEventsOutput) + .map { it.orEmpty() } + + override suspend fun refreshDeviceInfo() { + val output = executeShellCommandUseCase.execute( + command = "getevent -il", + executionMode = ShellExecutionMode.ADB, + timeoutMillis = 30_000L, + ).handle( + onSuccess = { it.stdout }, + onError = { "Error: ${it.getFullMessage(resourceProvider)}" }, + ) + preferenceRepository.set(Keys.getEventDeviceInfoOutput, output) + } + + override suspend fun recordEvents() { + val output = executeShellCommandUseCase.execute( + command = "getevent -lt", + executionMode = ShellExecutionMode.ADB, + timeoutMillis = 300_000L, + ).handle( + onSuccess = { it.stdout }, + onError = { "" }, + ) + if (output.isNotEmpty()) { + preferenceRepository.set(Keys.getEventEventsOutput, output) + } + } + + override suspend fun stopRecording() { + executeShellCommandUseCase.execute( + command = "pkill -x getevent || true", + executionMode = ShellExecutionMode.ADB, + timeoutMillis = 5_000L, + ) + } + + override fun copyOutput(output: String) { + clipboardAdapter.copy( + "getevent output", + output.takeLast(MAX_COPY_OUTPUT_LENGTH), + ) + } + + override suspend fun shareOutput(output: String) { + withContext(Dispatchers.IO) { + val fileName = "getevent/key_mapper_getevent_${FileUtils.createFileDate()}.txt" + val file: IFile = fileAdapter.getPrivateFile(fileName) + file.createFile() + file.outputStream()?.bufferedWriter()?.use { it.write(output) } + + val publicUri = fileAdapter.getPublicUriForPrivateFile(file).toUri() + ShareUtils.shareFile(context, publicUri, buildConfigProvider.packageName) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt index f448f8eec3..3322defce5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -48,6 +49,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily @@ -108,6 +110,7 @@ private fun GetEventScreen( ) { val pagerState = rememberPagerState(pageCount = { 2 }) val scope = rememberCoroutineScope() + val isExpertModeEnabled = state.expertModeStatus == ExpertModeStatus.ENABLED val selectedTab = if (pagerState.currentPage == 0) GetEventTab.INFO else GetEventTab.EVENTS val hasOutputForSelectedTab = when (selectedTab) { GetEventTab.INFO -> state.deviceInfoOutput.isNotEmpty() @@ -129,67 +132,69 @@ private fun GetEventScreen( bottomBar = { BottomAppBar( floatingActionButton = { - if (state.expertModeStatus == ExpertModeStatus.ENABLED) { - val containerColor = if (refreshButtonState == RefreshButtonState.STOP) { - MaterialTheme.colorScheme.errorContainer - } else { - MaterialTheme.colorScheme.primaryContainer - } - val contentColor = if (refreshButtonState == RefreshButtonState.STOP) { - MaterialTheme.colorScheme.onErrorContainer - } else { - MaterialTheme.colorScheme.onPrimaryContainer - } - FloatingActionButton( - onClick = { - if (selectedTab == GetEventTab.INFO) { - onRefreshDeviceInfoClick() - } else { - if (!state.isRecording) { - scope.launch { pagerState.animateScrollToPage(1) } - } - onToggleRecordClick() + val containerColor = if (refreshButtonState == RefreshButtonState.STOP) { + MaterialTheme.colorScheme.errorContainer + } else { + MaterialTheme.colorScheme.primaryContainer + } + val contentColor = if (refreshButtonState == RefreshButtonState.STOP) { + MaterialTheme.colorScheme.onErrorContainer + } else { + MaterialTheme.colorScheme.onPrimaryContainer + } + FloatingActionButton( + modifier = if (isExpertModeEnabled) Modifier else Modifier.alpha(0.5f), + onClick = { + if (!isExpertModeEnabled) { + return@FloatingActionButton + } + if (selectedTab == GetEventTab.INFO) { + onRefreshDeviceInfoClick() + } else { + if (!state.isRecording) { + scope.launch { pagerState.animateScrollToPage(1) } } + onToggleRecordClick() + } + }, + containerColor = containerColor, + contentColor = contentColor, + elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(), + ) { + AnimatedContent( + targetState = refreshButtonState, + transitionSpec = { + fadeIn(animationSpec = tween(200)) togetherWith + fadeOut(animationSpec = tween(200)) }, - containerColor = containerColor, - contentColor = contentColor, - elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(), - ) { - AnimatedContent( - targetState = refreshButtonState, - transitionSpec = { - fadeIn(animationSpec = tween(200)) togetherWith - fadeOut(animationSpec = tween(200)) - }, - label = "refresh_button_state", - ) { buttonState -> - when (buttonState) { - RefreshButtonState.REFRESH_INFO -> { - Icon( - imageVector = Icons.Outlined.Refresh, - contentDescription = stringResource( - R.string.debug_getevent_refresh, - ), - ) - } + label = "refresh_button_state", + ) { buttonState -> + when (buttonState) { + RefreshButtonState.REFRESH_INFO -> { + Icon( + imageVector = Icons.Outlined.Refresh, + contentDescription = stringResource( + R.string.debug_getevent_refresh, + ), + ) + } - RefreshButtonState.START_RECORDING -> { - Icon( - imageVector = Icons.Rounded.FiberManualRecord, - contentDescription = stringResource( - R.string.debug_getevent_start_recording, - ), - ) - } + RefreshButtonState.START_RECORDING -> { + Icon( + imageVector = Icons.Rounded.FiberManualRecord, + contentDescription = stringResource( + R.string.debug_getevent_start_recording, + ), + ) + } - RefreshButtonState.STOP -> { - Icon( - imageVector = Icons.Rounded.Stop, - contentDescription = stringResource( - R.string.debug_getevent_stop_recording, - ), - ) - } + RefreshButtonState.STOP -> { + Icon( + imageVector = Icons.Rounded.Stop, + contentDescription = stringResource( + R.string.debug_getevent_stop_recording, + ), + ) } } } @@ -247,39 +252,38 @@ private fun GetEventScreen( .padding(16.dp), onSetupExpertModeClick = onSetupExpertModeClick, ) - } else { - PrimaryTabRow( - selectedTabIndex = pagerState.targetPage, - modifier = Modifier.fillMaxWidth(), - contentColor = MaterialTheme.colorScheme.onSurface, - ) { - Tab( - selected = pagerState.targetPage == 0, - onClick = { scope.launch { pagerState.animateScrollToPage(0) } }, - text = { Text(stringResource(R.string.debug_getevent_tab_info)) }, - ) - Tab( - selected = pagerState.targetPage == 1, - onClick = { scope.launch { pagerState.animateScrollToPage(1) } }, - text = { Text(stringResource(R.string.debug_getevent_tab_events)) }, - ) - } + } + PrimaryTabRow( + selectedTabIndex = pagerState.targetPage, + modifier = Modifier.fillMaxWidth(), + contentColor = MaterialTheme.colorScheme.onSurface, + ) { + Tab( + selected = pagerState.targetPage == 0, + onClick = { scope.launch { pagerState.animateScrollToPage(0) } }, + text = { Text(stringResource(R.string.debug_getevent_tab_info)) }, + ) + Tab( + selected = pagerState.targetPage == 1, + onClick = { scope.launch { pagerState.animateScrollToPage(1) } }, + text = { Text(stringResource(R.string.debug_getevent_tab_events)) }, + ) + } - HorizontalPager( - modifier = Modifier.fillMaxSize(), - state = pagerState, - ) { pageIndex -> - when (pageIndex) { - 0 -> InfoContent( - modifier = Modifier.fillMaxSize(), - state = state, - ) + HorizontalPager( + modifier = Modifier.fillMaxSize(), + state = pagerState, + ) { pageIndex -> + when (pageIndex) { + 0 -> InfoContent( + modifier = Modifier.fillMaxSize(), + state = state, + ) - 1 -> EventsContent( - modifier = Modifier.fillMaxSize(), - state = state, - ) - } + 1 -> EventsContent( + modifier = Modifier.fillMaxSize(), + state = state, + ) } } } @@ -315,7 +319,8 @@ private fun ExpertModeSetupCard( private fun InfoContent(modifier: Modifier = Modifier, state: GetEventViewModel.State) { Column(modifier = modifier) { if (state.isLoadingDeviceInfo) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + Spacer(Modifier.height(16.dp)) + LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) } if (state.deviceInfoOutput.isNotEmpty()) { @@ -352,7 +357,13 @@ private fun EventsContent(modifier: Modifier = Modifier, state: GetEventViewMode Column(modifier = modifier) { if (state.isRecording) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + Spacer(Modifier.height(16.dp)) + LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + text = stringResource(R.string.debug_getevent_events_output_after_recording), + style = MaterialTheme.typography.bodySmall, + ) } if (state.recordingOutput.isNotEmpty()) { @@ -454,19 +465,6 @@ private fun PreviewRecording() { } } -@Preview -@Composable -private fun PreviewEventsContentEmptyIdle() { - KeyMapperTheme { - GetEventScreen( - state = GetEventViewModel.State( - isRecording = false, - expertModeStatus = ExpertModeStatus.ENABLED, - ), - ) - } -} - @Preview @Composable private fun PreviewEventsContentOutputIdle() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt index a5dc0fe931..aa4846544b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt @@ -1,48 +1,28 @@ package io.github.sds100.keymapper.base.debug -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import io.github.sds100.keymapper.base.actions.ExecuteShellCommandUseCase import io.github.sds100.keymapper.base.utils.ExpertModeStatus -import io.github.sds100.keymapper.base.utils.ShareUtils -import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.navigation.navigate -import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.BuildConfigProvider -import io.github.sds100.keymapper.common.models.ShellExecutionMode -import io.github.sds100.keymapper.common.utils.handle import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState -import io.github.sds100.keymapper.system.clipboard.ClipboardAdapter -import io.github.sds100.keymapper.system.files.FileAdapter -import io.github.sds100.keymapper.system.files.FileUtils import javax.inject.Inject -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @HiltViewModel class GetEventViewModel @Inject constructor( - private val executeShellCommandUseCase: ExecuteShellCommandUseCase, + private val outputUseCase: GetEventOutputUseCase, private val navigationProvider: NavigationProvider, private val systemBridgeConnectionManager: SystemBridgeConnectionManager, - private val clipboardAdapter: ClipboardAdapter, - private val fileAdapter: FileAdapter, - private val buildConfigProvider: BuildConfigProvider, - @ApplicationContext private val context: Context, - resourceProvider: ResourceProvider, ) : ViewModel(), - NavigationProvider by navigationProvider, - ResourceProvider by resourceProvider { + NavigationProvider by navigationProvider { enum class OutputTab { INFO, @@ -61,6 +41,18 @@ class GetEventViewModel @Inject constructor( private set init { + viewModelScope.launch { + outputUseCase.deviceInfoOutput.collect { output -> + state = state.copy(deviceInfoOutput = output) + } + } + + viewModelScope.launch { + outputUseCase.eventsOutput.collect { output -> + state = state.copy(recordingOutput = output) + } + } + viewModelScope.launch { systemBridgeConnectionManager.connectionState.map { connectionState -> when (connectionState) { @@ -83,16 +75,8 @@ class GetEventViewModel @Inject constructor( fun onRefreshDeviceInfoClick() { viewModelScope.launch { state = state.copy(isLoadingDeviceInfo = true) - val result = executeShellCommandUseCase.execute( - command = "getevent -il", - executionMode = ShellExecutionMode.ADB, - timeoutMillis = 30_000L, - ) - val output = result.handle( - onSuccess = { it.stdout }, - onError = { "Error: ${it.getFullMessage(this@GetEventViewModel)}" }, - ) - state = state.copy(deviceInfoOutput = output, isLoadingDeviceInfo = false) + outputUseCase.refreshDeviceInfo() + state = state.copy(isLoadingDeviceInfo = false) } } @@ -105,52 +89,26 @@ class GetEventViewModel @Inject constructor( } private fun startRecording() { - state = state.copy(isRecording = true) viewModelScope.launch { - val result = executeShellCommandUseCase.execute( - command = "getevent -lt", - executionMode = ShellExecutionMode.ADB, - timeoutMillis = 300_000L, - ) - val newOutput = result.handle( - onSuccess = { it.stdout }, - onError = { "" }, - ) - if (newOutput.isNotEmpty()) { - state = state.copy( - recordingOutput = state.recordingOutput + newOutput, - ) - } + state = state.copy(isRecording = true) + outputUseCase.recordEvents() state = state.copy(isRecording = false) } } private fun stopRecording() { viewModelScope.launch { - executeShellCommandUseCase.execute( - command = "pkill -x getevent || true", - executionMode = ShellExecutionMode.ADB, - timeoutMillis = 5_000L, - ) + outputUseCase.stopRecording() } } - fun onClearClick() { - state = state.copy(recordingOutput = "") - } - fun onCopyToClipboardClick(tab: OutputTab) { - clipboardAdapter.copy("getevent output", getOutputForTab(tab)) + outputUseCase.copyOutput(getOutputForTab(tab)) } fun onSaveToFileClick(tab: OutputTab) { - viewModelScope.launch(Dispatchers.IO) { - val fileName = "getevent/key_mapper_getevent_${FileUtils.createFileDate()}.txt" - val file = fileAdapter.getPrivateFile(fileName) - file.createFile() - file.outputStream()?.bufferedWriter()?.use { it.write(getOutputForTab(tab)) } - val publicUri = fileAdapter.getPublicUriForPrivateFile(file).toUri() - ShareUtils.shareFile(context, publicUri, buildConfigProvider.packageName) + viewModelScope.launch { + outputUseCase.shareOutput(getOutputForTab(tab)) } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 99a6abd93d..9d6bb9862b 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -648,6 +648,7 @@ Save to file Info Events + Output appears after recording stops. Report issue diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 65f9615f5f..59e88eb0a4 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -153,6 +153,8 @@ object Keys { booleanPreferencesKey("key_key_event_actions_use_system_bridge") val shellCommandScriptText = stringPreferencesKey("key_shell_command_script_text") + val getEventDeviceInfoOutput = stringPreferencesKey("key_getevent_device_info_output") + val getEventEventsOutput = stringPreferencesKey("key_getevent_events_output") /** * This is stored as true when PRO Mode has been auto started after updating