Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

## Added

- #262 Add "TalkBack gesture" action to simulate TalkBack navigation gestures (swipes, multi-finger taps, and multi-directional swipes).

## [4.1.1](https://github.com/sds100/KeyMapper/releases/tag/v4.1.1)

#### 15 May 2026
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,4 +1043,14 @@ sealed class ActionData : Comparable<ActionData> {
else -> super.compareTo(other)
}
}

@Serializable
data class TalkBackGesture(val gesture: TalkBackGestureType) : ActionData() {
override val id: ActionId = ActionId.TALKBACK_GESTURE

override fun compareTo(other: ActionData) = when (other) {
is TalkBackGesture -> gesture.compareTo(other.gesture)
else -> super.compareTo(other)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.base.actions

import android.util.Base64
import androidx.core.net.toUri
import io.github.sds100.keymapper.base.actions.TalkBackGestureType
import io.github.sds100.keymapper.common.models.ShellExecutionMode
import io.github.sds100.keymapper.common.utils.KMError
import io.github.sds100.keymapper.common.utils.KMResult
Expand Down Expand Up @@ -874,6 +875,20 @@ object ActionDataEntityMapper {

ActionId.CLEAR_RECENT_APP -> ActionData.ClearRecentApp

ActionId.TALKBACK_GESTURE -> {
val gestureTypeString =
entity.extras.getData(ActionEntity.EXTRA_TALKBACK_GESTURE_TYPE)
.valueOrNull() ?: return null

val gestureType = try {
TalkBackGestureType.valueOf(gestureTypeString)
} catch (_: IllegalArgumentException) {
return null
}

ActionData.TalkBackGesture(gesture = gestureType)
}

ActionId.MODIFY_SETTING -> {
val value = entity.extras.getData(ActionEntity.EXTRA_SETTING_VALUE)
.valueOrNull() ?: return null
Expand Down Expand Up @@ -1324,6 +1339,10 @@ object ActionDataEntityMapper {
EntityExtra(ActionEntity.EXTRA_TOAST_DURATION, data.duration.name),
)

is ActionData.TalkBackGesture -> listOf(
EntityExtra(ActionEntity.EXTRA_TALKBACK_GESTURE_TYPE, data.gesture.name),
)

else -> emptyList()
}

Expand Down Expand Up @@ -1510,5 +1529,7 @@ object ActionDataEntityMapper {
ActionId.CLEAR_RECENT_APP to "clear_recent_app",

ActionId.MODIFY_SETTING to "modify_setting",

ActionId.TALKBACK_GESTURE to "talkback_gesture",
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ class LazyActionErrorSnapshot(
}
}

is ActionData.TalkBackGesture -> {
return getAppError(TALKBACK_PACKAGE_NAME)
}

else -> {}
}

Expand Down Expand Up @@ -317,3 +321,5 @@ interface ActionErrorSnapshot {
fun getError(action: ActionData): KMError?
fun getErrors(actions: List<ActionData>): Map<ActionData, KMError?>
}

private const val TALKBACK_PACKAGE_NAME = "com.google.android.marvin.talkback"
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,6 @@ enum class ActionId {
CLEAR_RECENT_APP,

MODIFY_SETTING,

TALKBACK_GESTURE,
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.github.sds100.keymapper.base.keymaps.KeyMap
import io.github.sds100.keymapper.base.utils.DndModeStrings
import io.github.sds100.keymapper.base.utils.KeyCodeStrings
import io.github.sds100.keymapper.base.utils.RingerModeStrings
import io.github.sds100.keymapper.base.utils.TalkBackGestureStrings
import io.github.sds100.keymapper.base.utils.VolumeStreamStrings
import io.github.sds100.keymapper.base.utils.ui.IconInfo
import io.github.sds100.keymapper.base.utils.ui.ResourceProvider
Expand Down Expand Up @@ -686,6 +687,11 @@ class ActionUiHelper(
}
}
}

is ActionData.TalkBackGesture -> {
val actionLabel = getString(TalkBackGestureStrings.getActionLabel(action.gesture))
getString(R.string.action_talkback_gesture_formatted, actionLabel)
}
}

