Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/version.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION_NAME=4.0.0-beta.1
VERSION_CODE=169
VERSION_CODE=171
VERSION_NUM=01
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import io.github.sds100.keymapper.base.actions.ChooseActionScreen
import io.github.sds100.keymapper.base.actions.ChooseActionViewModel
import io.github.sds100.keymapper.base.actions.ConfigShellCommandViewModel
import io.github.sds100.keymapper.base.actions.ShellCommandActionScreen
import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementScreen
import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementViewModel
import io.github.sds100.keymapper.base.constraints.ChooseConstraintScreen
Expand Down Expand Up @@ -66,6 +68,19 @@ fun BaseMainNavHost(
)
}

composable<NavDestination.ConfigShellCommand> { backStackEntry ->
val viewModel: ConfigShellCommandViewModel = hiltViewModel()

backStackEntry.handleRouteArgs<NavDestination.ConfigShellCommand> { destination ->
destination.actionJson?.let { viewModel.loadAction(Json.decodeFromString(it)) }
}

ShellCommandActionScreen(
modifier = Modifier.fillMaxSize(),
viewModel = viewModel
)
}

composable<NavDestination.ChooseConstraint> {
val viewModel: ChooseConstraintViewModel = hiltViewModel()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.sds100.keymapper.base.actions

import io.github.sds100.keymapper.common.models.ShellExecutionMode
import io.github.sds100.keymapper.common.utils.NodeInteractionType
import io.github.sds100.keymapper.common.utils.Orientation
import io.github.sds100.keymapper.common.utils.PinchScreenType
Expand Down Expand Up @@ -932,6 +933,21 @@ sealed class ActionData : Comparable<ActionData> {
}
}

@Serializable
data class ShellCommand(
val description: String,
val command: String,
val executionMode: ShellExecutionMode,
val timeoutMillis: Int = 10000, // milliseconds (default 10 seconds)
) : ActionData() {
override val id: ActionId = ActionId.SHELL_COMMAND

override fun toString(): String {
// Do not leak sensitive command info to logs.
return "ShellCommand(description=$description, executionMode=$executionMode, timeoutMs=$timeoutMillis)"
}
}

@Serializable
data class InteractUiElement(
val description: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.github.sds100.keymapper.base.actions

import android.util.Base64
import androidx.core.net.toUri
import io.github.sds100.keymapper.common.models.ShellExecutionMode
import io.github.sds100.keymapper.common.utils.KMError
import io.github.sds100.keymapper.common.utils.KMResult
import io.github.sds100.keymapper.common.utils.NodeInteractionType
Expand Down Expand Up @@ -47,6 +49,7 @@ object ActionDataEntityMapper {
}

ActionEntity.Type.INTERACT_UI_ELEMENT -> ActionId.INTERACT_UI_ELEMENT
ActionEntity.Type.SHELL_COMMAND -> ActionId.SHELL_COMMAND
}

return when (actionId) {
Expand Down Expand Up @@ -652,6 +655,38 @@ object ActionDataEntityMapper {
ActionData.MoveCursor(moveType = type, direction = direction)
}

ActionId.SHELL_COMMAND -> {
val useRoot = entity.flags.hasFlag(ActionEntity.ACTION_FLAG_SHELL_COMMAND_USE_ROOT)
val useAdb = entity.flags.hasFlag(ActionEntity.ACTION_FLAG_SHELL_COMMAND_USE_ADB)

val executionMode = when {
useAdb -> ShellExecutionMode.ADB
useRoot -> ShellExecutionMode.ROOT
else -> ShellExecutionMode.STANDARD
}

val description =
entity.extras.getData(ActionEntity.EXTRA_SHELL_COMMAND_DESCRIPTION)
.valueOrNull() ?: return null

val timeoutMs = entity.extras.getData(ActionEntity.EXTRA_SHELL_COMMAND_TIMEOUT)
.valueOrNull()?.toIntOrNull() ?: 10000

// Decode Base64 command
val command = try {
String(Base64.decode(entity.data, Base64.DEFAULT))
} catch (e: Exception) {
return null
}

ActionData.ShellCommand(
description = description,
command = command,
executionMode = executionMode,
timeoutMillis = timeoutMs,
)
}

ActionId.FORCE_STOP_APP -> ActionData.ForceStopApp
ActionId.CLEAR_RECENT_APP -> ActionData.ClearRecentApp
}
Expand Down Expand Up @@ -679,6 +714,7 @@ object ActionDataEntityMapper {
is ActionData.Url -> ActionEntity.Type.URL
is ActionData.Sound -> ActionEntity.Type.SOUND
is ActionData.InteractUiElement -> ActionEntity.Type.INTERACT_UI_ELEMENT
is ActionData.ShellCommand -> ActionEntity.Type.SHELL_COMMAND
else -> ActionEntity.Type.SYSTEM_ACTION
}

