Skip to content

Commit 4a01b9c

Browse files
authored
Merge pull request #2138 from keymapperorg/feature/2137-getevent-capture
#2137 feat: capture getevent output during trigger recording
2 parents a03d68c + 06298c8 commit 4a01b9c

8 files changed

Lines changed: 155 additions & 72 deletions

File tree

app/version.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
VERSION_NAME=4.1.0
2-
VERSION_CODE=248
2+
VERSION_CODE=250

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import io.github.sds100.keymapper.base.backup.BackupManager
1414
import io.github.sds100.keymapper.base.backup.BackupManagerImpl
1515
import io.github.sds100.keymapper.base.constraints.GetConstraintErrorUseCase
1616
import io.github.sds100.keymapper.base.constraints.GetConstraintErrorUseCaseImpl
17+
import io.github.sds100.keymapper.base.debug.GetEventRecorder
18+
import io.github.sds100.keymapper.base.debug.GetEventRecorderImpl
1719
import io.github.sds100.keymapper.base.input.InputEventHub
1820
import io.github.sds100.keymapper.base.input.InputEventHubImpl
1921
import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState
@@ -133,6 +135,10 @@ abstract class BaseSingletonHiltModule {
133135
impl: RecordTriggerControllerImpl,
134136
): RecordTriggerController
135137