fun getIcon(action: ActionData): ComposeIconInfo = when (action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.material.icons.automirrored.outlined.Undo
import androidx.compose.material.icons.automirrored.outlined.VolumeDown
import androidx.compose.material.icons.automirrored.outlined.VolumeMute
import androidx.compose.material.icons.automirrored.outlined.VolumeUp
import androidx.compose.material.icons.outlined.Accessibility
import androidx.compose.material.icons.outlined.AirplanemodeActive
import androidx.compose.material.icons.outlined.AirplanemodeInactive
import androidx.compose.material.icons.outlined.Assistant
Expand Down Expand Up @@ -248,6 +249,7 @@ object ActionUtils {
ActionId.CLEAR_RECENT_APP -> ActionCategory.APPS
ActionId.MODIFY_SETTING -> ActionCategory.APPS
ActionId.CONSUME_KEY_EVENT -> ActionCategory.SPECIAL
ActionId.TALKBACK_GESTURE -> ActionCategory.INTERFACE
}

@StringRes
Expand Down Expand Up @@ -519,6 +521,8 @@ object ActionUtils {
ActionId.ENABLE_HOTSPOT -> R.string.action_enable_hotspot

ActionId.DISABLE_HOTSPOT -> R.string.action_disable_hotspot

ActionId.TALKBACK_GESTURE -> R.string.action_talkback_gesture
}

@DrawableRes
Expand Down Expand Up @@ -1090,6 +1094,7 @@ object ActionUtils {
ActionId.TOGGLE_HOTSPOT -> Icons.Outlined.WifiTethering
ActionId.ENABLE_HOTSPOT -> Icons.Outlined.WifiTethering
ActionId.DISABLE_HOTSPOT -> Icons.Outlined.WifiTetheringOff
ActionId.TALKBACK_GESTURE -> Icons.Outlined.Accessibility
}
}

Expand Down Expand Up @@ -1140,6 +1145,7 @@ fun ActionData.isEditable(): Boolean = when (this) {
is ActionData.InteractUiElement,
is ActionData.MoveCursor,
is ActionData.ModifySetting,
is ActionData.TalkBackGesture,
-> true

else -> false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.github.sds100.keymapper.base.actions.tapscreen.PickCoordinateResult
import io.github.sds100.keymapper.base.system.intents.ConfigIntentResult
import io.github.sds100.keymapper.base.utils.DndModeStrings
import io.github.sds100.keymapper.base.utils.RingerModeStrings
import io.github.sds100.keymapper.base.utils.TalkBackGestureStrings
import io.github.sds100.keymapper.base.utils.navigation.NavDestination
import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider
import io.github.sds100.keymapper.base.utils.navigation.navigate
Expand Down Expand Up @@ -1213,6 +1214,23 @@ class CreateActionDelegate(

return null
}

ActionId.TALKBACK_GESTURE -> {
val items = TalkBackGestureType.entries.map { gestureType ->
val actionLabel = getString(TalkBackGestureStrings.getActionLabel(gestureType))
val gestureName = getString(TalkBackGestureStrings.getGestureLabel(gestureType))
gestureType to getString(
R.string.talkback_gesture_choice_label,
arrayOf(actionLabel, gestureName),
)
}

val gestureType =
showDialog("pick_talkback_gesture", DialogModel.SingleChoice(items))
?: return null

return ActionData.TalkBackGesture(gestureType)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,10 @@ class PerformActionsUseCaseImpl @AssistedInject constructor(
newValue,
)
}

is ActionData.TalkBackGesture -> {
result = service.performTalkBackGesture(action.gesture)
}
}

when (result) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.github.sds100.keymapper.base.actions

enum class TalkBackGestureType {
// 1-finger swipes
SWIPE_UP,
SWIPE_DOWN,
SWIPE_LEFT,
SWIPE_RIGHT,

// 1-finger angular swipes (two-direction)
SWIPE_UP_THEN_DOWN,
SWIPE_DOWN_THEN_UP,
SWIPE_LEFT_THEN_RIGHT,
SWIPE_RIGHT_THEN_LEFT,
SWIPE_RIGHT_THEN_UP,

// 2-finger gestures
TWO_FINGER_TAP,
TWO_FINGER_DOUBLE_TAP_HOLD,
TWO_FINGER_TRIPLE_TAP,
TWO_FINGER_TRIPLE_TAP_HOLD,

// 3-finger gestures
THREE_FINGER_TAP,
THREE_FINGER_TAP_HOLD,
THREE_FINGER_TRIPLE_TAP_HOLD,
THREE_FINGER_SWIPE_UP,
THREE_FINGER_SWIPE_DOWN,

// 4-finger gestures
FOUR_FINGER_TAP,
FOUR_FINGER_DOUBLE_TAP,
FOUR_FINGER_SWIPE_UP,
FOUR_FINGER_SWIPE_DOWN,
FOUR_FINGER_SWIPE_LEFT,
FOUR_FINGER_SWIPE_RIGHT,
}
Loading
Loading