Skip to content

Commit 7646b3e

Browse files
committed
#1915 ask user to remove "adb shell" from Shell command
1 parent 1e305d8 commit 7646b3e

5 files changed

Lines changed: 343 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## [4.0.0 Beta 4](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.04)
2+
3+
#### TO BE RELEASED
4+
5+
## Added
6+
- #1915 ask user to remove "adb shell" from Shell command
7+
18
## [4.0.0 Beta 3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.03)
29

310
#### 25 November 2025

base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigShellCommandViewModel.kt

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package io.github.sds100.keymapper.base.actions
22

33
import android.os.Build
4-
import android.util.Base64
54
import androidx.compose.runtime.getValue
65
import androidx.compose.runtime.mutableStateOf
76
import androidx.compose.runtime.setValue
87
import androidx.lifecycle.ViewModel
98
import androidx.lifecycle.viewModelScope
109
import dagger.hilt.android.lifecycle.HiltViewModel
10+
import io.github.sds100.keymapper.base.R
1111
import io.github.sds100.keymapper.base.utils.ProModeStatus
1212
import io.github.sds100.keymapper.base.utils.navigation.NavDestination
1313
import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
1414
import io.github.sds100.keymapper.base.utils.navigation.navigate
15+
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
1516
import io.github.sds100.keymapper.common.models.ShellExecutionMode
1617
import io.github.sds100.keymapper.common.models.isExecuting
1718
import io.github.sds100.keymapper.common.utils.Constants
@@ -20,6 +21,7 @@ import io.github.sds100.keymapper.data.Keys
2021
import io.github.sds100.keymapper.data.repositories.PreferenceRepository
2122
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
2223
import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
24+
import java.util.Base64
2325
import javax.inject.Inject
2426
import kotlinx.coroutines.Job
2527
import kotlinx.coroutines.flow.map
@@ -32,7 +34,9 @@ class ConfigShellCommandViewModel @Inject constructor(
3234
private val navigationProvider: NavigationProvider,
3335
private val systemBridgeConnectionManager: SystemBridgeConnectionManager,
3436
private val preferenceRepository: PreferenceRepository,
35-
) : ViewModel() {
37+
resourceProvider: ResourceProvider,
38+
) : ViewModel(),
39+
ResourceProvider by resourceProvider {
3640

3741
var state: ShellCommandActionState by mutableStateOf(ShellCommandActionState())
3842
private set
@@ -68,11 +72,11 @@ class ConfigShellCommandViewModel @Inject constructor(
6872
}
6973

7074
fun onDescriptionChanged(newDescription: String) {
71-
state = state.copy(description = newDescription)
75+
state = state.copy(description = newDescription, descriptionError = null)
7276
}
7377

7478
fun onCommandChanged(newCommand: String) {
75-
state = state.copy(command = newCommand)
79+
state = state.copy(command = newCommand, commandError = null)
7680
saveScriptText(newCommand)
7781
}
7882

@@ -84,9 +88,15 @@ class ConfigShellCommandViewModel @Inject constructor(
8488
state = state.copy(timeoutSeconds = newTimeoutSeconds)
8589
}
8690

87-
fun onTestClick() {
91+
fun onTestClick(): Boolean {
8892
testJob?.cancel()
8993

94+
val commandError = validateCommand(state.command)
95+
if (commandError != null) {
96+
state = state.copy(commandError = commandError)
97+
return false
98+
}
99+
90100
state = state.copy(
91101
isRunning = true,
92102
testResult = null,
@@ -95,6 +105,8 @@ class ConfigShellCommandViewModel @Inject constructor(
95105
testJob = viewModelScope.launch {
96106
testCommand()
97107
}
108+
109+
return true
98110
}
99111

100112
private suspend fun testCommand() {
@@ -119,7 +131,21 @@ class ConfigShellCommandViewModel @Inject constructor(
119131
)
120132
}
121133

122-
fun onDoneClick() {
134+
fun onDoneClick(): Boolean {
135+
val commandError = validateCommand(state.command)
136+
if (commandError != null) {
137+
state = state.copy(commandError = commandError)
138+
return false
139+
}
140+
141+
if (state.description.isBlank()) {
142+
state = state.copy(
143+
descriptionError = getString(R.string.error_cant_be_empty),
144+
)
145+
146+
return false
147+
}
148+
123149
val action = ActionData.ShellCommand(
124150
description = state.description,
125151
command = state.command,
@@ -133,6 +159,8 @@ class ConfigShellCommandViewModel @Inject constructor(
133159
viewModelScope.launch {
134160
navigationProvider.popBackStackWithResult(Json.encodeToString(action))
135161
}
162+
163+
return true
136164
}
137165

138166
fun onCancelClick() {
@@ -150,9 +178,24 @@ class ConfigShellCommandViewModel @Inject constructor(
150178
}
151179
}
152180

181+
/**
182+
* @return the error message.
183+
*/
184+
private fun validateCommand(command: String): String? {
185+
if (state.command.isBlank()) {
186+
return getString(R.string.action_shell_command_command_empty_error)
187+
}
188+
189+
if (state.command.trimStart().startsWith("adb shell")) {
190+
return getString(R.string.action_shell_command_adb_shell_error)
191+
}
192+
193+
return null
194+
}
195+
153196
private fun saveScriptText(scriptText: String) {
154197
viewModelScope.launch {
155-
val encodedText = Base64.encodeToString(scriptText.toByteArray(), Base64.DEFAULT).trim()
198+
val encodedText = Base64.getEncoder().encodeToString(scriptText.toByteArray()).trim()
156199
preferenceRepository.set(Keys.shellCommandScriptText, encodedText)
157200
}
158201
}
@@ -162,7 +205,7 @@ class ConfigShellCommandViewModel @Inject constructor(
162205
preferenceRepository.get(Keys.shellCommandScriptText).collect { savedScriptText ->
163206
if (savedScriptText != null && state.command.isEmpty()) {
164207
try {
165-
val decodedText = String(Base64.decode(savedScriptText, Base64.DEFAULT))
208+
val decodedText = String(Base64.getDecoder().decode(savedScriptText))
166209
state = state.copy(command = decodedText)
167210
} catch (e: Exception) {
168211
// If decoding fails, ignore the saved text

base/src/main/java/io/github/sds100/keymapper/base/actions/ShellCommandActionScreen.kt

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,7 @@ import androidx.compose.material3.Tab
3838
import androidx.compose.material3.Text
3939
import androidx.compose.material3.TopAppBar
4040
import androidx.compose.runtime.Composable
41-
import androidx.compose.runtime.getValue
42-
import androidx.compose.runtime.mutableStateOf
4341
import androidx.compose.runtime.rememberCoroutineScope
44-
import androidx.compose.runtime.saveable.rememberSaveable
45-
import androidx.compose.runtime.setValue
4642
import androidx.compose.ui.Alignment
4743
import androidx.compose.ui.Modifier
4844
import androidx.compose.ui.platform.LocalContext
@@ -71,7 +67,9 @@ import kotlinx.coroutines.launch
7167

7268
data class ShellCommandActionState(
7369
val description: String = "",
70+
val descriptionError: String? = null,
7471
val command: String = "",
72+
val commandError: String? = null,
7573
val executionMode: ShellExecutionMode = ShellExecutionMode.STANDARD,
7674
/**
7775
* UI works with seconds for user-friendliness
@@ -111,20 +109,21 @@ private fun ShellCommandActionScreen(
111109
onCommandChanged: (String) -> Unit = {},
112110
onExecutionModeChanged: (ShellExecutionMode) -> Unit = {},
113111
onTimeoutChanged: (Int) -> Unit = {},
114-
onTestClick: () -> Unit = {},
112+
/**
113+
* Returns whether validation passed
114+
*/
115+
onTestClick: () -> Boolean = { true },
115116
onKillClick: () -> Unit = {},
116-
onDoneClick: () -> Unit = {},
117+
/**
118+
* Returns whether validation passed
119+
*/
120+
onDoneClick: () -> Boolean = { true },
117121
onCancelClick: () -> Unit = {},
118122
onSetupProModeClick: () -> Unit = {},
119123
) {
120124
val scrollState = rememberScrollState()
121125
val scope = rememberCoroutineScope()
122126

123-
var descriptionError: String? by rememberSaveable { mutableStateOf(null) }
124-
var commandError: String? by rememberSaveable { mutableStateOf(null) }
125-
val descriptionEmptyErrorString = stringResource(R.string.error_cant_be_empty)
126-
val commandEmptyErrorString = stringResource(R.string.action_shell_command_command_empty_error)
127-
128127
Scaffold(
129128
modifier = modifier,
130129
topBar = {
@@ -137,24 +136,11 @@ private fun ShellCommandActionScreen(
137136
floatingActionButton = {
138137
ExtendedFloatingActionButton(
139138
onClick = {
140-
var hasError = false
141-
142-
if (state.description.isBlank()) {
143-
descriptionError = descriptionEmptyErrorString
144-
hasError = true
145-
}
146-
147-
if (state.command.isBlank()) {
148-
commandError = commandEmptyErrorString
149-
hasError = true
150-
}
151-
152-
if (hasError) {
139+
// Go to the configuration tab if validation failed
140+
if (!onDoneClick()) {
153141
scope.launch {
154142
scrollState.animateScrollTo(0)
155143
}
156-
} else {
157-
onDoneClick()
158144
}
159145
},
160146
text = { Text(stringResource(R.string.pos_done)) },
@@ -226,23 +212,14 @@ private fun ShellCommandActionScreen(
226212
0 -> ShellCommandConfigurationContent(
227213
modifier = Modifier.fillMaxSize(),
228214
state = state,
229-
descriptionError = descriptionError,
230-
commandError = commandError,
231-
onDescriptionChanged = {
232-
descriptionError = null
233-
onDescriptionChanged(it)
234-
},
235-
onCommandChanged = {
236-
commandError = null
237-
onCommandChanged(it)
238-
},
215+
descriptionError = state.descriptionError,
216+
commandError = state.commandError,
217+
onDescriptionChanged = onDescriptionChanged,
218+
onCommandChanged = onCommandChanged,
239219
onExecutionModeChanged = onExecutionModeChanged,
240220
onTimeoutChanged = onTimeoutChanged,
241221
onTestClick = {
242-
if (state.command.isBlank()) {
243-
commandError = commandEmptyErrorString
244-
} else {
245-
onTestClick()
222+
if (onTestClick()) {
246223
scope.launch {
247224
pagerState.animateScrollToPage(1) // Switch to output tab
248225
}

base/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,7 @@
11371137
<string name="action_shell_command_title">Shell command action</string>
11381138
<string name="action_shell_command_command_label">Script</string>
11391139
<string name="action_shell_command_command_empty_error">Script cannot be empty!</string>
1140+
<string name="action_shell_command_adb_shell_error">Do not put \'adb shell\'!</string>
11401141
<string name="action_shell_command_use_root_label">Run as root</string>
11411142
<string name="action_shell_command_execution_mode_standard">Standard</string>
11421143
<string name="action_shell_command_execution_mode_root">Root</string>

0 commit comments

Comments
 (0)