Expand All @@ -691,6 +727,8 @@ object ActionDataEntityMapper {
}

private fun getFlags(data: ActionData): Int {
var flags = 0

val showVolumeUiFlag = when (data) {
is ActionData.Volume.Stream -> data.showVolumeUi
is ActionData.Volume.Up -> data.showVolumeUi
Expand All @@ -702,10 +740,26 @@ object ActionDataEntityMapper {
}

if (showVolumeUiFlag) {
return ActionEntity.ACTION_FLAG_SHOW_VOLUME_UI
} else {
return 0
flags = flags or ActionEntity.ACTION_FLAG_SHOW_VOLUME_UI
}

if (data is ActionData.ShellCommand) {
when (data.executionMode) {
ShellExecutionMode.ROOT -> {
flags = flags or ActionEntity.ACTION_FLAG_SHELL_COMMAND_USE_ROOT
}

ShellExecutionMode.ADB -> {
flags = flags or ActionEntity.ACTION_FLAG_SHELL_COMMAND_USE_ADB
}

ShellExecutionMode.STANDARD -> {
// No flag needed for standard mode
}
}
}

return flags
}

private fun getDataString(data: ActionData): String = when (data) {
Expand All @@ -727,6 +781,11 @@ object ActionDataEntityMapper {
}

is ActionData.InteractUiElement -> data.description
is ActionData.ShellCommand -> Base64.encodeToString(
data.command.toByteArray(),
Base64.DEFAULT
).trim() // Trim to remove trailing newline added by Base64.DEFAULT
is ActionData.HttpRequest -> SYSTEM_ACTION_ID_MAP[data.id]!!
is ActionData.ControlMediaForApp.Rewind -> SYSTEM_ACTION_ID_MAP[data.id]!!
is ActionData.ControlMediaForApp.Stop -> SYSTEM_ACTION_ID_MAP[data.id]!!
is ActionData.ControlMedia.Rewind -> SYSTEM_ACTION_ID_MAP[data.id]!!
Expand Down Expand Up @@ -986,6 +1045,11 @@ object ActionDataEntityMapper {
add(EntityExtra(ActionEntity.EXTRA_MOVE_CURSOR_DIRECTION, directionString))
}

is ActionData.ShellCommand -> listOf(
EntityExtra(ActionEntity.EXTRA_SHELL_COMMAND_DESCRIPTION, data.description),
EntityExtra(ActionEntity.EXTRA_SHELL_COMMAND_TIMEOUT, data.timeoutMillis.toString()),
)

else -> emptyList()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.sds100.keymapper.base.actions.sound.SoundsManager
import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper
import io.github.sds100.keymapper.base.system.inputmethod.SwitchImeInterface
import io.github.sds100.keymapper.common.BuildConfigProvider
import io.github.sds100.keymapper.common.models.ShellExecutionMode
import io.github.sds100.keymapper.common.utils.Constants
import io.github.sds100.keymapper.common.utils.KMError
import io.github.sds100.keymapper.common.utils.firstBlocking
Expand Down Expand Up @@ -202,6 +203,28 @@ class LazyActionErrorSnapshot(
return it
}

is ActionData.ShellCommand -> {
return when (action.executionMode) {
ShellExecutionMode.ROOT -> {
if (!isPermissionGranted(Permission.ROOT)) {
SystemError.PermissionDenied(Permission.ROOT)
} else {
null
}
}

ShellExecutionMode.ADB -> {
if (!isSystemBridgeConnected) {
SystemBridgeError.Disconnected
} else {
null
}
}

ShellExecutionMode.STANDARD -> null
}
}

else -> {}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum class ActionId {
PINCH_SCREEN,
URL,
HTTP_REQUEST,
SHELL_COMMAND,
INTENT,
PHONE_CALL,
INTERACT_UI_ELEMENT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import io.github.sds100.keymapper.base.utils.ui.IconInfo
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
import io.github.sds100.keymapper.base.utils.ui.TintType
import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo
import io.github.sds100.keymapper.common.models.ShellExecutionMode
import io.github.sds100.keymapper.common.utils.InputDeviceUtils
import io.github.sds100.keymapper.common.utils.Orientation
import io.github.sds100.keymapper.common.utils.PinchScreenType
Expand Down Expand Up @@ -572,6 +573,23 @@ class ActionUiHelper(
ActionData.DeviceControls -> getString(R.string.action_device_controls)
is ActionData.HttpRequest -> action.description

is ActionData.ShellCommand -> when (action.executionMode) {
ShellExecutionMode.ROOT -> getString(
R.string.action_shell_command_description_with_root,
action.description
)

ShellExecutionMode.ADB -> getString(
R.string.action_shell_command_description_with_adb,
action.description
)

ShellExecutionMode.STANDARD -> getString(
R.string.action_shell_command_description_with_standard,
action.description
)
}

is ActionData.InteractUiElement -> action.description

ActionData.ClearRecentApp -> getString(R.string.action_clear_recent_app)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.Message
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.automirrored.outlined.ShortText
import androidx.compose.material.icons.automirrored.outlined.Undo
Expand Down Expand Up @@ -40,7 +41,6 @@ import androidx.compose.material.icons.outlined.Keyboard
import androidx.compose.material.icons.outlined.KeyboardHide
import androidx.compose.material.icons.outlined.Link
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Message
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Nfc
import androidx.compose.material.icons.outlined.NotStarted
Expand Down Expand Up @@ -72,6 +72,7 @@ import androidx.compose.material.icons.rounded.BluetoothDisabled
import androidx.compose.material.icons.rounded.ContentCopy
import androidx.compose.material.icons.rounded.ContentCut
import androidx.compose.material.icons.rounded.ContentPaste
import androidx.compose.material.icons.rounded.Terminal
import androidx.compose.material.icons.rounded.Wifi
import androidx.compose.material.icons.rounded.WifiOff
import androidx.compose.ui.graphics.vector.ImageVector
Expand Down Expand Up @@ -126,6 +127,7 @@ object ActionUtils {
ActionId.INTENT -> ActionCategory.APPS
ActionId.URL -> ActionCategory.APPS
ActionId.HTTP_REQUEST -> ActionCategory.APPS
ActionId.SHELL_COMMAND -> ActionCategory.APPS

ActionId.TOGGLE_WIFI -> ActionCategory.CONNECTIVITY
ActionId.ENABLE_WIFI -> ActionCategory.CONNECTIVITY
Expand Down Expand Up @@ -368,6 +370,7 @@ object ActionUtils {
ActionId.COMPOSE_SMS -> R.string.action_compose_sms
ActionId.DEVICE_CONTROLS -> R.string.action_device_controls
ActionId.HTTP_REQUEST -> R.string.action_http_request
ActionId.SHELL_COMMAND -> R.string.action_shell_command
ActionId.INTERACT_UI_ELEMENT -> R.string.action_interact_ui_element_title
ActionId.FORCE_STOP_APP -> R.string.action_force_stop_app
ActionId.CLEAR_RECENT_APP -> R.string.action_clear_recent_app
Expand Down Expand Up @@ -867,15 +870,16 @@ object ActionUtils {
ActionId.URL -> Icons.Outlined.Link
ActionId.INTENT -> Icons.Outlined.DataObject
ActionId.PHONE_CALL -> Icons.Outlined.Call
ActionId.SEND_SMS -> Icons.Outlined.Message
ActionId.COMPOSE_SMS -> Icons.Outlined.Message
ActionId.SEND_SMS -> Icons.AutoMirrored.Outlined.Message
ActionId.COMPOSE_SMS -> Icons.AutoMirrored.Outlined.Message
ActionId.SOUND -> Icons.AutoMirrored.Outlined.VolumeUp
ActionId.DISMISS_MOST_RECENT_NOTIFICATION -> Icons.Outlined.ClearAll
ActionId.DISMISS_ALL_NOTIFICATIONS -> Icons.Outlined.ClearAll
ActionId.ANSWER_PHONE_CALL -> Icons.Outlined.Call
ActionId.END_PHONE_CALL -> Icons.Outlined.CallEnd
ActionId.DEVICE_CONTROLS -> KeyMapperIcons.HomeIotDevice
ActionId.HTTP_REQUEST -> Icons.Outlined.Http
ActionId.SHELL_COMMAND -> Icons.Rounded.Terminal
ActionId.INTERACT_UI_ELEMENT -> KeyMapperIcons.JumpToElement
ActionId.FORCE_STOP_APP -> Icons.Outlined.Dangerous
ActionId.CLEAR_RECENT_APP -> Icons.Outlined.VerticalSplit
Expand Down Expand Up @@ -924,6 +928,7 @@ fun ActionData.isEditable(): Boolean = when (this) {
is ActionData.SendSms,
is ActionData.ComposeSms,
is ActionData.HttpRequest,
is ActionData.ShellCommand,
is ActionData.InteractUiElement,
is ActionData.MoveCursor,
-> true
Expand Down
Loading
Loading