From c2eed0af5a0ef46389eb297ee8feebca914fd4f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 00:38:40 +0000 Subject: [PATCH 1/4] #262 feat: add support for accessibility navigation gestures (TalkBack) Adds a new "TalkBack gesture" action that dispatches TalkBack navigation gestures (swipes, multi-finger taps, and multi-directional swipes) via the accessibility service's dispatchGesture API. https://claude.ai/code/session_01K15jUcBgeWMmHcmWJ3DWgK --- CHANGELOG.md | 6 + .../keymapper/base/actions/ActionData.kt | 10 + .../base/actions/ActionDataEntityMapper.kt | 21 ++ .../base/actions/ActionErrorSnapshot.kt | 6 + .../sds100/keymapper/base/actions/ActionId.kt | 2 + .../keymapper/base/actions/ActionUiHelper.kt | 6 + .../keymapper/base/actions/ActionUtils.kt | 6 + .../base/actions/CreateActionDelegate.kt | 18 + .../base/actions/PerformActionsUseCase.kt | 4 + .../base/actions/TalkBackGestureType.kt | 37 +++ .../accessibility/BaseAccessibilityService.kt | 308 ++++++++++++++++++ .../accessibility/IAccessibilityService.kt | 3 + .../base/utils/TalkBackGestureStrings.kt | 108 ++++++ base/src/main/res/values/strings.xml | 56 ++++ .../keymapper/data/entities/ActionEntity.kt | 2 + 15 files changed, 593 insertions(+) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/utils/TalkBackGestureStrings.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d05d7c4f..8d540eebc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt index 3c608e7277..ddcdabf3fb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt @@ -1043,4 +1043,14 @@ sealed class ActionData : Comparable { 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) + } + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt index b8caede87b..4b11fb0462 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt @@ -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 @@ -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 @@ -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() } @@ -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", ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt index 6c3e5c78ed..4dcc63aa74 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt @@ -265,6 +265,10 @@ class LazyActionErrorSnapshot( } } + is ActionData.TalkBackGesture -> { + return getAppError(TALKBACK_PACKAGE_NAME) + } + else -> {} } @@ -317,3 +321,5 @@ interface ActionErrorSnapshot { fun getError(action: ActionData): KMError? fun getErrors(actions: List): Map } + +private const val TALKBACK_PACKAGE_NAME = "com.google.android.marvin.talkback" diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionId.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionId.kt index 63f4e62f07..d7f3694432 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionId.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionId.kt @@ -161,4 +161,6 @@ enum class ActionId { CLEAR_RECENT_APP, MODIFY_SETTING, + + TALKBACK_GESTURE, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt index fe8323f74d..61617c5c3c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt @@ -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 @@ -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) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt index 8707fa2c9a..becac27282 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt @@ -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 @@ -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 @@ -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 @@ -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 } } @@ -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 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt index 0f125641dc..f384a4d7b4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt @@ -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 @@ -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) + } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index d193ee28fe..eac8372e48 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -1115,6 +1115,10 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( newValue, ) } + + is ActionData.TalkBackGesture -> { + result = service.performTalkBackGesture(action.gesture) + } } when (result) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt new file mode 100644 index 0000000000..790ce31d86 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt @@ -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, +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index c1d5991a29..d962c5725c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -29,6 +29,7 @@ import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.actions.TalkBackGestureType import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError @@ -573,4 +574,311 @@ abstract class BaseAccessibilityService : override fun performImeAction() { inputMethod?.currentInputConnection?.performEditorAction(EditorInfo.IME_ACTION_UNSPECIFIED) } + + override fun performTalkBackGesture(gesture: TalkBackGestureType): KMResult<*> { + val dm = resources.displayMetrics + val cx = dm.widthPixels / 2f + val cy = dm.heightPixels / 2f + // Use 40% of the smaller screen dimension as swipe length + val swipeLen = minOf(dm.widthPixels, dm.heightPixels) * 0.4f + // Finger spacing for multi-finger gestures (pixels) + val fingerSpacing = dm.density * 40f + + val gestureBuilder = GestureDescription.Builder() + + when (gesture) { + TalkBackGestureType.SWIPE_UP -> + gestureBuilder.addStroke(buildSwipe(cx, cy, cx, cy - swipeLen, 200)) + + TalkBackGestureType.SWIPE_DOWN -> + gestureBuilder.addStroke(buildSwipe(cx, cy, cx, cy + swipeLen, 200)) + + TalkBackGestureType.SWIPE_LEFT -> + gestureBuilder.addStroke(buildSwipe(cx, cy, cx - swipeLen, cy, 200)) + + TalkBackGestureType.SWIPE_RIGHT -> + gestureBuilder.addStroke(buildSwipe(cx, cy, cx + swipeLen, cy, 200)) + + TalkBackGestureType.SWIPE_UP_THEN_DOWN -> { + val s1 = buildChainedSwipe(cx, cy, cx, cy - swipeLen, 150, willContinue = true) + val s2 = s1.continueStroke(buildPath(cx, cy - swipeLen, cx, cy), 150, 150, false) + gestureBuilder.addStroke(s1).addStroke(s2) + } + + TalkBackGestureType.SWIPE_DOWN_THEN_UP -> { + val s1 = buildChainedSwipe(cx, cy, cx, cy + swipeLen, 150, willContinue = true) + val s2 = s1.continueStroke(buildPath(cx, cy + swipeLen, cx, cy), 150, 150, false) + gestureBuilder.addStroke(s1).addStroke(s2) + } + + TalkBackGestureType.SWIPE_LEFT_THEN_RIGHT -> { + val s1 = buildChainedSwipe(cx, cy, cx - swipeLen, cy, 150, willContinue = true) + val s2 = s1.continueStroke(buildPath(cx - swipeLen, cy, cx, cy), 150, 150, false) + gestureBuilder.addStroke(s1).addStroke(s2) + } + + TalkBackGestureType.SWIPE_RIGHT_THEN_LEFT -> { + val s1 = buildChainedSwipe(cx, cy, cx + swipeLen, cy, 150, willContinue = true) + val s2 = s1.continueStroke(buildPath(cx + swipeLen, cy, cx, cy), 150, 150, false) + gestureBuilder.addStroke(s1).addStroke(s2) + } + + TalkBackGestureType.SWIPE_RIGHT_THEN_UP -> { + val s1 = buildChainedSwipe(cx, cy, cx + swipeLen, cy, 150, willContinue = true) + val s2 = s1.continueStroke( + buildPath(cx + swipeLen, cy, cx, cy - swipeLen), + 150, + 150, + false, + ) + gestureBuilder.addStroke(s1).addStroke(s2) + } + + TalkBackGestureType.TWO_FINGER_TAP -> + addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 2, + tapCount = 1, + holdDuration = 50, + ) + + TalkBackGestureType.TWO_FINGER_DOUBLE_TAP_HOLD -> + addMultiFingerDoubleTapHold(gestureBuilder, cx, cy, fingerSpacing, fingerCount = 2) + + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP -> + addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 2, + tapCount = 3, + holdDuration = 50, + ) + + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP_HOLD -> + addMultiFingerTripleTapHold(gestureBuilder, cx, cy, fingerSpacing, fingerCount = 2) + + TalkBackGestureType.THREE_FINGER_TAP -> + addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 3, + tapCount = 1, + holdDuration = 50, + ) + + TalkBackGestureType.THREE_FINGER_TAP_HOLD -> + addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 3, + tapCount = 1, + holdDuration = 600, + ) + + TalkBackGestureType.THREE_FINGER_TRIPLE_TAP_HOLD -> + addMultiFingerTripleTapHold(gestureBuilder, cx, cy, fingerSpacing, fingerCount = 3) + + TalkBackGestureType.THREE_FINGER_SWIPE_UP -> + addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx, + cy - swipeLen, + fingerSpacing, + fingerCount = 3, + ) + + TalkBackGestureType.THREE_FINGER_SWIPE_DOWN -> + addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx, + cy + swipeLen, + fingerSpacing, + fingerCount = 3, + ) + + TalkBackGestureType.FOUR_FINGER_TAP -> + addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 4, + tapCount = 1, + holdDuration = 50, + ) + + TalkBackGestureType.FOUR_FINGER_DOUBLE_TAP -> + addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 4, + tapCount = 2, + holdDuration = 50, + ) + + TalkBackGestureType.FOUR_FINGER_SWIPE_UP -> + addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx, + cy - swipeLen, + fingerSpacing, + fingerCount = 4, + ) + + TalkBackGestureType.FOUR_FINGER_SWIPE_DOWN -> + addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx, + cy + swipeLen, + fingerSpacing, + fingerCount = 4, + ) + + TalkBackGestureType.FOUR_FINGER_SWIPE_LEFT -> + addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx - swipeLen, + cy, + fingerSpacing, + fingerCount = 4, + ) + + TalkBackGestureType.FOUR_FINGER_SWIPE_RIGHT -> + addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx + swipeLen, + cy, + fingerSpacing, + fingerCount = 4, + ) + } + + val success = dispatchGesture(gestureBuilder.build(), null, gestureHandler) + return if (success) Success(Unit) else KMError.FailedToDispatchGesture + } + + private fun buildPath(x1: Float, y1: Float, x2: Float, y2: Float): Path = Path().apply { + moveTo(x1, y1) + lineTo(x2, y2) + } + + private fun buildSwipe( + x1: Float, + y1: Float, + x2: Float, + y2: Float, + duration: Long, + ): StrokeDescription = StrokeDescription(buildPath(x1, y1, x2, y2), 0, duration) + + private fun buildChainedSwipe( + x1: Float, + y1: Float, + x2: Float, + y2: Float, + duration: Long, + willContinue: Boolean, + ): StrokeDescription = StrokeDescription(buildPath(x1, y1, x2, y2), 0, duration, willContinue) + + private fun fingerPositions( + cx: Float, + cy: Float, + spacing: Float, + count: Int, + ): List> { + val total = (count - 1) * spacing + val start = cx - total / 2f + return (0 until count).map { i -> Pair(start + i * spacing, cy) } + } + + private fun addMultiFingerTaps( + builder: GestureDescription.Builder, + cx: Float, + cy: Float, + spacing: Float, + fingerCount: Int, + tapCount: Int, + holdDuration: Long, + ) { + val positions = fingerPositions(cx, cy, spacing, fingerCount) + val tapInterval = 200L + for (tap in 0 until tapCount) { + val startTime = tap * tapInterval + for ((x, y) in positions) { + val path = Path().apply { moveTo(x, y) } + builder.addStroke(StrokeDescription(path, startTime, holdDuration)) + } + } + } + + private fun addMultiFingerDoubleTapHold( + builder: GestureDescription.Builder, + cx: Float, + cy: Float, + spacing: Float, + fingerCount: Int, + ) { + val positions = fingerPositions(cx, cy, spacing, fingerCount) + for ((x, y) in positions) { + val path = Path().apply { moveTo(x, y) } + builder.addStroke(StrokeDescription(path, 0, 50)) + builder.addStroke(StrokeDescription(path, 200, 600)) + } + } + + private fun addMultiFingerTripleTapHold( + builder: GestureDescription.Builder, + cx: Float, + cy: Float, + spacing: Float, + fingerCount: Int, + ) { + val positions = fingerPositions(cx, cy, spacing, fingerCount) + for ((x, y) in positions) { + val path = Path().apply { moveTo(x, y) } + builder.addStroke(StrokeDescription(path, 0, 50)) + builder.addStroke(StrokeDescription(path, 200, 50)) + builder.addStroke(StrokeDescription(path, 400, 600)) + } + } + + private fun addMultiFingerSwipe( + builder: GestureDescription.Builder, + xStart: Float, + yStart: Float, + xEnd: Float, + yEnd: Float, + spacing: Float, + fingerCount: Int, + ) { + val startPositions = fingerPositions(xStart, yStart, spacing, fingerCount) + val endPositions = fingerPositions(xEnd, yEnd, spacing, fingerCount) + for (i in 0 until fingerCount) { + val (sx, sy) = startPositions[i] + val (ex, ey) = endPositions[i] + builder.addStroke(StrokeDescription(buildPath(sx, sy, ex, ey), 0, 200)) + } + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt index 7ae1761d8c..4ceeb3b23a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.base.system.accessibility import android.os.Build import androidx.annotation.RequiresApi +import io.github.sds100.keymapper.base.actions.TalkBackGestureType import io.github.sds100.keymapper.base.system.inputmethod.SwitchImeInterface import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMResult @@ -69,4 +70,6 @@ interface IAccessibilityService : SwitchImeInterface { @RequiresApi(Build.VERSION_CODES.TIRAMISU) fun performImeAction() + + fun performTalkBackGesture(gesture: TalkBackGestureType): KMResult<*> } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/TalkBackGestureStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/TalkBackGestureStrings.kt new file mode 100644 index 0000000000..a4bc3d4a09 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/TalkBackGestureStrings.kt @@ -0,0 +1,108 @@ +package io.github.sds100.keymapper.base.utils + +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.actions.TalkBackGestureType + +object TalkBackGestureStrings { + fun getActionLabel(gesture: TalkBackGestureType): Int = when (gesture) { + TalkBackGestureType.SWIPE_UP -> + R.string.talkback_gesture_action_swipe_up + TalkBackGestureType.SWIPE_DOWN -> + R.string.talkback_gesture_action_swipe_down + TalkBackGestureType.SWIPE_LEFT -> + R.string.talkback_gesture_action_swipe_left + TalkBackGestureType.SWIPE_RIGHT -> + R.string.talkback_gesture_action_swipe_right + TalkBackGestureType.SWIPE_UP_THEN_DOWN -> + R.string.talkback_gesture_action_swipe_up_then_down + TalkBackGestureType.SWIPE_DOWN_THEN_UP -> + R.string.talkback_gesture_action_swipe_down_then_up + TalkBackGestureType.SWIPE_LEFT_THEN_RIGHT -> + R.string.talkback_gesture_action_swipe_left_then_right + TalkBackGestureType.SWIPE_RIGHT_THEN_LEFT -> + R.string.talkback_gesture_action_swipe_right_then_left + TalkBackGestureType.SWIPE_RIGHT_THEN_UP -> + R.string.talkback_gesture_action_swipe_right_then_up + TalkBackGestureType.TWO_FINGER_TAP -> + R.string.talkback_gesture_action_two_finger_tap + TalkBackGestureType.TWO_FINGER_DOUBLE_TAP_HOLD -> + R.string.talkback_gesture_action_two_finger_double_tap_hold + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP -> + R.string.talkback_gesture_action_two_finger_triple_tap + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP_HOLD -> + R.string.talkback_gesture_action_two_finger_triple_tap_hold + TalkBackGestureType.THREE_FINGER_TAP -> + R.string.talkback_gesture_action_three_finger_tap + TalkBackGestureType.THREE_FINGER_TAP_HOLD -> + R.string.talkback_gesture_action_three_finger_tap_hold + TalkBackGestureType.THREE_FINGER_TRIPLE_TAP_HOLD -> + R.string.talkback_gesture_action_three_finger_triple_tap_hold + TalkBackGestureType.THREE_FINGER_SWIPE_UP -> + R.string.talkback_gesture_action_three_finger_swipe_up + TalkBackGestureType.THREE_FINGER_SWIPE_DOWN -> + R.string.talkback_gesture_action_three_finger_swipe_down + TalkBackGestureType.FOUR_FINGER_TAP -> + R.string.talkback_gesture_action_four_finger_tap + TalkBackGestureType.FOUR_FINGER_DOUBLE_TAP -> + R.string.talkback_gesture_action_four_finger_double_tap + TalkBackGestureType.FOUR_FINGER_SWIPE_UP -> + R.string.talkback_gesture_action_four_finger_swipe_up + TalkBackGestureType.FOUR_FINGER_SWIPE_DOWN -> + R.string.talkback_gesture_action_four_finger_swipe_down + TalkBackGestureType.FOUR_FINGER_SWIPE_LEFT -> + R.string.talkback_gesture_action_four_finger_swipe_left + TalkBackGestureType.FOUR_FINGER_SWIPE_RIGHT -> + R.string.talkback_gesture_action_four_finger_swipe_right + } + + fun getGestureLabel(gesture: TalkBackGestureType): Int = when (gesture) { + TalkBackGestureType.SWIPE_UP -> + R.string.talkback_gesture_name_swipe_up + TalkBackGestureType.SWIPE_DOWN -> + R.string.talkback_gesture_name_swipe_down + TalkBackGestureType.SWIPE_LEFT -> + R.string.talkback_gesture_name_swipe_left + TalkBackGestureType.SWIPE_RIGHT -> + R.string.talkback_gesture_name_swipe_right + TalkBackGestureType.SWIPE_UP_THEN_DOWN -> + R.string.talkback_gesture_name_swipe_up_then_down + TalkBackGestureType.SWIPE_DOWN_THEN_UP -> + R.string.talkback_gesture_name_swipe_down_then_up + TalkBackGestureType.SWIPE_LEFT_THEN_RIGHT -> + R.string.talkback_gesture_name_swipe_left_then_right + TalkBackGestureType.SWIPE_RIGHT_THEN_LEFT -> + R.string.talkback_gesture_name_swipe_right_then_left + TalkBackGestureType.SWIPE_RIGHT_THEN_UP -> + R.string.talkback_gesture_name_swipe_right_then_up + TalkBackGestureType.TWO_FINGER_TAP -> + R.string.talkback_gesture_name_two_finger_tap + TalkBackGestureType.TWO_FINGER_DOUBLE_TAP_HOLD -> + R.string.talkback_gesture_name_two_finger_double_tap_hold + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP -> + R.string.talkback_gesture_name_two_finger_triple_tap + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP_HOLD -> + R.string.talkback_gesture_name_two_finger_triple_tap_hold + TalkBackGestureType.THREE_FINGER_TAP -> + R.string.talkback_gesture_name_three_finger_tap + TalkBackGestureType.THREE_FINGER_TAP_HOLD -> + R.string.talkback_gesture_name_three_finger_tap_hold + TalkBackGestureType.THREE_FINGER_TRIPLE_TAP_HOLD -> + R.string.talkback_gesture_name_three_finger_triple_tap_hold + TalkBackGestureType.THREE_FINGER_SWIPE_UP -> + R.string.talkback_gesture_name_three_finger_swipe_up + TalkBackGestureType.THREE_FINGER_SWIPE_DOWN -> + R.string.talkback_gesture_name_three_finger_swipe_down + TalkBackGestureType.FOUR_FINGER_TAP -> + R.string.talkback_gesture_name_four_finger_tap + TalkBackGestureType.FOUR_FINGER_DOUBLE_TAP -> + R.string.talkback_gesture_name_four_finger_double_tap + TalkBackGestureType.FOUR_FINGER_SWIPE_UP -> + R.string.talkback_gesture_name_four_finger_swipe_up + TalkBackGestureType.FOUR_FINGER_SWIPE_DOWN -> + R.string.talkback_gesture_name_four_finger_swipe_down + TalkBackGestureType.FOUR_FINGER_SWIPE_LEFT -> + R.string.talkback_gesture_name_four_finger_swipe_left + TalkBackGestureType.FOUR_FINGER_SWIPE_RIGHT -> + R.string.talkback_gesture_name_four_finger_swipe_right + } +} diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index a081813537..9f9a2c8ca8 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1242,6 +1242,62 @@ Force stop app Close and clear app from recents Modify setting + TalkBack gesture + TalkBack: %s + Choose TalkBack gesture + %1$s (%2$s) + + + Move reading control up or backwards + Move reading control down or forwards + Previous item + Next item + Previous reading control + Next reading control + Scroll back + Scroll forwards + Start voice command + Pause or resume speech + Start or end selection mode + Read from focused item + Turn speech on or off + Open TalkBack menu + Screen search + Tap to assign + Previous reading control + Next reading control + Practice gestures + Open tutorial + Previous window + Next window + Previous container + Next container + + + Swipe up + Swipe down + Swipe left + Swipe right + Swipe up then down + Swipe down then up + Swipe left then right + Swipe right then left + Swipe right then up + Tap with 2 fingers + Double-tap and hold with 2 fingers + Triple-tap with 2 fingers + Triple-tap and hold with 2 fingers + Tap with 3 fingers + Tap and hold with 3 fingers + Triple-tap and hold with 3 fingers + Swipe up with 3 fingers + Swipe down with 3 fingers + Tap with 4 fingers + Double-tap with 4 fingers + Swipe up with 4 fingers + Swipe down with 4 fingers + Swipe left with 4 fingers + Swipe right with 4 fingers Key Value Setting key cannot be empty diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt index 65f644da15..8b0074dbb2 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt @@ -146,6 +146,8 @@ data class ActionEntity( const val EXTRA_SETTING_VALUE = "extra_setting_value" const val EXTRA_SETTING_TYPE = "extra_setting_type" + const val EXTRA_TALKBACK_GESTURE_TYPE = "extra_talkback_gesture_type" + val DESERIALIZER = jsonDeserializer { val typeString by it.json.byNullableString(NAME_ACTION_TYPE) // If it is an unknown type then do not deserialize From e1a2c852e4ffb19592246fe67cf49e85eeb8cb09 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 29 May 2026 15:38:17 +0200 Subject: [PATCH 2/4] #262 add comment --- .../sds100/keymapper/base/actions/TalkBackGestureType.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt index 790ce31d86..5d79d27760 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt @@ -34,4 +34,9 @@ enum class TalkBackGestureType { FOUR_FINGER_SWIPE_DOWN, FOUR_FINGER_SWIPE_LEFT, FOUR_FINGER_SWIPE_RIGHT, + + // NOTE: 4-finger triple tap is not possible. + // Android limits GestureDescription to 10 strokes. + // Four-finger triple-tap would require 12 strokes and is not included in the gesture set + // for this reason. } From 9fd3c9f5c4414b375011e793350fb14636a4a499 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 29 May 2026 15:54:21 +0200 Subject: [PATCH 3/4] #262 refactor code --- .../keymapper/base/actions/ActionData.kt | 4 + .../base/actions/ActionDataEntityMapper.kt | 3 +- .../keymapper/base/actions/ActionUiHelper.kt | 100 ++++- .../base/actions/CreateActionDelegate.kt | 3 +- .../talkback}/TalkBackGestureStrings.kt | 49 ++- .../{ => talkback}/TalkBackGestureType.kt | 2 +- .../talkback/TalkbackGesturePerformer.kt | 350 ++++++++++++++++++ .../AccessibilityGestureUtils.kt | 105 ++++++ .../accessibility/BaseAccessibilityService.kt | 310 +--------------- .../accessibility/IAccessibilityService.kt | 2 +- 10 files changed, 615 insertions(+), 313 deletions(-) rename base/src/main/java/io/github/sds100/keymapper/base/{utils => actions/talkback}/TalkBackGestureStrings.kt (98%) rename base/src/main/java/io/github/sds100/keymapper/base/actions/{ => talkback}/TalkBackGestureType.kt (94%) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkbackGesturePerformer.kt create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityGestureUtils.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt index ddcdabf3fb..0fbbf4a6fc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.actions +import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType import io.github.sds100.keymapper.common.models.ShellExecutionMode import io.github.sds100.keymapper.common.utils.NodeInteractionType import io.github.sds100.keymapper.common.utils.Orientation @@ -95,6 +96,7 @@ sealed class ActionData : Comparable { { it.showVolumeUi }, { it.volumeStream }, ) + else -> super.compareTo(other) } } @@ -111,6 +113,7 @@ sealed class ActionData : Comparable { { it.showVolumeUi }, { it.volumeStream }, ) + else -> super.compareTo(other) } } @@ -1040,6 +1043,7 @@ sealed class ActionData : Comparable { { it.settingKey }, { it.value }, ) + else -> super.compareTo(other) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt index 4b11fb0462..1a84d7c59f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt @@ -2,7 +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.base.actions.talkback.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 @@ -70,6 +70,7 @@ object ActionDataEntityMapper { ActionEntity.Type.MODIFY_SETTING -> ActionId.MODIFY_SETTING ActionEntity.Type.CREATE_NOTIFICATION -> ActionId.CREATE_NOTIFICATION + ActionEntity.Type.TOAST -> ActionId.TOAST } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt index 61617c5c3c..bfa3840b36 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt @@ -5,11 +5,11 @@ import android.view.KeyEvent import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Android import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureStrings 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 @@ -210,22 +210,31 @@ class ActionUiHelper( val resId = when (action) { is ActionData.ControlMediaForApp.Play -> R.string.action_play_media_package_formatted + is ActionData.ControlMediaForApp.FastForward -> R.string.action_fast_forward_package_formatted + is ActionData.ControlMediaForApp.NextTrack -> R.string.action_next_track_package_formatted + is ActionData.ControlMediaForApp.Pause -> R.string.action_pause_media_package_formatted + is ActionData.ControlMediaForApp.PlayPause -> R.string.action_play_pause_media_package_formatted + is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package_formatted + is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package_formatted + is ActionData.ControlMediaForApp.Stop -> R.string.action_stop_media_package_formatted + is ActionData.ControlMediaForApp.StepForward -> R.string.action_step_forward_media_package_formatted + is ActionData.ControlMediaForApp.StepBackward -> R.string.action_step_backward_media_package_formatted } @@ -236,22 +245,31 @@ class ActionUiHelper( val resId = when (action) { is ActionData.ControlMediaForApp.Play -> R.string.action_play_media_package + is ActionData.ControlMediaForApp.FastForward -> R.string.action_fast_forward_package + is ActionData.ControlMediaForApp.NextTrack -> R.string.action_next_track_package + is ActionData.ControlMediaForApp.Pause -> R.string.action_pause_media_package + is ActionData.ControlMediaForApp.PlayPause -> R.string.action_play_pause_media_package + is ActionData.ControlMediaForApp.PreviousTrack -> R.string.action_previous_track_package + is ActionData.ControlMediaForApp.Rewind -> R.string.action_rewind_package + is ActionData.ControlMediaForApp.Stop -> R.string.action_stop_media_package + is ActionData.ControlMediaForApp.StepForward -> R.string.action_step_forward_media_package + is ActionData.ControlMediaForApp.StepBackward -> R.string.action_step_backward_media_package } @@ -449,7 +467,9 @@ class ActionUiHelper( } is ActionData.Text -> getString(R.string.description_text_block, action.text) + is ActionData.Url -> getString(R.string.description_url, action.url) + is ActionData.Sound.SoundFile -> getString( R.string.description_sound, action.soundDescription, @@ -463,57 +483,87 @@ class ActionUiHelper( } ActionData.AirplaneMode.Disable -> getString(R.string.action_disable_airplane_mode) + ActionData.AirplaneMode.Enable -> getString(R.string.action_enable_airplane_mode) + ActionData.AirplaneMode.Toggle -> getString(R.string.action_toggle_airplane_mode) ActionData.Bluetooth.Disable -> getString(R.string.action_disable_bluetooth) + ActionData.Bluetooth.Enable -> getString(R.string.action_enable_bluetooth) + ActionData.Bluetooth.Toggle -> getString(R.string.action_toggle_bluetooth) ActionData.Brightness.Decrease -> getString(R.string.action_decrease_brightness) + ActionData.Brightness.DisableAuto -> getString(R.string.action_disable_auto_brightness) + ActionData.Brightness.EnableAuto -> getString(R.string.action_enable_auto_brightness) + ActionData.Brightness.Increase -> getString(R.string.action_increase_brightness) + ActionData.Brightness.ToggleAuto -> getString(R.string.action_toggle_auto_brightness) ActionData.NightShift.Disable -> getString(R.string.action_disable_night_shift) + ActionData.NightShift.Enable -> getString(R.string.action_enable_night_shift) + ActionData.NightShift.Toggle -> getString(R.string.action_toggle_night_shift) ActionData.ConsumeKeyEvent -> getString(R.string.action_consume_keyevent) ActionData.ControlMedia.FastForward -> getString(R.string.action_fast_forward) + ActionData.ControlMedia.NextTrack -> getString(R.string.action_next_track) + ActionData.ControlMedia.Pause -> getString(R.string.action_pause_media) + ActionData.ControlMedia.Play -> getString(R.string.action_play_media) + ActionData.ControlMedia.PlayPause -> getString(R.string.action_play_pause_media) + ActionData.ControlMedia.PreviousTrack -> getString(R.string.action_previous_track) + ActionData.ControlMedia.Rewind -> getString(R.string.action_rewind) + ActionData.ControlMedia.Stop -> getString(R.string.action_stop_media) + ActionData.ControlMedia.StepForward -> getString(R.string.action_step_forward_media) + ActionData.ControlMedia.StepBackward -> getString(R.string.action_step_backward_media) ActionData.CopyText -> getString(R.string.action_text_copy) + ActionData.CutText -> getString(R.string.action_text_cut) + ActionData.PasteText -> getString(R.string.action_text_paste) ActionData.DeviceAssistant -> getString(R.string.action_open_device_assistant) ActionData.GoBack -> getString(R.string.action_go_back) + ActionData.GoHome -> getString(R.string.action_go_home) + ActionData.GoLastApp -> getString(R.string.action_go_last_app) + ActionData.OpenMenu -> getString(R.string.action_open_menu) + ActionData.OpenRecents -> getString(R.string.action_open_recents) ActionData.HideKeyboard -> getString(R.string.action_hide_keyboard) + ActionData.LockDevice -> getString(R.string.action_lock_device) ActionData.MobileData.Disable -> getString(R.string.action_disable_mobile_data) + ActionData.MobileData.Enable -> getString(R.string.action_enable_mobile_data) + ActionData.MobileData.Toggle -> getString(R.string.action_toggle_mobile_data) ActionData.Hotspot.Disable -> getString(R.string.action_disable_hotspot) + ActionData.Hotspot.Enable -> getString(R.string.action_enable_hotspot) + ActionData.Hotspot.Toggle -> getString(R.string.action_toggle_hotspot) is ActionData.MoveCursor -> { @@ -523,15 +573,19 @@ class ActionUiHelper( ActionData.MoveCursor.Type.CHAR -> getString( R.string.action_move_cursor_prev_character, ) + ActionData.MoveCursor.Type.WORD -> getString( R.string.action_move_cursor_start_word, ) + ActionData.MoveCursor.Type.LINE -> getString( R.string.action_move_cursor_start_line, ) + ActionData.MoveCursor.Type.PARAGRAPH -> getString( R.string.action_move_cursor_start_paragraph, ) + ActionData.MoveCursor.Type.PAGE -> getString( R.string.action_move_cursor_start_page, ) @@ -543,15 +597,19 @@ class ActionUiHelper( ActionData.MoveCursor.Type.CHAR -> getString( R.string.action_move_cursor_next_character, ) + ActionData.MoveCursor.Type.WORD -> getString( R.string.action_move_cursor_end_word, ) + ActionData.MoveCursor.Type.LINE -> getString( R.string.action_move_cursor_end_line, ) + ActionData.MoveCursor.Type.PARAGRAPH -> getString( R.string.action_move_cursor_end_paragraph, ) + ActionData.MoveCursor.Type.PAGE -> getString( R.string.action_move_cursor_end_page, ) @@ -561,10 +619,13 @@ class ActionUiHelper( } ActionData.Nfc.Disable -> getString(R.string.action_nfc_disable) + ActionData.Nfc.Enable -> getString(R.string.action_nfc_enable) + ActionData.Nfc.Toggle -> getString(R.string.action_nfc_toggle) ActionData.OpenCamera -> getString(R.string.action_open_camera) + ActionData.OpenSettings -> getString(R.string.action_open_settings) is ActionData.Rotation.CycleRotations -> { @@ -584,48 +645,73 @@ class ActionUiHelper( } ActionData.Rotation.DisableAuto -> getString(R.string.action_disable_auto_rotate) + ActionData.Rotation.EnableAuto -> getString(R.string.action_enable_auto_rotate) + ActionData.Rotation.Landscape -> getString(R.string.action_landscape_mode) + ActionData.Rotation.Portrait -> getString(R.string.action_portrait_mode) + ActionData.Rotation.SwitchOrientation -> getString(R.string.action_switch_orientation) + ActionData.Rotation.ToggleAuto -> getString(R.string.action_toggle_auto_rotate) ActionData.ScreenOnOff -> getString(R.string.action_power_on_off_device) + ActionData.Screenshot -> getString(R.string.action_screenshot) + ActionData.SecureLock -> getString(R.string.action_secure_lock_device) + ActionData.SelectWordAtCursor -> getString(R.string.action_select_word_at_cursor) + ActionData.SelectAllText -> getString(R.string.action_select_all_text) + ActionData.ShowKeyboard -> getString(R.string.action_show_keyboard) + ActionData.ShowKeyboardPicker -> getString(R.string.action_show_keyboard_picker) + ActionData.PerformImeAction -> getString(R.string.action_perform_ime_action) + ActionData.ShowPowerMenu -> getString(R.string.action_show_power_menu) ActionData.StatusBar.Collapse -> getString(R.string.action_collapse_status_bar) + ActionData.StatusBar.ExpandNotifications -> getString( R.string.action_expand_notification_drawer, ) + ActionData.StatusBar.ExpandQuickSettings -> getString(R.string.action_expand_quick_settings) + ActionData.StatusBar.ToggleNotifications -> getString( R.string.action_toggle_notification_drawer, ) + ActionData.StatusBar.ToggleQuickSettings -> getString(R.string.action_toggle_quick_settings) ActionData.ToggleKeyboard -> getString(R.string.action_toggle_keyboard) + ActionData.ToggleSplitScreen -> getString(R.string.action_toggle_split_screen) + ActionData.VoiceAssistant -> getString(R.string.action_open_assistant) ActionData.Wifi.Disable -> getString(R.string.action_disable_wifi) + ActionData.Wifi.Enable -> getString(R.string.action_enable_wifi) + ActionData.Wifi.Toggle -> getString(R.string.action_toggle_wifi) + ActionData.DismissAllNotifications -> getString(R.string.action_dismiss_all_notifications) + ActionData.DismissLastNotification -> getString( R.string.action_dismiss_most_recent_notification, ) ActionData.AnswerCall -> getString(R.string.action_answer_call) + ActionData.EndCall -> getString(R.string.action_end_call) ActionData.DeviceControls -> getString(R.string.action_device_controls) + is ActionData.HttpRequest -> action.description is ActionData.ShellCommand -> when (action.executionMode) { @@ -648,7 +734,9 @@ class ActionUiHelper( is ActionData.InteractUiElement -> action.description ActionData.ClearRecentApp -> getString(R.string.action_clear_recent_app) + ActionData.ForceStopApp -> getString(R.string.action_force_stop_app) + is ActionData.ComposeSms -> getString( R.string.action_compose_sms_description, arrayOf(action.message, action.number), @@ -660,7 +748,9 @@ class ActionUiHelper( ) ActionData.Microphone.Mute -> getString(R.string.action_mute_microphone) + ActionData.Microphone.Toggle -> getString(R.string.action_toggle_mute_microphone) + ActionData.Microphone.Unmute -> getString(R.string.action_unmute_microphone) is ActionData.ModifySetting -> { @@ -682,6 +772,7 @@ class ActionUiHelper( ActionData.Toast.Duration.SHORT -> { getString(R.string.action_toast_description_short, action.message) } + ActionData.Toast.Duration.LONG -> { getString(R.string.action_toast_description_long, action.message) } @@ -749,7 +840,9 @@ class ActionUiHelper( ) is ActionData.Text -> null + is ActionData.Url -> null + is ActionData.Sound -> IconInfo( getDrawable(R.drawable.ic_outline_volume_up_24), TintType.OnSurface, @@ -770,7 +863,10 @@ class ActionUiHelper( val repeatLimit = when { action.repeatLimit != null -> action.repeatLimit - action.repeatMode == RepeatMode.LIMIT_REACHED -> 1 // and is null + + action.repeatMode == RepeatMode.LIMIT_REACHED -> 1 + + // and is null else -> null } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt index f384a4d7b4..9cedfd1ec2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt @@ -8,11 +8,12 @@ import androidx.compose.runtime.snapshotFlow import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.pinchscreen.PinchPickCoordinateResult import io.github.sds100.keymapper.base.actions.swipescreen.SwipePickCoordinateResult +import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureStrings +import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType 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 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/TalkBackGestureStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureStrings.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/utils/TalkBackGestureStrings.kt rename to base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureStrings.kt index a4bc3d4a09..e6956c4f69 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/TalkBackGestureStrings.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureStrings.kt @@ -1,56 +1,78 @@ -package io.github.sds100.keymapper.base.utils +package io.github.sds100.keymapper.base.actions.talkback import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.actions.TalkBackGestureType object TalkBackGestureStrings { fun getActionLabel(gesture: TalkBackGestureType): Int = when (gesture) { TalkBackGestureType.SWIPE_UP -> R.string.talkback_gesture_action_swipe_up + TalkBackGestureType.SWIPE_DOWN -> R.string.talkback_gesture_action_swipe_down + TalkBackGestureType.SWIPE_LEFT -> R.string.talkback_gesture_action_swipe_left + TalkBackGestureType.SWIPE_RIGHT -> R.string.talkback_gesture_action_swipe_right + TalkBackGestureType.SWIPE_UP_THEN_DOWN -> R.string.talkback_gesture_action_swipe_up_then_down + TalkBackGestureType.SWIPE_DOWN_THEN_UP -> R.string.talkback_gesture_action_swipe_down_then_up + TalkBackGestureType.SWIPE_LEFT_THEN_RIGHT -> R.string.talkback_gesture_action_swipe_left_then_right + TalkBackGestureType.SWIPE_RIGHT_THEN_LEFT -> R.string.talkback_gesture_action_swipe_right_then_left + TalkBackGestureType.SWIPE_RIGHT_THEN_UP -> R.string.talkback_gesture_action_swipe_right_then_up + TalkBackGestureType.TWO_FINGER_TAP -> R.string.talkback_gesture_action_two_finger_tap + TalkBackGestureType.TWO_FINGER_DOUBLE_TAP_HOLD -> R.string.talkback_gesture_action_two_finger_double_tap_hold + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP -> R.string.talkback_gesture_action_two_finger_triple_tap + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP_HOLD -> R.string.talkback_gesture_action_two_finger_triple_tap_hold + TalkBackGestureType.THREE_FINGER_TAP -> R.string.talkback_gesture_action_three_finger_tap + TalkBackGestureType.THREE_FINGER_TAP_HOLD -> R.string.talkback_gesture_action_three_finger_tap_hold + TalkBackGestureType.THREE_FINGER_TRIPLE_TAP_HOLD -> R.string.talkback_gesture_action_three_finger_triple_tap_hold + TalkBackGestureType.THREE_FINGER_SWIPE_UP -> R.string.talkback_gesture_action_three_finger_swipe_up + TalkBackGestureType.THREE_FINGER_SWIPE_DOWN -> R.string.talkback_gesture_action_three_finger_swipe_down + TalkBackGestureType.FOUR_FINGER_TAP -> R.string.talkback_gesture_action_four_finger_tap + TalkBackGestureType.FOUR_FINGER_DOUBLE_TAP -> R.string.talkback_gesture_action_four_finger_double_tap + TalkBackGestureType.FOUR_FINGER_SWIPE_UP -> R.string.talkback_gesture_action_four_finger_swipe_up + TalkBackGestureType.FOUR_FINGER_SWIPE_DOWN -> R.string.talkback_gesture_action_four_finger_swipe_down + TalkBackGestureType.FOUR_FINGER_SWIPE_LEFT -> R.string.talkback_gesture_action_four_finger_swipe_left + TalkBackGestureType.FOUR_FINGER_SWIPE_RIGHT -> R.string.talkback_gesture_action_four_finger_swipe_right } @@ -58,50 +80,73 @@ object TalkBackGestureStrings { fun getGestureLabel(gesture: TalkBackGestureType): Int = when (gesture) { TalkBackGestureType.SWIPE_UP -> R.string.talkback_gesture_name_swipe_up + TalkBackGestureType.SWIPE_DOWN -> R.string.talkback_gesture_name_swipe_down + TalkBackGestureType.SWIPE_LEFT -> R.string.talkback_gesture_name_swipe_left + TalkBackGestureType.SWIPE_RIGHT -> R.string.talkback_gesture_name_swipe_right + TalkBackGestureType.SWIPE_UP_THEN_DOWN -> R.string.talkback_gesture_name_swipe_up_then_down + TalkBackGestureType.SWIPE_DOWN_THEN_UP -> R.string.talkback_gesture_name_swipe_down_then_up + TalkBackGestureType.SWIPE_LEFT_THEN_RIGHT -> R.string.talkback_gesture_name_swipe_left_then_right + TalkBackGestureType.SWIPE_RIGHT_THEN_LEFT -> R.string.talkback_gesture_name_swipe_right_then_left + TalkBackGestureType.SWIPE_RIGHT_THEN_UP -> R.string.talkback_gesture_name_swipe_right_then_up + TalkBackGestureType.TWO_FINGER_TAP -> R.string.talkback_gesture_name_two_finger_tap + TalkBackGestureType.TWO_FINGER_DOUBLE_TAP_HOLD -> R.string.talkback_gesture_name_two_finger_double_tap_hold + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP -> R.string.talkback_gesture_name_two_finger_triple_tap + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP_HOLD -> R.string.talkback_gesture_name_two_finger_triple_tap_hold + TalkBackGestureType.THREE_FINGER_TAP -> R.string.talkback_gesture_name_three_finger_tap + TalkBackGestureType.THREE_FINGER_TAP_HOLD -> R.string.talkback_gesture_name_three_finger_tap_hold + TalkBackGestureType.THREE_FINGER_TRIPLE_TAP_HOLD -> R.string.talkback_gesture_name_three_finger_triple_tap_hold + TalkBackGestureType.THREE_FINGER_SWIPE_UP -> R.string.talkback_gesture_name_three_finger_swipe_up + TalkBackGestureType.THREE_FINGER_SWIPE_DOWN -> R.string.talkback_gesture_name_three_finger_swipe_down + TalkBackGestureType.FOUR_FINGER_TAP -> R.string.talkback_gesture_name_four_finger_tap + TalkBackGestureType.FOUR_FINGER_DOUBLE_TAP -> R.string.talkback_gesture_name_four_finger_double_tap + TalkBackGestureType.FOUR_FINGER_SWIPE_UP -> R.string.talkback_gesture_name_four_finger_swipe_up + TalkBackGestureType.FOUR_FINGER_SWIPE_DOWN -> R.string.talkback_gesture_name_four_finger_swipe_down + TalkBackGestureType.FOUR_FINGER_SWIPE_LEFT -> R.string.talkback_gesture_name_four_finger_swipe_left + TalkBackGestureType.FOUR_FINGER_SWIPE_RIGHT -> R.string.talkback_gesture_name_four_finger_swipe_right } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureType.kt similarity index 94% rename from base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt rename to base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureType.kt index 5d79d27760..db81147d59 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/TalkBackGestureType.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureType.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.actions +package io.github.sds100.keymapper.base.actions.talkback enum class TalkBackGestureType { // 1-finger swipes diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkbackGesturePerformer.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkbackGesturePerformer.kt new file mode 100644 index 0000000000..1823e6ab54 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkbackGesturePerformer.kt @@ -0,0 +1,350 @@ +package io.github.sds100.keymapper.base.actions.talkback + +import android.accessibilityservice.AccessibilityService +import android.accessibilityservice.GestureDescription +import android.os.Handler +import io.github.sds100.keymapper.base.system.accessibility.AccessibilityGestureUtils +import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success + +object TalkbackGesturePerformer { + fun performTalkBackGesture( + service: AccessibilityService, + gesture: TalkBackGestureType, + gestureHandler: Handler?, + ): KMResult<*> { + val dm = service.resources.displayMetrics + val cx = dm.widthPixels / 2f + val cy = dm.heightPixels / 2f + // Use 40% of the smaller screen dimension as swipe length + val swipeLen = minOf(dm.widthPixels, dm.heightPixels) * 0.4f + // Finger spacing for multi-finger gestures (pixels) + val fingerSpacing = dm.density * 40f + + val gestureBuilder = GestureDescription.Builder() + + when (gesture) { + TalkBackGestureType.SWIPE_UP -> + gestureBuilder.addStroke( + AccessibilityGestureUtils.buildSwipe( + cx, + cy, + cx, + cy - swipeLen, + 200, + ), + ) + + TalkBackGestureType.SWIPE_DOWN -> + gestureBuilder.addStroke( + AccessibilityGestureUtils.buildSwipe( + cx, + cy, + cx, + cy + swipeLen, + 200, + ), + ) + + TalkBackGestureType.SWIPE_LEFT -> + gestureBuilder.addStroke( + AccessibilityGestureUtils.buildSwipe( + cx, + cy, + cx - swipeLen, + cy, + 200, + ), + ) + + TalkBackGestureType.SWIPE_RIGHT -> + gestureBuilder.addStroke( + AccessibilityGestureUtils.buildSwipe( + cx, + cy, + cx + swipeLen, + cy, + 200, + ), + ) + + TalkBackGestureType.SWIPE_UP_THEN_DOWN -> { + val s1 = AccessibilityGestureUtils.buildChainedSwipe( + cx, + cy, + cx, + cy - swipeLen, + 150, + willContinue = true, + ) + val s2 = s1.continueStroke( + AccessibilityGestureUtils.buildPath( + cx, + cy - swipeLen, + cx, + cy, + ), + 150, + 150, + false, + ) + gestureBuilder.addStroke(s1).addStroke(s2) + } + + TalkBackGestureType.SWIPE_DOWN_THEN_UP -> { + val s1 = AccessibilityGestureUtils.buildChainedSwipe( + cx, + cy, + cx, + cy + swipeLen, + 150, + willContinue = true, + ) + val s2 = s1.continueStroke( + AccessibilityGestureUtils.buildPath( + cx, + cy + swipeLen, + cx, + cy, + ), + 150, + 150, + false, + ) + gestureBuilder.addStroke(s1).addStroke(s2) + } + + TalkBackGestureType.SWIPE_LEFT_THEN_RIGHT -> { + val s1 = AccessibilityGestureUtils.buildChainedSwipe( + cx, + cy, + cx - swipeLen, + cy, + 150, + willContinue = true, + ) + val s2 = s1.continueStroke( + AccessibilityGestureUtils.buildPath( + cx - swipeLen, + cy, + cx, + cy, + ), + 150, + 150, + false, + ) + gestureBuilder.addStroke(s1).addStroke(s2) + } + + TalkBackGestureType.SWIPE_RIGHT_THEN_LEFT -> { + val s1 = AccessibilityGestureUtils.buildChainedSwipe( + cx, + cy, + cx + swipeLen, + cy, + 150, + willContinue = true, + ) + val s2 = s1.continueStroke( + AccessibilityGestureUtils.buildPath( + cx + swipeLen, + cy, + cx, + cy, + ), + 150, + 150, + false, + ) + gestureBuilder.addStroke(s1).addStroke(s2) + } + + TalkBackGestureType.SWIPE_RIGHT_THEN_UP -> { + val s1 = AccessibilityGestureUtils.buildChainedSwipe( + cx, + cy, + cx + swipeLen, + cy, + 150, + willContinue = true, + ) + val s2 = s1.continueStroke( + AccessibilityGestureUtils.buildPath(cx + swipeLen, cy, cx, cy - swipeLen), + 150, + 150, + false, + ) + gestureBuilder.addStroke(s1).addStroke(s2) + } + + TalkBackGestureType.TWO_FINGER_TAP -> + AccessibilityGestureUtils.addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 2, + tapCount = 1, + holdDuration = 50, + ) + + TalkBackGestureType.TWO_FINGER_DOUBLE_TAP_HOLD -> + AccessibilityGestureUtils.addMultiFingerDoubleTapHold( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 2, + ) + + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP -> + AccessibilityGestureUtils.addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 2, + tapCount = 3, + holdDuration = 50, + ) + + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP_HOLD -> + AccessibilityGestureUtils.addMultiFingerTripleTapHold( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 2, + ) + + TalkBackGestureType.THREE_FINGER_TAP -> + AccessibilityGestureUtils.addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 3, + tapCount = 1, + holdDuration = 50, + ) + + TalkBackGestureType.THREE_FINGER_TAP_HOLD -> + AccessibilityGestureUtils.addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 3, + tapCount = 1, + holdDuration = 600, + ) + + TalkBackGestureType.THREE_FINGER_TRIPLE_TAP_HOLD -> + AccessibilityGestureUtils.addMultiFingerTripleTapHold( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 3, + ) + + TalkBackGestureType.THREE_FINGER_SWIPE_UP -> + AccessibilityGestureUtils.addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx, + cy - swipeLen, + fingerSpacing, + fingerCount = 3, + ) + + TalkBackGestureType.THREE_FINGER_SWIPE_DOWN -> + AccessibilityGestureUtils.addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx, + cy + swipeLen, + fingerSpacing, + fingerCount = 3, + ) + + TalkBackGestureType.FOUR_FINGER_TAP -> + AccessibilityGestureUtils.addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 4, + tapCount = 1, + holdDuration = 50, + ) + + TalkBackGestureType.FOUR_FINGER_DOUBLE_TAP -> + AccessibilityGestureUtils.addMultiFingerTaps( + gestureBuilder, + cx, + cy, + fingerSpacing, + fingerCount = 4, + tapCount = 2, + holdDuration = 50, + ) + + TalkBackGestureType.FOUR_FINGER_SWIPE_UP -> + AccessibilityGestureUtils.addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx, + cy - swipeLen, + fingerSpacing, + fingerCount = 4, + ) + + TalkBackGestureType.FOUR_FINGER_SWIPE_DOWN -> + AccessibilityGestureUtils.addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx, + cy + swipeLen, + fingerSpacing, + fingerCount = 4, + ) + + TalkBackGestureType.FOUR_FINGER_SWIPE_LEFT -> + AccessibilityGestureUtils.addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx - swipeLen, + cy, + fingerSpacing, + fingerCount = 4, + ) + + TalkBackGestureType.FOUR_FINGER_SWIPE_RIGHT -> + AccessibilityGestureUtils.addMultiFingerSwipe( + gestureBuilder, + cx, + cy, + cx + swipeLen, + cy, + fingerSpacing, + fingerCount = 4, + ) + } + + val success = service.dispatchGesture(gestureBuilder.build(), null, gestureHandler) + + return if (success) { + Success(Unit) + } else { + KMError.FailedToDispatchGesture + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityGestureUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityGestureUtils.kt new file mode 100644 index 0000000000..2dbdda4b0b --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityGestureUtils.kt @@ -0,0 +1,105 @@ +package io.github.sds100.keymapper.base.system.accessibility + +import android.accessibilityservice.GestureDescription +import android.accessibilityservice.GestureDescription.StrokeDescription +import android.graphics.Path + +object AccessibilityGestureUtils { + + fun buildSwipe(x1: Float, y1: Float, x2: Float, y2: Float, duration: Long): StrokeDescription = + StrokeDescription(buildPath(x1, y1, x2, y2), 0, duration) + + fun buildChainedSwipe( + x1: Float, + y1: Float, + x2: Float, + y2: Float, + duration: Long, + willContinue: Boolean, + ): StrokeDescription = StrokeDescription(buildPath(x1, y1, x2, y2), 0, duration, willContinue) + + fun fingerPositions( + cx: Float, + cy: Float, + spacing: Float, + count: Int, + ): List> { + val total = (count - 1) * spacing + val start = cx - total / 2f + return (0 until count).map { i -> Pair(start + i * spacing, cy) } + } + + fun addMultiFingerTaps( + builder: GestureDescription.Builder, + cx: Float, + cy: Float, + spacing: Float, + fingerCount: Int, + tapCount: Int, + holdDuration: Long, + ) { + val positions = fingerPositions(cx, cy, spacing, fingerCount) + val tapInterval = 200L + for (tap in 0 until tapCount) { + val startTime = tap * tapInterval + for ((x, y) in positions) { + val path = Path().apply { moveTo(x, y) } + builder.addStroke(StrokeDescription(path, startTime, holdDuration)) + } + } + } + + fun addMultiFingerDoubleTapHold( + builder: GestureDescription.Builder, + cx: Float, + cy: Float, + spacing: Float, + fingerCount: Int, + ) { + val positions = fingerPositions(cx, cy, spacing, fingerCount) + for ((x, y) in positions) { + val path = Path().apply { moveTo(x, y) } + builder.addStroke(StrokeDescription(path, 0, 50)) + builder.addStroke(StrokeDescription(path, 200, 600)) + } + } + + fun addMultiFingerTripleTapHold( + builder: GestureDescription.Builder, + cx: Float, + cy: Float, + spacing: Float, + fingerCount: Int, + ) { + val positions = fingerPositions(cx, cy, spacing, fingerCount) + for ((x, y) in positions) { + val path = Path().apply { moveTo(x, y) } + builder.addStroke(StrokeDescription(path, 0, 50)) + builder.addStroke(StrokeDescription(path, 200, 50)) + builder.addStroke(StrokeDescription(path, 400, 600)) + } + } + + fun addMultiFingerSwipe( + builder: GestureDescription.Builder, + xStart: Float, + yStart: Float, + xEnd: Float, + yEnd: Float, + spacing: Float, + fingerCount: Int, + ) { + val startPositions = fingerPositions(xStart, yStart, spacing, fingerCount) + val endPositions = fingerPositions(xEnd, yEnd, spacing, fingerCount) + for (i in 0 until fingerCount) { + val (sx, sy) = startPositions[i] + val (ex, ey) = endPositions[i] + builder.addStroke(StrokeDescription(buildPath(sx, sy, ex, ey), 0, 200)) + } + } + + fun buildPath(x1: Float, y1: Float, x2: Float, y2: Float): Path = Path().apply { + moveTo(x1, y1) + lineTo(x2, y2) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index d962c5725c..d247d41075 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -29,7 +29,8 @@ import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.actions.TalkBackGestureType +import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType +import io.github.sds100.keymapper.base.actions.talkback.TalkbackGesturePerformer import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError @@ -562,6 +563,7 @@ abstract class BaseAccessibilityService : return imeWindow != null && imeWindow.root?.isVisibleToUser == true } + @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun injectText(text: String) { inputMethod?.currentInputConnection?.commitText( text, @@ -571,314 +573,12 @@ abstract class BaseAccessibilityService : ) } + @RequiresApi(Build.VERSION_CODES.TIRAMISU) override fun performImeAction() { inputMethod?.currentInputConnection?.performEditorAction(EditorInfo.IME_ACTION_UNSPECIFIED) } override fun performTalkBackGesture(gesture: TalkBackGestureType): KMResult<*> { - val dm = resources.displayMetrics - val cx = dm.widthPixels / 2f - val cy = dm.heightPixels / 2f - // Use 40% of the smaller screen dimension as swipe length - val swipeLen = minOf(dm.widthPixels, dm.heightPixels) * 0.4f - // Finger spacing for multi-finger gestures (pixels) - val fingerSpacing = dm.density * 40f - - val gestureBuilder = GestureDescription.Builder() - - when (gesture) { - TalkBackGestureType.SWIPE_UP -> - gestureBuilder.addStroke(buildSwipe(cx, cy, cx, cy - swipeLen, 200)) - - TalkBackGestureType.SWIPE_DOWN -> - gestureBuilder.addStroke(buildSwipe(cx, cy, cx, cy + swipeLen, 200)) - - TalkBackGestureType.SWIPE_LEFT -> - gestureBuilder.addStroke(buildSwipe(cx, cy, cx - swipeLen, cy, 200)) - - TalkBackGestureType.SWIPE_RIGHT -> - gestureBuilder.addStroke(buildSwipe(cx, cy, cx + swipeLen, cy, 200)) - - TalkBackGestureType.SWIPE_UP_THEN_DOWN -> { - val s1 = buildChainedSwipe(cx, cy, cx, cy - swipeLen, 150, willContinue = true) - val s2 = s1.continueStroke(buildPath(cx, cy - swipeLen, cx, cy), 150, 150, false) - gestureBuilder.addStroke(s1).addStroke(s2) - } - - TalkBackGestureType.SWIPE_DOWN_THEN_UP -> { - val s1 = buildChainedSwipe(cx, cy, cx, cy + swipeLen, 150, willContinue = true) - val s2 = s1.continueStroke(buildPath(cx, cy + swipeLen, cx, cy), 150, 150, false) - gestureBuilder.addStroke(s1).addStroke(s2) - } - - TalkBackGestureType.SWIPE_LEFT_THEN_RIGHT -> { - val s1 = buildChainedSwipe(cx, cy, cx - swipeLen, cy, 150, willContinue = true) - val s2 = s1.continueStroke(buildPath(cx - swipeLen, cy, cx, cy), 150, 150, false) - gestureBuilder.addStroke(s1).addStroke(s2) - } - - TalkBackGestureType.SWIPE_RIGHT_THEN_LEFT -> { - val s1 = buildChainedSwipe(cx, cy, cx + swipeLen, cy, 150, willContinue = true) - val s2 = s1.continueStroke(buildPath(cx + swipeLen, cy, cx, cy), 150, 150, false) - gestureBuilder.addStroke(s1).addStroke(s2) - } - - TalkBackGestureType.SWIPE_RIGHT_THEN_UP -> { - val s1 = buildChainedSwipe(cx, cy, cx + swipeLen, cy, 150, willContinue = true) - val s2 = s1.continueStroke( - buildPath(cx + swipeLen, cy, cx, cy - swipeLen), - 150, - 150, - false, - ) - gestureBuilder.addStroke(s1).addStroke(s2) - } - - TalkBackGestureType.TWO_FINGER_TAP -> - addMultiFingerTaps( - gestureBuilder, - cx, - cy, - fingerSpacing, - fingerCount = 2, - tapCount = 1, - holdDuration = 50, - ) - - TalkBackGestureType.TWO_FINGER_DOUBLE_TAP_HOLD -> - addMultiFingerDoubleTapHold(gestureBuilder, cx, cy, fingerSpacing, fingerCount = 2) - - TalkBackGestureType.TWO_FINGER_TRIPLE_TAP -> - addMultiFingerTaps( - gestureBuilder, - cx, - cy, - fingerSpacing, - fingerCount = 2, - tapCount = 3, - holdDuration = 50, - ) - - TalkBackGestureType.TWO_FINGER_TRIPLE_TAP_HOLD -> - addMultiFingerTripleTapHold(gestureBuilder, cx, cy, fingerSpacing, fingerCount = 2) - - TalkBackGestureType.THREE_FINGER_TAP -> - addMultiFingerTaps( - gestureBuilder, - cx, - cy, - fingerSpacing, - fingerCount = 3, - tapCount = 1, - holdDuration = 50, - ) - - TalkBackGestureType.THREE_FINGER_TAP_HOLD -> - addMultiFingerTaps( - gestureBuilder, - cx, - cy, - fingerSpacing, - fingerCount = 3, - tapCount = 1, - holdDuration = 600, - ) - - TalkBackGestureType.THREE_FINGER_TRIPLE_TAP_HOLD -> - addMultiFingerTripleTapHold(gestureBuilder, cx, cy, fingerSpacing, fingerCount = 3) - - TalkBackGestureType.THREE_FINGER_SWIPE_UP -> - addMultiFingerSwipe( - gestureBuilder, - cx, - cy, - cx, - cy - swipeLen, - fingerSpacing, - fingerCount = 3, - ) - - TalkBackGestureType.THREE_FINGER_SWIPE_DOWN -> - addMultiFingerSwipe( - gestureBuilder, - cx, - cy, - cx, - cy + swipeLen, - fingerSpacing, - fingerCount = 3, - ) - - TalkBackGestureType.FOUR_FINGER_TAP -> - addMultiFingerTaps( - gestureBuilder, - cx, - cy, - fingerSpacing, - fingerCount = 4, - tapCount = 1, - holdDuration = 50, - ) - - TalkBackGestureType.FOUR_FINGER_DOUBLE_TAP -> - addMultiFingerTaps( - gestureBuilder, - cx, - cy, - fingerSpacing, - fingerCount = 4, - tapCount = 2, - holdDuration = 50, - ) - - TalkBackGestureType.FOUR_FINGER_SWIPE_UP -> - addMultiFingerSwipe( - gestureBuilder, - cx, - cy, - cx, - cy - swipeLen, - fingerSpacing, - fingerCount = 4, - ) - - TalkBackGestureType.FOUR_FINGER_SWIPE_DOWN -> - addMultiFingerSwipe( - gestureBuilder, - cx, - cy, - cx, - cy + swipeLen, - fingerSpacing, - fingerCount = 4, - ) - - TalkBackGestureType.FOUR_FINGER_SWIPE_LEFT -> - addMultiFingerSwipe( - gestureBuilder, - cx, - cy, - cx - swipeLen, - cy, - fingerSpacing, - fingerCount = 4, - ) - - TalkBackGestureType.FOUR_FINGER_SWIPE_RIGHT -> - addMultiFingerSwipe( - gestureBuilder, - cx, - cy, - cx + swipeLen, - cy, - fingerSpacing, - fingerCount = 4, - ) - } - - val success = dispatchGesture(gestureBuilder.build(), null, gestureHandler) - return if (success) Success(Unit) else KMError.FailedToDispatchGesture - } - - private fun buildPath(x1: Float, y1: Float, x2: Float, y2: Float): Path = Path().apply { - moveTo(x1, y1) - lineTo(x2, y2) - } - - private fun buildSwipe( - x1: Float, - y1: Float, - x2: Float, - y2: Float, - duration: Long, - ): StrokeDescription = StrokeDescription(buildPath(x1, y1, x2, y2), 0, duration) - - private fun buildChainedSwipe( - x1: Float, - y1: Float, - x2: Float, - y2: Float, - duration: Long, - willContinue: Boolean, - ): StrokeDescription = StrokeDescription(buildPath(x1, y1, x2, y2), 0, duration, willContinue) - - private fun fingerPositions( - cx: Float, - cy: Float, - spacing: Float, - count: Int, - ): List> { - val total = (count - 1) * spacing - val start = cx - total / 2f - return (0 until count).map { i -> Pair(start + i * spacing, cy) } - } - - private fun addMultiFingerTaps( - builder: GestureDescription.Builder, - cx: Float, - cy: Float, - spacing: Float, - fingerCount: Int, - tapCount: Int, - holdDuration: Long, - ) { - val positions = fingerPositions(cx, cy, spacing, fingerCount) - val tapInterval = 200L - for (tap in 0 until tapCount) { - val startTime = tap * tapInterval - for ((x, y) in positions) { - val path = Path().apply { moveTo(x, y) } - builder.addStroke(StrokeDescription(path, startTime, holdDuration)) - } - } - } - - private fun addMultiFingerDoubleTapHold( - builder: GestureDescription.Builder, - cx: Float, - cy: Float, - spacing: Float, - fingerCount: Int, - ) { - val positions = fingerPositions(cx, cy, spacing, fingerCount) - for ((x, y) in positions) { - val path = Path().apply { moveTo(x, y) } - builder.addStroke(StrokeDescription(path, 0, 50)) - builder.addStroke(StrokeDescription(path, 200, 600)) - } - } - - private fun addMultiFingerTripleTapHold( - builder: GestureDescription.Builder, - cx: Float, - cy: Float, - spacing: Float, - fingerCount: Int, - ) { - val positions = fingerPositions(cx, cy, spacing, fingerCount) - for ((x, y) in positions) { - val path = Path().apply { moveTo(x, y) } - builder.addStroke(StrokeDescription(path, 0, 50)) - builder.addStroke(StrokeDescription(path, 200, 50)) - builder.addStroke(StrokeDescription(path, 400, 600)) - } - } - - private fun addMultiFingerSwipe( - builder: GestureDescription.Builder, - xStart: Float, - yStart: Float, - xEnd: Float, - yEnd: Float, - spacing: Float, - fingerCount: Int, - ) { - val startPositions = fingerPositions(xStart, yStart, spacing, fingerCount) - val endPositions = fingerPositions(xEnd, yEnd, spacing, fingerCount) - for (i in 0 until fingerCount) { - val (sx, sy) = startPositions[i] - val (ex, ey) = endPositions[i] - builder.addStroke(StrokeDescription(buildPath(sx, sy, ex, ey), 0, 200)) - } + return TalkbackGesturePerformer.performTalkBackGesture(this, gesture, gestureHandler) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt index 4ceeb3b23a..df4cd6c0d9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt @@ -2,7 +2,7 @@ package io.github.sds100.keymapper.base.system.accessibility import android.os.Build import androidx.annotation.RequiresApi -import io.github.sds100.keymapper.base.actions.TalkBackGestureType +import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType import io.github.sds100.keymapper.base.system.inputmethod.SwitchImeInterface import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMResult From 39805789c651b9cac94b90af7c465f56a6f4c9d7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 29 May 2026 16:51:20 +0200 Subject: [PATCH 4/4] #262 use custom dialog to select talkback gesture action --- .../base/actions/ChooseActionScreen.kt | 2 + .../base/actions/CreateActionDelegate.kt | 28 ++- .../talkback/PickTalkBackGestureDialog.kt | 184 ++++++++++++++++++ base/src/main/res/values/strings.xml | 4 + 4 files changed, 203 insertions(+), 15 deletions(-) create mode 100644 base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/PickTalkBackGestureDialog.kt diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt index 3ad37eb413..39a36e39af 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.actions.talkback.PickTalkBackGestureDialog import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.base.utils.ui.compose.SearchAppBarActions @@ -59,6 +60,7 @@ fun HandleActionBottomSheets(delegate: CreateActionDelegate) { ModifySettingActionBottomSheet(delegate) CreateNotificationActionBottomSheet(delegate) ToastActionBottomSheet(delegate) + PickTalkBackGestureDialog(delegate) } @Composable diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt index 9cedfd1ec2..9776d3073e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/CreateActionDelegate.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.snapshotFlow import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.pinchscreen.PinchPickCoordinateResult import io.github.sds100.keymapper.base.actions.swipescreen.SwipePickCoordinateResult -import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureStrings +import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureDialogState import io.github.sds100.keymapper.base.actions.talkback.TalkBackGestureType import io.github.sds100.keymapper.base.actions.tapscreen.PickCoordinateResult import io.github.sds100.keymapper.base.system.intents.ConfigIntentResult @@ -63,6 +63,7 @@ class CreateActionDelegate( var httpRequestBottomSheetState: ActionData.HttpRequest? by mutableStateOf(null) var smsActionBottomSheetState: SmsActionBottomSheetState? by mutableStateOf(null) var volumeActionState: VolumeActionBottomSheetState? by mutableStateOf(null) + var talkBackGestureDialogState: TalkBackGestureDialogState? by mutableStateOf(null) var modifySettingActionBottomSheetState: ModifySettingActionBottomSheetState? by mutableStateOf(null) var createNotificationActionBottomSheetState: CreateNotificationActionBottomSheetState? @@ -204,6 +205,13 @@ class CreateActionDelegate( actionResult.update { action } } + fun onDoneConfigTalkBackGestureClick() { + talkBackGestureDialogState?.also { state -> + talkBackGestureDialogState = null + actionResult.update { ActionData.TalkBackGesture(state.selectedGesture) } + } + } + fun onDoneConfigVolumeClick() { volumeActionState?.also { state -> val action = when (state.actionId) { @@ -1217,20 +1225,10 @@ class CreateActionDelegate( } 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) + val initialGesture = (oldData as? ActionData.TalkBackGesture)?.gesture + ?: TalkBackGestureType.entries.first() + talkBackGestureDialogState = TalkBackGestureDialogState(initialGesture) + return null } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/PickTalkBackGestureDialog.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/PickTalkBackGestureDialog.kt new file mode 100644 index 0000000000..47dcd83d72 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/PickTalkBackGestureDialog.kt @@ -0,0 +1,184 @@ +package io.github.sds100.keymapper.base.actions.talkback + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.actions.CreateActionDelegate +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.CustomDialog + +data class TalkBackGestureDialogState(val selectedGesture: TalkBackGestureType) + +@Composable +fun PickTalkBackGestureDialog(delegate: CreateActionDelegate) { + val state = delegate.talkBackGestureDialogState ?: return + + var selected by remember(state) { mutableStateOf(state.selectedGesture) } + + PickTalkBackGestureDialog( + selected = selected, + onSelectGesture = { selected = it }, + onDismissRequest = { delegate.talkBackGestureDialogState = null }, + onConfirm = { + delegate.talkBackGestureDialogState = + delegate.talkBackGestureDialogState?.copy(selectedGesture = selected) + delegate.onDoneConfigTalkBackGestureClick() + }, + ) +} + +@Composable +private fun PickTalkBackGestureDialog( + selected: TalkBackGestureType, + onSelectGesture: (TalkBackGestureType) -> Unit, + onDismissRequest: () -> Unit, + onConfirm: () -> Unit, +) { + val groups = remember { + listOf( + R.string.talkback_gesture_section_1_finger to listOf( + TalkBackGestureType.SWIPE_UP, + TalkBackGestureType.SWIPE_DOWN, + TalkBackGestureType.SWIPE_LEFT, + TalkBackGestureType.SWIPE_RIGHT, + TalkBackGestureType.SWIPE_UP_THEN_DOWN, + TalkBackGestureType.SWIPE_DOWN_THEN_UP, + TalkBackGestureType.SWIPE_LEFT_THEN_RIGHT, + TalkBackGestureType.SWIPE_RIGHT_THEN_LEFT, + TalkBackGestureType.SWIPE_RIGHT_THEN_UP, + ), + R.string.talkback_gesture_section_2_finger to listOf( + TalkBackGestureType.TWO_FINGER_TAP, + TalkBackGestureType.TWO_FINGER_DOUBLE_TAP_HOLD, + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP, + TalkBackGestureType.TWO_FINGER_TRIPLE_TAP_HOLD, + ), + R.string.talkback_gesture_section_3_finger to listOf( + TalkBackGestureType.THREE_FINGER_TAP, + TalkBackGestureType.THREE_FINGER_TAP_HOLD, + TalkBackGestureType.THREE_FINGER_TRIPLE_TAP_HOLD, + TalkBackGestureType.THREE_FINGER_SWIPE_UP, + TalkBackGestureType.THREE_FINGER_SWIPE_DOWN, + ), + R.string.talkback_gesture_section_4_finger to listOf( + TalkBackGestureType.FOUR_FINGER_TAP, + TalkBackGestureType.FOUR_FINGER_DOUBLE_TAP, + TalkBackGestureType.FOUR_FINGER_SWIPE_UP, + TalkBackGestureType.FOUR_FINGER_SWIPE_DOWN, + TalkBackGestureType.FOUR_FINGER_SWIPE_LEFT, + TalkBackGestureType.FOUR_FINGER_SWIPE_RIGHT, + ), + ) + } + + CustomDialog( + title = stringResource(R.string.action_talkback_gesture), + confirmButton = { + Button(onClick = onConfirm) { + Text(stringResource(R.string.pos_done)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.neg_cancel)) + } + }, + onDismissRequest = onDismissRequest, + ) { + LazyColumn { + for ((headerResId, gestures) in groups) { + stickyHeader(key = headerResId) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + text = stringResource(headerResId), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + } + } + items(gestures, key = { it.name }) { gesture -> + TalkBackGestureItem( + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), + gesture = gesture, + isSelected = selected == gesture, + onSelected = { onSelectGesture(gesture) }, + ) + } + } + } + } +} + +@Composable +private fun TalkBackGestureItem( + modifier: Modifier = Modifier, + gesture: TalkBackGestureType, + isSelected: Boolean, + onSelected: () -> Unit, +) { + Surface(modifier = modifier, shape = MaterialTheme.shapes.medium, color = Color.Transparent) { + Row( + modifier = Modifier + .clickable(onClick = onSelected) + .padding(horizontal = 16.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + RadioButton(selected = isSelected, onClick = null) + + Column(modifier = Modifier.padding(start = 8.dp)) { + Text( + text = stringResource(TalkBackGestureStrings.getActionLabel(gesture)), + style = MaterialTheme.typography.titleSmall, + ) + Text( + text = stringResource(TalkBackGestureStrings.getGestureLabel(gesture)), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Preview +@Composable +private fun PreviewPickTalkBackGestureDialog() { + KeyMapperTheme { + var selected by remember { mutableStateOf(TalkBackGestureType.SWIPE_UP) } + + PickTalkBackGestureDialog( + selected = selected, + onSelectGesture = { selected = it }, + onDismissRequest = {}, + onConfirm = {}, + ) + } +} diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 9f9a2c8ca8..ccf0be12c5 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1246,6 +1246,10 @@ TalkBack: %s Choose TalkBack gesture %1$s (%2$s) + 1-finger gestures + 2-finger gestures + 3-finger gestures + 4-finger gestures Move reading control up or backwards