Skip to content

Commit 6532c66

Browse files
committed
#2081 save getevent output and show it even if system bridge disabled
1 parent dc6a326 commit 6532c66

6 files changed

Lines changed: 242 additions & 167 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ 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
2830
import io.github.sds100.keymapper.base.expertmode.ExpertModeSetupDelegateImpl
2931
import io.github.sds100.keymapper.base.expertmode.SystemBridgeSetupDelegate
3032
import io.github.sds100.keymapper.base.expertmode.SystemBridgeSetupUseCase
@@ -185,6 +187,10 @@ abstract class BaseViewModelHiltModule {
185187
@ViewModelScoped
186188
abstract fun bindShareLogcatUseCase(impl: ShareLogcatUseCaseImpl): ShareLogcatUseCase
187189

190+
@Binds
191+
@ViewModelScoped
192+
abstract fun bindGetEventOutputUseCase(impl: GetEventOutputUseCaseImpl): GetEventOutputUseCase
193+
188194
@Binds
189195
@ViewModelScoped
190196
abstract fun bindOnboardingTipDelegate(impl: OnboardingTipDelegateImpl): OnboardingTipDelegate
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package io.github.sds100.keymapper.base.debug
2+
3+
import android.content.Context
4+
import androidx.core.net.toUri
5+
import dagger.hilt.android.qualifiers.ApplicationContext
6+
import io.github.sds100.keymapper.base.actions.ExecuteShellCommandUseCase
7+
import io.github.sds100.keymapper.base.utils.ShareUtils
8+
import io.github.sds100.keymapper.base.utils.getFullMessage
9+
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
10+
import io.github.sds100.keymapper.common.BuildConfigProvider
11+
import io.github.sds100.keymapper.common.models.ShellExecutionMode
12+
import io.github.sds100.keymapper.common.utils.handle
13+
import io.github.sds100.keymapper.data.Keys
14+
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
15+
import io.github.sds100.keymapper.system.clipboard.ClipboardAdapter
16+
import io.github.sds100.keymapper.system.files.FileAdapter
17+
import io.github.sds100.keymapper.system.files.FileUtils
18+
import io.github.sds100.keymapper.system.files.IFile
19+
import javax.inject.Inject
20+
import kotlinx.coroutines.Dispatchers
21+
import kotlinx.coroutines.flow.Flow
22+
import kotlinx.coroutines.flow.map
23+
import kotlinx.coroutines.withContext
24+
25+
interface GetEventOutputUseCase {
26+
val deviceInfoOutput: Flow<String>
27+
val eventsOutput: Flow<String>
28+
29+
suspend fun refreshDeviceInfo()
30+
suspend fun recordEvents()
31+
suspend fun stopRecording()
32+
fun copyOutput(output: String)
33+
suspend fun shareOutput(output: String)
34+
}
35+
36+
class GetEventOutputUseCaseImpl @Inject constructor(
37+
@ApplicationContext private val context: Context,
38+
private val executeShellCommandUseCase: ExecuteShellCommandUseCase,
39+
private val preferenceRepository: PreferenceRepository,
40+
private val clipboardAdapter: ClipboardAdapter,
41+
private val fileAdapter: FileAdapter,
42+
private val buildConfigProvider: BuildConfigProvider,
43+
private val resourceProvider: ResourceProvider,
44+
) : GetEventOutputUseCase {
45+
46+
companion object {
47+
private const val MAX_COPY_OUTPUT_LENGTH = 150_000
48+
}
49+
50+
override val deviceInfoOutput: Flow<String> = preferenceRepository
51+
.get(Keys.getEventDeviceInfoOutput)
52+
.map { it.orEmpty() }
53+
54+
override val eventsOutput: Flow<String> = preferenceRepository
55+
.get(Keys.getEventEventsOutput)
56+
.map { it.orEmpty() }
57+
58+
override suspend fun refreshDeviceInfo() {
59+
val output = executeShellCommandUseCase.execute(
60+
command = "getevent -il",
61+
executionMode = ShellExecutionMode.ADB,
62+
timeoutMillis = 30_000L,
63+
).handle(
64+
onSuccess = { it.stdout },
65+
onError = { "Error: ${it.getFullMessage(resourceProvider)}" },
66+
)
67+
preferenceRepository.set(Keys.getEventDeviceInfoOutput, output)
68+
}
69+
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)
81+
}
82+
}
83+
84+
override suspend fun stopRecording() {
85+
executeShellCommandUseCase.execute(
86+
command = "pkill -x getevent || true",
87+
executionMode = ShellExecutionMode.ADB,
88+
timeoutMillis = 5_000L,
89+
)
90+
}
91+
92+
override fun copyOutput(output: String) {
93+
clipboardAdapter.copy(
94+
"getevent output",
95+
output.takeLast(MAX_COPY_OUTPUT_LENGTH),
96+
)
97+
}
98+
99+
override suspend fun shareOutput(output: String) {
100+
withContext(Dispatchers.IO) {
101+
val fileName = "getevent/key_mapper_getevent_${FileUtils.createFileDate()}.txt"
102+
val file: IFile = fileAdapter.getPrivateFile(fileName)
103+
file.createFile()
104+
file.outputStream()?.bufferedWriter()?.use { it.write(output) }
105+
106+
val publicUri = fileAdapter.getPublicUriForPrivateFile(file).toUri()
107+
ShareUtils.shareFile(context, publicUri, buildConfigProvider.packageName)
108+
}
109+
}
110+
}

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

