11package io.github.sds100.keymapper.base.actions
22
33import android.os.Build
4- import android.util.Base64
54import androidx.compose.runtime.getValue
65import androidx.compose.runtime.mutableStateOf
76import androidx.compose.runtime.setValue
87import androidx.lifecycle.ViewModel
98import androidx.lifecycle.viewModelScope
109import dagger.hilt.android.lifecycle.HiltViewModel
10+ import io.github.sds100.keymapper.base.R
1111import io.github.sds100.keymapper.base.utils.ProModeStatus
1212import io.github.sds100.keymapper.base.utils.navigation.NavDestination
1313import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
1414import io.github.sds100.keymapper.base.utils.navigation.navigate
15+ import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
1516import io.github.sds100.keymapper.common.models.ShellExecutionMode
1617import io.github.sds100.keymapper.common.models.isExecuting
1718import io.github.sds100.keymapper.common.utils.Constants
@@ -20,6 +21,7 @@ import io.github.sds100.keymapper.data.Keys
2021import io.github.sds100.keymapper.data.repositories.PreferenceRepository
2122import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager
2223import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState
24+ import java.util.Base64
2325import javax.inject.Inject
2426import kotlinx.coroutines.Job
2527import 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
0 commit comments