Skip to content

Commit 06298c8

Browse files
committed
fix various bugs with recording getevent while recording
1 parent ef110d4 commit 06298c8

4 files changed

Lines changed: 104 additions & 69 deletions

File tree

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +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.GetEventOutputUseCase
18-
import io.github.sds100.keymapper.base.debug.GetEventOutputUseCaseImpl
17+
import io.github.sds100.keymapper.base.debug.GetEventRecorder
18+
import io.github.sds100.keymapper.base.debug.GetEventRecorderImpl
1919
import io.github.sds100.keymapper.base.input.InputEventHub
2020
import io.github.sds100.keymapper.base.input.InputEventHubImpl
2121
import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState
@@ -137,7 +137,7 @@ abstract class BaseSingletonHiltModule {
137137

138138
@Binds
139139
@Singleton
140-
abstract fun bindGetEventOutputUseCase(impl: GetEventOutputUseCaseImpl): GetEventOutputUseCase
140+
abstract fun bindGetEventOutputUseCase(impl: GetEventRecorderImpl): GetEventRecorder
141141

142142
@Binds
143143
@Singleton

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/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+
)

base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package io.github.sds100.keymapper.base.trigger
22

33
import android.view.KeyEvent
4-
import io.github.sds100.keymapper.base.debug.GetEventOutputUseCase
4+
import io.github.sds100.keymapper.base.debug.GetEventRecorder
55
import io.github.sds100.keymapper.base.detection.DpadMotionEventTracker
66
import io.github.sds100.keymapper.base.input.InputEventDetectionSource
77
import io.github.sds100.keymapper.base.input.InputEventHub
@@ -23,7 +23,6 @@ import javax.inject.Singleton
2323
import kotlinx.coroutines.CoroutineScope
2424
import kotlinx.coroutines.Dispatchers
2525
import kotlinx.coroutines.Job
26-
import kotlinx.coroutines.NonCancellable
2726
import kotlinx.coroutines.delay
2827
import kotlinx.coroutines.flow.Flow
2928
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -32,15 +31,14 @@ import kotlinx.coroutines.flow.StateFlow
3231
import kotlinx.coroutines.flow.update
3332
import kotlinx.coroutines.launch
3433
import kotlinx.coroutines.runBlocking
35-
import kotlinx.coroutines.withContext
3634
import timber.log.Timber
3735

3836
@Singleton
3937
class RecordTriggerControllerImpl @Inject constructor(
4038
private val coroutineScope: CoroutineScope,
4139
private val inputEventHub: InputEventHub,
4240
private val accessibilityServiceAdapter: AccessibilityServiceAdapter,
43-
private val getEventOutputUseCase: GetEventOutputUseCase,
41+
private val getEventRecorder: GetEventRecorder,
4442
private val systemBridgeConnectionManager: SystemBridgeConnectionManager,
4543
) : RecordTriggerController,
4644
InputEventHubCallback {
@@ -122,9 +120,11 @@ class RecordTriggerControllerImpl @Inject constructor(
122120
} else if (event.isUpEvent) {
123121
onRecordKey(createEvdevRecordedKey(event))
124122
Timber.d(
125-
"Recorded evdev event ${event.code} ${KeyEvent.keyCodeToString(
126-
event.androidCode,
127-
)}",
123+
"Recorded evdev event ${event.code} ${
124+
KeyEvent.keyCodeToString(
125+
event.androidCode,
126+
)
127+
}",
128128
)
129129
}
130130

@@ -200,6 +200,7 @@ class RecordTriggerControllerImpl @Inject constructor(
200200
recordingTriggerJob?.cancel()
201201
recordingTriggerJob = null
202202

203+
getEventRecorder.stopRecording()
203204
dpadMotionEventTracker.reset()
204205
inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID)
205206
state.update { RecordTriggerState.Completed(recordedKeys) }
@@ -264,17 +265,11 @@ class RecordTriggerControllerImpl @Inject constructor(
264265

265266
// Capture getevent output in parallel for the bug report and getevent debug screen.
266267
// ADB shell is only available when expert mode (system bridge) is connected.
267-
// Launch on the outer coroutineScope so the capture lifecycle is independent of this
268-
// job's cancellation state and we can drain its output even when the user stops early.
269-
val geteventCaptureJob: Job? = if (systemBridgeConnectionManager.isConnected()) {
270-
coroutineScope.launch(Dispatchers.IO) {
271-
runCatching { getEventOutputUseCase.refreshDeviceInfo() }
272-
.onFailure { Timber.w(it, "Failed to refresh getevent device info") }
273-
runCatching { getEventOutputUseCase.recordEvents() }
274-
.onFailure { Timber.w(it, "Failed to record getevent events") }
275-
}
276-
} else {
277-
null
268+
if (systemBridgeConnectionManager.isConnected()) {
269+
runCatching { getEventRecorder.refreshDeviceInfo() }
270+
.onFailure { Timber.w(it, "Failed to refresh getevent device info") }
271+
272+
getEventRecorder.recordEvents()
278273
}
279274

280275
try {
@@ -286,21 +281,14 @@ class RecordTriggerControllerImpl @Inject constructor(
286281
delay(1000)
287282
}
288283
} finally {
284+
// Stop the getevent shell process so the parallel capture job exits and
285+
// its output is persisted to the existing preference keys.
286+
getEventRecorder.stopRecording()
287+
289288
downKeyEvents.clear()
290289
dpadMotionEventTracker.reset()
291290
inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID)
292291
state.update { RecordTriggerState.Completed(recordedKeys) }
293-
294-
if (geteventCaptureJob != null) {
295-
// Stop the getevent shell process so the parallel capture job exits and
296-
// its output is persisted to the existing preference keys. Run on
297-
// NonCancellable so we still kill getevent when this job is cancelled.
298-
withContext(NonCancellable) {
299-
runCatching { getEventOutputUseCase.stopRecording() }
300-
.onFailure { Timber.w(it, "Failed to stop parallel getevent capture") }
301-
geteventCaptureJob.join()
302-
}
303-
}
304292
}
305293
}
306294
}

0 commit comments

Comments
 (0)