138+
@Binds
139+
@Singleton
140+
abstract fun bindGetEventOutputUseCase(impl: GetEventRecorderImpl): GetEventRecorder
141+
136142
@Binds
137143
@Singleton
138144
abstract fun bindFingerprintGesturesSupportedUseCase(

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCaseImpl
2525
import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCase
2626
import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCaseImpl
2727
import io.github.sds100.keymapper.base.constraints.DisplayConstraintUseCase
28-
import io.github.sds100.keymapper.base.debug.GetEventOutputUseCase
29-
import io.github.sds100.keymapper.base.debug.GetEventOutputUseCaseImpl
3028
import io.github.sds100.keymapper.base.expertmode.ExpertModeSetupDelegateImpl
3129
import io.github.sds100.keymapper.base.expertmode.SystemBridgeSetupDelegate
3230
import io.github.sds100.keymapper.base.expertmode.SystemBridgeSetupUseCase
@@ -187,10 +185,6 @@ abstract class BaseViewModelHiltModule {
187185
@ViewModelScoped
188186
abstract fun bindShareLogcatUseCase(impl: ShareLogcatUseCaseImpl): ShareLogcatUseCase
189187

190-
@Binds
191-
@ViewModelScoped
192-
abstract fun bindGetEventOutputUseCase(impl: GetEventOutputUseCaseImpl): GetEventOutputUseCase
193-
194188
@Binds
195189
@ViewModelScoped
196190
abstract fun bindOnboardingTipDelegate(impl: OnboardingTipDelegateImpl): OnboardingTipDelegate

base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventOutputUseCase.kt renamed to base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventRecorder.kt

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,36 +17,49 @@ import io.github.sds100.keymapper.system.files.FileAdapter
1717
import io.github.sds100.keymapper.system.files.FileUtils
1818
import io.github.sds100.keymapper.system.files.IFile
1919
import javax.inject.Inject
20+
import javax.inject.Singleton
21+
import kotlinx.coroutines.CoroutineScope
2022
import kotlinx.coroutines.Dispatchers
23+
import kotlinx.coroutines.Job
2124
import kotlinx.coroutines.flow.Flow
25+
import kotlinx.coroutines.flow.MutableStateFlow
2226
import kotlinx.coroutines.flow.map
27+
import kotlinx.coroutines.flow.update
28+
import kotlinx.coroutines.launch
2329
import kotlinx.coroutines.withContext
2430

25-
interface GetEventOutputUseCase {
31+
interface GetEventRecorder {
32+
val isRecording: Flow<Boolean>
2633
val deviceInfoOutput: Flow<String>
2734
val eventsOutput: Flow<String>
2835

2936
suspend fun refreshDeviceInfo()
30-
suspend fun recordEvents()
31-
suspend fun stopRecording()
37+
fun recordEvents()
38+
fun stopRecording()
3239
fun copyOutput(output: String)
3340
suspend fun shareOutput(output: String)
3441
}
3542

36-
class GetEventOutputUseCaseImpl @Inject constructor(
43+
@Singleton
44+
class GetEventRecorderImpl @Inject constructor(
3745
@ApplicationContext private val context: Context,
46+
private val coroutineScope: CoroutineScope,
3847
private val executeShellCommandUseCase: ExecuteShellCommandUseCase,
3948
private val preferenceRepository: PreferenceRepository,
4049
private val clipboardAdapter: ClipboardAdapter,
4150
private val fileAdapter: FileAdapter,
4251
private val buildConfigProvider: BuildConfigProvider,
4352
private val resourceProvider: ResourceProvider,
44-
) : GetEventOutputUseCase {
53+
) : GetEventRecorder {
4554

4655
companion object {
4756
private const val MAX_COPY_OUTPUT_LENGTH = 150_000
4857
}
4958

59+
private val recordingJobState: MutableStateFlow<Job?> = MutableStateFlow(null)
60+
61+
override val isRecording: Flow<Boolean> = recordingJobState.map { it != null && it.isActive }
62+
5063
override val deviceInfoOutput: Flow<String> = preferenceRepository
5164
.get(Keys.getEventDeviceInfoOutput)
5265
.map { it.orEmpty() }
@@ -59,34 +72,49 @@ class GetEventOutputUseCaseImpl @Inject constructor(
5972
val output = executeShellCommandUseCase.execute(
6073
command = "getevent -il",
6174
executionMode = ShellExecutionMode.ADB,
62-
timeoutMillis = 30_000L,
75+
timeoutMillis = 5_000L,
6376
).handle(
6477
onSuccess = { it.stdout },
6578
onError = { "Error: ${it.getFullMessage(resourceProvider)}" },
6679
)
6780
preferenceRepository.set(Keys.getEventDeviceInfoOutput, output)
6881
}
6982

70-
override suspend fun recordEvents() {
71-
val output = executeShellCommandUseCase.execute(
72-
command = "getevent -lt",
73-
executionMode = ShellExecutionMode.ADB,
74-
timeoutMillis = 300_000L,
75-
).handle(
76-
onSuccess = { it.stdout },
77-
onError = { "" },
78-
)
79-
if (output.isNotEmpty()) {
80-
preferenceRepository.set(Keys.getEventEventsOutput, output)
83+
override fun recordEvents() {
84+
recordingJobState.update { oldJob ->
85+
oldJob?.cancel()
86+
87+
coroutineScope.launch {
88+
val output = executeShellCommandUseCase.execute(
89+
command = "getevent -lt",
90+
executionMode = ShellExecutionMode.ADB,
91+
timeoutMillis = 60_000L,
92+
).handle(
93+
onSuccess = { it.stdout },
94+
onError = { "" },
95+
)
96+
97+
if (output.isNotEmpty()) {
98+
preferenceRepository.set(Keys.getEventEventsOutput, output)
99+
}
100+
}
81101
}
82102
}
83103

84-
override suspend fun stopRecording() {
85-
executeShellCommandUseCase.execute(
86-
command = "pkill -x getevent || true",
87-
executionMode = ShellExecutionMode.ADB,
88-
timeoutMillis = 5_000L,
89-
)
104+
override fun stopRecording() {
105+
coroutineScope.launch {
106+
executeShellCommandUseCase.execute(
107+
command = "pkill -x getevent || true",
108+
executionMode = ShellExecutionMode.ADB,
109+
timeoutMillis = 5_000L,
110+
)
111+
112+
recordingJobState.update { oldJob ->
113+
oldJob?.join()
114+
oldJob?.cancel()
115+
null
116+
}
117+
}
90118
}
91119

92120
override fun copyOutput(output: String) {

base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventScreen.kt

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private enum class RefreshButtonState {
9595
@Composable
9696
private fun GetEventScreen(
9797
modifier: Modifier = Modifier,
98-
state: GetEventViewModel.State,
98+
state: GetEventState,
9999
onBackClick: () -> Unit = {},
100100
onToggleRecordClick: () -> Unit = {},
101101
onRefreshDeviceInfoClick: () -> Unit = {},
@@ -309,11 +309,15 @@ private fun ExpertModeSetupCard(
309309
}
310310

311311
@Composable
312-
private fun InfoContent(modifier: Modifier = Modifier, state: GetEventViewModel.State) {
312+
private fun InfoContent(modifier: Modifier = Modifier, state: GetEventState) {
313313
Column(modifier = modifier) {
314314
if (state.isLoadingDeviceInfo) {
315315
Spacer(Modifier.height(16.dp))
316-
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp))
316+
LinearProgressIndicator(
317+
modifier = Modifier
318+
.fillMaxWidth()
319+
.padding(horizontal = 16.dp),
320+
)
317321
}
318322

319323
if (state.deviceInfoOutput.isNotEmpty()) {
@@ -339,7 +343,7 @@ private fun InfoContent(modifier: Modifier = Modifier, state: GetEventViewModel.
339343
}
340344

341345
@Composable
342-
private fun EventsContent(modifier: Modifier = Modifier, state: GetEventViewModel.State) {
346+
private fun EventsContent(modifier: Modifier = Modifier, state: GetEventState) {
343347
val verticalScrollState = rememberScrollState()
344348

345349
LaunchedEffect(state.recordingOutput) {
@@ -351,15 +355,23 @@ private fun EventsContent(modifier: Modifier = Modifier, state: GetEventViewMode
351355
Column(modifier = modifier) {
352356
if (state.isRecording) {
353357
Spacer(Modifier.height(16.dp))
354-
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp))
358+
LinearProgressIndicator(
359+
modifier = Modifier
360+
.fillMaxWidth()
361+
.padding(horizontal = 16.dp),
362+
)
355363
Text(
356364
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
357365
text = stringResource(R.string.debug_getevent_events_output_after_recording),
358366
style = MaterialTheme.typography.bodySmall,
359367
)
360-
}
361-
362-
if (!state.isRecording && state.recordingOutput.isNotEmpty()) {
368+
} else if (state.recordingOutput.isEmpty()) {
369+
Text(
370+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
371+
text = stringResource(R.string.debug_getevent_events_empty),
372+
style = MaterialTheme.typography.bodySmall,
373+
)
374+
} else {
363375
SelectionContainer(
364376
modifier = Modifier
365377
.weight(1f)
@@ -386,7 +398,7 @@ private fun EventsContent(modifier: Modifier = Modifier, state: GetEventViewMode
386398
private fun PreviewInfoTab() {
387399
KeyMapperTheme {
388400
GetEventScreen(
389-
state = GetEventViewModel.State(
401+
state = GetEventState(
390402
deviceInfoOutput = """add device 1: /dev/input/event0
391403
bus: 0019
392404
vendor 0001
@@ -423,7 +435,7 @@ add device 2: /dev/input/event1
423435
private fun PreviewInfoTabLoading() {
424436
KeyMapperTheme {
425437
GetEventScreen(
426-
state = GetEventViewModel.State(
438+
state = GetEventState(
427439
isLoadingDeviceInfo = true,
428440
expertModeStatus = ExpertModeStatus.ENABLED,
429441
),
@@ -436,7 +448,7 @@ private fun PreviewInfoTabLoading() {
436448
private fun PreviewInfoTabEmptyOutput() {
437449
KeyMapperTheme {
438450
GetEventScreen(
439-
state = GetEventViewModel.State(
451+
state = GetEventState(
440452
expertModeStatus = ExpertModeStatus.ENABLED,
441453
),
442454
)
@@ -448,7 +460,7 @@ private fun PreviewInfoTabEmptyOutput() {
448460
private fun PreviewRecording() {
449461
KeyMapperTheme {
450462
GetEventScreen(
451-
state = GetEventViewModel.State(
463+
state = GetEventState(
452464
recordingOutput = """/dev/input/event1: EV_KEY KEY_VOLUMEDOWN DOWN
453465
/dev/input/event1: EV_SYN SYN_REPORT 00""",
454466
isRecording = true,
@@ -463,7 +475,7 @@ private fun PreviewRecording() {
463475
private fun PreviewEventsContentOutputIdle() {
464476
KeyMapperTheme {
465477
GetEventScreen(
466-
state = GetEventViewModel.State(
478+
state = GetEventState(
467479
recordingOutput = """/dev/input/event2: EV_KEY KEY_VOLUMEUP DOWN
468480
/dev/input/event2: EV_SYN SYN_REPORT 00""",
469481
isRecording = false,
@@ -478,7 +490,7 @@ private fun PreviewEventsContentOutputIdle() {
478490
private fun PreviewInfoContentOutputAndLoading() {
479491
KeyMapperTheme {
480492
GetEventScreen(
481-
state = GetEventViewModel.State(
493+
state = GetEventState(
482494
deviceInfoOutput = """add device 3: /dev/input/event2
483495
bus: 0019
484496
vendor 0001
@@ -498,7 +510,7 @@ private fun PreviewInfoContentOutputAndLoading() {
498510
private fun PreviewExpertModeDisabled() {
499511
KeyMapperTheme {
500512
GetEventScreen(
501-
state = GetEventViewModel.State(
513+
state = GetEventState(
502514
expertModeStatus = ExpertModeStatus.DISABLED,
503515
),
504516
)

base/src/main/java/io/github/sds100/keymapper/base/debug/GetEventViewModel.kt

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import androidx.compose.runtime.setValue
66
import androidx.lifecycle.ViewModel
77
import androidx.lifecycle.viewModelScope
88
import dagger.hilt.android.lifecycle.HiltViewModel
9+
import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase
910
import io.github.sds100.keymapper.base.utils.ExpertModeStatus
1011
import io.github.sds100.keymapper.base.utils.navigation.NavDestination
1112
import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
1213
import io.github.sds100.keymapper.base.utils.navigation.navigate
14+
import io.github.sds100.keymapper.common.utils.firstBlocking
1315
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
1416
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
1517
import javax.inject.Inject
@@ -18,21 +20,16 @@ import kotlinx.coroutines.launch
1820

1921
@HiltViewModel
2022
class GetEventViewModel @Inject constructor(
21-
private val outputUseCase: GetEventOutputUseCase,
23+
private val outputUseCase: GetEventRecorder,
2224
private val navigationProvider: NavigationProvider,
2325
private val systemBridgeConnectionManager: SystemBridgeConnectionManager,
26+
private val pauseKeyMapsUseCase: PauseKeyMapsUseCase,
2427
) : ViewModel(),
2528
NavigationProvider by navigationProvider {
2629

27-
data class State(
28-
val deviceInfoOutput: String = "",
29-
val recordingOutput: String = "",
30-
val isLoadingDeviceInfo: Boolean = false,
31-
val isRecording: Boolean = false,
32-
val expertModeStatus: ExpertModeStatus = ExpertModeStatus.DISABLED,
33-
)
30+
private var resumeKeyMapsOnStop = false
3431

35-
var state: State by mutableStateOf(State())
32+
var state: GetEventState by mutableStateOf(GetEventState())
3633
private set
3734

3835
init {
@@ -48,6 +45,17 @@ class GetEventViewModel @Inject constructor(
4845
}
4946
}
5047

48+
viewModelScope.launch {
49+
outputUseCase.isRecording.collect { isRecording ->
50+
if (!isRecording && resumeKeyMapsOnStop) {
51+
pauseKeyMapsUseCase.resume()
52+
resumeKeyMapsOnStop = false
53+
}
54+
55+
state = state.copy(isRecording = isRecording)
56+
}
57+
}
58+
5159
viewModelScope.launch {
5260
systemBridgeConnectionManager.connectionState.map { connectionState ->
5361
when (connectionState) {
@@ -84,11 +92,14 @@ class GetEventViewModel @Inject constructor(
8492
}
8593

8694
private fun startRecording() {
87-
viewModelScope.launch {
88-
state = state.copy(isRecording = true)
89-
outputUseCase.recordEvents()
90-
state = state.copy(isRecording = false)
91-
}
95+
// Only unpause key maps when recording stops if the user hadn't previously paused it
96+
// themselves.
97+
resumeKeyMapsOnStop = !pauseKeyMapsUseCase.isPaused.firstBlocking()
98+
99+
// Key maps should be paused while recording because any events from grabbed devices
100+
// will not appear in the getevent log.
101+
pauseKeyMapsUseCase.pause()
102+
outputUseCase.recordEvents()
92103
}
93104

94105
private fun stopRecording() {
@@ -135,3 +146,11 @@ class GetEventViewModel @Inject constructor(
135146
}
136147
}
137148
}
149+
150+
data class GetEventState(
151+
val deviceInfoOutput: String = "",
152+
val recordingOutput: String = "",
153+
val isLoadingDeviceInfo: Boolean = false,
154+
val isRecording: Boolean = false,
155+
val expertModeStatus: ExpertModeStatus = ExpertModeStatus.DISABLED,
156+
)

0 commit comments

Comments
 (0)