Lines changed: 101 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.calculateStartPadding
1414
import androidx.compose.foundation.layout.displayCutoutPadding
1515
import androidx.compose.foundation.layout.fillMaxSize
1616
import androidx.compose.foundation.layout.fillMaxWidth
17+
import androidx.compose.foundation.layout.height
1718
import androidx.compose.foundation.layout.padding
1819
import androidx.compose.foundation.pager.HorizontalPager
1920
import androidx.compose.foundation.pager.rememberPagerState
@@ -48,6 +49,7 @@ import androidx.compose.runtime.Composable
4849
import androidx.compose.runtime.LaunchedEffect
4950
import androidx.compose.runtime.rememberCoroutineScope
5051
import androidx.compose.ui.Modifier
52+
import androidx.compose.ui.draw.alpha
5153
import androidx.compose.ui.platform.LocalLayoutDirection
5254
import androidx.compose.ui.res.stringResource
5355
import androidx.compose.ui.text.font.FontFamily
@@ -108,6 +110,7 @@ private fun GetEventScreen(
108110
) {
109111
val pagerState = rememberPagerState(pageCount = { 2 })
110112
val scope = rememberCoroutineScope()
113+
val isExpertModeEnabled = state.expertModeStatus == ExpertModeStatus.ENABLED
111114
val selectedTab = if (pagerState.currentPage == 0) GetEventTab.INFO else GetEventTab.EVENTS
112115
val hasOutputForSelectedTab = when (selectedTab) {
113116
GetEventTab.INFO -> state.deviceInfoOutput.isNotEmpty()
@@ -129,67 +132,69 @@ private fun GetEventScreen(
129132
bottomBar = {
130133
BottomAppBar(
131134
floatingActionButton = {
132-
if (state.expertModeStatus == ExpertModeStatus.ENABLED) {
133-
val containerColor = if (refreshButtonState == RefreshButtonState.STOP) {
134-
MaterialTheme.colorScheme.errorContainer
135-
} else {
136-
MaterialTheme.colorScheme.primaryContainer
137-
}
138-
val contentColor = if (refreshButtonState == RefreshButtonState.STOP) {
139-
MaterialTheme.colorScheme.onErrorContainer
140-
} else {
141-
MaterialTheme.colorScheme.onPrimaryContainer
142-
}
143-
FloatingActionButton(
144-
onClick = {
145-
if (selectedTab == GetEventTab.INFO) {
146-
onRefreshDeviceInfoClick()
147-
} else {
148-
if (!state.isRecording) {
149-
scope.launch { pagerState.animateScrollToPage(1) }
150-
}
151-
onToggleRecordClick()
135+
val containerColor = if (refreshButtonState == RefreshButtonState.STOP) {
136+
MaterialTheme.colorScheme.errorContainer
137+
} else {
138+
MaterialTheme.colorScheme.primaryContainer
139+
}
140+
val contentColor = if (refreshButtonState == RefreshButtonState.STOP) {
141+
MaterialTheme.colorScheme.onErrorContainer
142+
} else {
143+
MaterialTheme.colorScheme.onPrimaryContainer
144+
}
145+
FloatingActionButton(
146+
modifier = if (isExpertModeEnabled) Modifier else Modifier.alpha(0.5f),
147+
onClick = {
148+
if (!isExpertModeEnabled) {
149+
return@FloatingActionButton
150+
}
151+
if (selectedTab == GetEventTab.INFO) {
152+
onRefreshDeviceInfoClick()
153+
} else {
154+
if (!state.isRecording) {
155+
scope.launch { pagerState.animateScrollToPage(1) }
152156
}
157+
onToggleRecordClick()
158+
}
159+
},
160+
containerColor = containerColor,
161+
contentColor = contentColor,
162+
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
163+
) {
164+
AnimatedContent(
165+
targetState = refreshButtonState,
166+
transitionSpec = {
167+
fadeIn(animationSpec = tween(200)) togetherWith
168+
fadeOut(animationSpec = tween(200))
153169
},
154-
containerColor = containerColor,
155-
contentColor = contentColor,
156-
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation(),
157-
) {
158-
AnimatedContent(
159-
targetState = refreshButtonState,
160-
transitionSpec = {
161-
fadeIn(animationSpec = tween(200)) togetherWith
162-
fadeOut(animationSpec = tween(200))
163-
},
164-
label = "refresh_button_state",
165-
) { buttonState ->
166-
when (buttonState) {
167-
RefreshButtonState.REFRESH_INFO -> {
168-
Icon(
169-
imageVector = Icons.Outlined.Refresh,
170-
contentDescription = stringResource(
171-
R.string.debug_getevent_refresh,
172-
),
173-
)
174-
}
170+
label = "refresh_button_state",
171+
) { buttonState ->
172+
when (buttonState) {
173+
RefreshButtonState.REFRESH_INFO -> {
174+
Icon(
175+
imageVector = Icons.Outlined.Refresh,
176+
contentDescription = stringResource(
177+
R.string.debug_getevent_refresh,
178+
),
179+
)
180+
}
175181

176-
RefreshButtonState.START_RECORDING -> {
177-
Icon(
178-
imageVector = Icons.Rounded.FiberManualRecord,
179-
contentDescription = stringResource(
180-
R.string.debug_getevent_start_recording,
181-
),
182-
)
183-
}
182+
RefreshButtonState.START_RECORDING -> {
183+
Icon(
184+
imageVector = Icons.Rounded.FiberManualRecord,
185+
contentDescription = stringResource(
186+
R.string.debug_getevent_start_recording,
187+
),
188+
)
189+
}
184190

185-
RefreshButtonState.STOP -> {
186-
Icon(
187-
imageVector = Icons.Rounded.Stop,
188-
contentDescription = stringResource(
189-
R.string.debug_getevent_stop_recording,
190-
),
191-
)
192-
}
191+
RefreshButtonState.STOP -> {
192+
Icon(
193+
imageVector = Icons.Rounded.Stop,
194+
contentDescription = stringResource(
195+
R.string.debug_getevent_stop_recording,
196+
),
197+
)
193198
}
194199
}
195200
}
@@ -247,39 +252,38 @@ private fun GetEventScreen(
247252
.padding(16.dp),
248253
onSetupExpertModeClick = onSetupExpertModeClick,
249254
)
250-
} else {
251-
PrimaryTabRow(
252-
selectedTabIndex = pagerState.targetPage,
253-
modifier = Modifier.fillMaxWidth(),
254-
contentColor = MaterialTheme.colorScheme.onSurface,
255-
) {
256-
Tab(
257-
selected = pagerState.targetPage == 0,
258-
onClick = { scope.launch { pagerState.animateScrollToPage(0) } },
259-
text = { Text(stringResource(R.string.debug_getevent_tab_info)) },
260-
)
261-
Tab(
262-
selected = pagerState.targetPage == 1,
263-
onClick = { scope.launch { pagerState.animateScrollToPage(1) } },
264-
text = { Text(stringResource(R.string.debug_getevent_tab_events)) },
265-
)
266-
}
255+
}
256+
PrimaryTabRow(
257+
selectedTabIndex = pagerState.targetPage,
258+
modifier = Modifier.fillMaxWidth(),
259+
contentColor = MaterialTheme.colorScheme.onSurface,
260+
) {
261+
Tab(
262+
selected = pagerState.targetPage == 0,
263+
onClick = { scope.launch { pagerState.animateScrollToPage(0) } },
264+
text = { Text(stringResource(R.string.debug_getevent_tab_info)) },
265+
)
266+
Tab(
267+
selected = pagerState.targetPage == 1,
268+
onClick = { scope.launch { pagerState.animateScrollToPage(1) } },
269+
text = { Text(stringResource(R.string.debug_getevent_tab_events)) },
270+
)
271+
}
267272

268-
HorizontalPager(
269-
modifier = Modifier.fillMaxSize(),
270-
state = pagerState,
271-
) { pageIndex ->
272-
when (pageIndex) {
273-
0 -> InfoContent(
274-
modifier = Modifier.fillMaxSize(),
275-
state = state,
276-
)
273+
HorizontalPager(
274+
modifier = Modifier.fillMaxSize(),
275+
state = pagerState,
276+
) { pageIndex ->
277+
when (pageIndex) {
278+
0 -> InfoContent(
279+
modifier = Modifier.fillMaxSize(),
280+
state = state,
281+
)
277282

278-
1 -> EventsContent(
279-
modifier = Modifier.fillMaxSize(),
280-
state = state,
281-
)
282-
}
283+
1 -> EventsContent(
284+
modifier = Modifier.fillMaxSize(),
285+
state = state,
286+
)
283287
}
284288
}
285289
}
@@ -315,7 +319,8 @@ private fun ExpertModeSetupCard(
315319
private fun InfoContent(modifier: Modifier = Modifier, state: GetEventViewModel.State) {
316320
Column(modifier = modifier) {
317321
if (state.isLoadingDeviceInfo) {
318-
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
322+
Spacer(Modifier.height(16.dp))
323+
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp))
319324
}
320325

321326
if (state.deviceInfoOutput.isNotEmpty()) {
@@ -352,7 +357,13 @@ private fun EventsContent(modifier: Modifier = Modifier, state: GetEventViewMode
352357

353358
Column(modifier = modifier) {
354359
if (state.isRecording) {
355-
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
360+
Spacer(Modifier.height(16.dp))
361+
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp))
362+
Text(
363+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
364+
text = stringResource(R.string.debug_getevent_events_output_after_recording),
365+
style = MaterialTheme.typography.bodySmall,
366+
)
356367
}
357368

358369
if (state.recordingOutput.isNotEmpty()) {
@@ -454,19 +465,6 @@ private fun PreviewRecording() {
454465
}
455466
}
456467

457-
@Preview
458-
@Composable
459-
private fun PreviewEventsContentEmptyIdle() {
460-
KeyMapperTheme {
461-
GetEventScreen(
462-
state = GetEventViewModel.State(
463-
isRecording = false,
464-
expertModeStatus = ExpertModeStatus.ENABLED,
465-
),
466-
)
467-
}
468-
}
469-
470468
@Preview
471469
@Composable
472470
private fun PreviewEventsContentOutputIdle() {

0 commit comments

Comments
 (0)