From 9886cdaaa57de7b22150d3a2fd5e77b55cc55b7f Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 11 May 2026 00:02:12 +0100 Subject: [PATCH 01/17] write whats-new --- base/src/main/assets/whats-new.txt | 33 ++++-------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/base/src/main/assets/whats-new.txt b/base/src/main/assets/whats-new.txt index 3466af1254..fab11d04bb 100644 --- a/base/src/main/assets/whats-new.txt +++ b/base/src/main/assets/whats-new.txt @@ -1,34 +1,9 @@ -✨ Screen-off remapping -You can now remap ALL buttons when the screen is off (including the power button) for free with Expert mode. - 🎯 New Actions -• Run shell commands -• Send SMS messages -• Force stop current app or clear from recents -• Mute/unmute microphone -• Modify any system setting -• Show a custom notification -• Toggle hotspot -• Toggle night shift - -🆕 New Features -• Redesigned Settings screen -• Constraints for foldable hinge open/closed -• Shortcuts on the trigger screen to guide setup -• Select notification and alarm sounds for Sound action -• Constraints for keyboard is showing -• Switch next to record trigger button to use PRO mode - - -⚙️ Enhanced Controls -• Enable or disable all key maps in a group at once -• Intent API to enable, disable, or toggle individual key maps -• Floating buttons can now appear on top of keyboard or in notification panel -• Make floating buttons movable and completely invisible +• Select all text at the cursor +• Input on-screen keyboard enter/send button +• Show a toast message 🔧 Improvements -• Auto-switching keyboard more reliable and quicker on Android 13+ -• Wi-Fi connected constraints more reliable -• A lot of bug fixes +• Bug fixes 📖 View the complete changelog at: http://keymapper.app/changelog From 4030b0f4a63989d4ea8ac9cfaea8534ee0e0b99d Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 11 May 2026 00:18:05 +0100 Subject: [PATCH 02/17] bump version to 4.1.1 --- app/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.properties b/app/version.properties index f93e854d79..d45def2351 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.1.0 -VERSION_CODE=251 +VERSION_NAME=4.1.1 +VERSION_CODE=252 From 0dfaf05a711c0c416f3d631890b1d54e3d038670 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 15 May 2026 15:24:12 +0200 Subject: [PATCH 03/17] #2076 use any input device as the default for triggers Closes #2076 --- CHANGELOG.md | 7 +++++++ .../keymapper/base/trigger/BaseConfigTriggerViewModel.kt | 7 ++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad0e23a167..23d05d7c4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [4.1.1](https://github.com/sds100/KeyMapper/releases/tag/v4.1.1) + +#### 15 May 2026 + +- #2076 Use "any input device" as the default for triggers. + + ## [4.1.0](https://github.com/sds100/KeyMapper/releases/tag/v4.1.0) #### 10 May 2026 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index bb96f39815..60942a8d8b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -400,11 +400,8 @@ abstract class BaseConfigTriggerViewModel( } private suspend fun onRecordKeyEvent(key: RecordedKey.KeyEvent) { - val triggerDevice = if (key.isExternalDevice) { - KeyEventTriggerDevice.External(key.deviceDescriptor, key.deviceName) - } else { - KeyEventTriggerDevice.Internal - } + // See issue #2076 + val triggerDevice = KeyEventTriggerDevice.Any config.addKeyEventTriggerKey( key.keyCode, From 1273c74e263544560a9cf2988265d5bbedbddcf3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 15 May 2026 15:34:22 +0200 Subject: [PATCH 04/17] update email templates --- .../sds100/keymapper/base/utils/ShareUtils.kt | 13 +++++++++++++ base/src/main/res/values/strings.xml | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ShareUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ShareUtils.kt index d4e0bd5fb7..50d6704825 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ShareUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ShareUtils.kt @@ -12,8 +12,21 @@ import androidx.core.app.ShareCompat import androidx.core.net.toUri import io.github.sds100.keymapper.base.BaseMainActivity import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.utils.ui.str object ShareUtils { + fun sendRefundEmail(ctx: Context) { + val subject = ctx.str(R.string.customer_email_subject_refund) + val body = ctx.str(R.string.customer_email_body_refund) + + sendMail( + ctx, + email = ctx.getString(R.string.purchasing_contact_email), + subject = subject, + body = body, + ) + } + fun sendBugReportEmail(ctx: Context, subject: String) { val body = ctx.getString( R.string.customer_email_body, diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index a6cb758233..a081813537 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1381,6 +1381,7 @@ Email us Key Mapper Pro help + You can ask us any questions.\n\nAre you requesting a refund?\n\nPlease go to the Google Play Store app to find your payment receipt. Once you find the receipt, include the Transaction ID (starting with GPA) in this email.\n\nMy Transaction ID: Your support keeps Key Mapper alive Google Play reviewer Floating buttons is a game changer! @@ -1460,7 +1461,8 @@ You must purchase the floating buttons feature! Tap on the key map and then purchase it by clicking on the shop. contact@keymapper.app Key Mapper Pro query - Key Mapper refund query + Unsatisfied with Key Mapper + To request a refund, please go to the Google Play Store app to find your payment receipt. Once you find the receipt, include the Transaction ID (starting with GPA) in this email.\n\nMy Transaction ID: Key Mapper Bug report Please fill the following information so I can help you.\n\n1. Device model: %s\n2. Android version: %s\n3. Key Mapper version: %s\n4, Key maps (make a backup in the home screen menu)\n6. Screenshot of Key Mapper home screen\n6. Describe the problem you are having The advanced triggers are paid feature but you downloaded the FOSS build of Key Mapper that does not include this closed source module or Google Play billing. Please download Key Mapper from Google Play to get access to this feature. From 7e805ef1ca3e92d340c4a59c612ffa03ec7614be Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 00:22:13 +0000 Subject: [PATCH 05/17] #1369 feat: improve TalkBack accessibility for reorderable list items Add contentDescriptions to drag handle icons using the existing drag_handle_for string resource, and add custom accessibility actions ("Move up", "Move down") to trigger key and action list items so TalkBack users can reorder items without needing to perform drag gestures. --- CHANGELOG.md | 6 + .../keymapper/base/actions/ActionListItem.kt | 24 +++- .../keymapper/base/actions/ActionsScreen.kt | 10 ++ .../base/trigger/BaseTriggerScreen.kt | 10 ++ .../base/trigger/TriggerKeyListItem.kt | 126 ++++++++++-------- base/src/main/res/values/strings.xml | 2 + 6 files changed, 123 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d05d7c4f..3bbeb134f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +## Changed + +- #1369 Add content descriptions to drag handles and custom "Move up"/"Move down" accessibility actions for trigger and action list items, improving TalkBack support for reordering. + ## [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/ActionListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt index f842381aae..e4e00967d4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt @@ -39,6 +39,9 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -62,11 +65,16 @@ fun ActionListItem( onRemoveClick: () -> Unit = {}, onFixClick: () -> Unit = {}, onTestClick: () -> Unit = {}, + onMoveUp: (() -> Unit)? = null, + onMoveDown: (() -> Unit)? = null, ) { val draggableState = rememberDraggableState { dragDropState?.onDrag(Offset(0f, it)) } + val moveUpLabel = stringResource(R.string.accessibility_action_move_up) + val moveDownLabel = stringResource(R.string.accessibility_action_move_down) + Column(modifier = modifier.fillMaxWidth()) { ElevatedCard( modifier = Modifier @@ -83,7 +91,19 @@ fun ActionListItem( dragDropState?.onDragStart(index, offset) }, onDragStopped = { dragDropState?.onDragInterrupted() }, - ), + ) + .semantics { + if (isReorderingEnabled) { + customActions = buildList { + onMoveUp?.let { action -> + add(CustomAccessibilityAction(moveUpLabel) { action(); true }) + } + onMoveDown?.let { action -> + add(CustomAccessibilityAction(moveDownLabel) { action(); true }) + } + } + } + }, colors = CardDefaults.elevatedCardColors( containerColor = if (isDragging) { MaterialTheme.colorScheme.surfaceContainerHighest @@ -102,7 +122,7 @@ fun ActionListItem( Icon( modifier = Modifier.size(24.dp), imageVector = Icons.Rounded.DragHandle, - contentDescription = null, + contentDescription = stringResource(R.string.drag_handle_for, model.text), tint = MaterialTheme.colorScheme.onSurface, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt index b7fec30f2c..4198aa2287 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt @@ -314,6 +314,16 @@ private fun ActionList( onRemoveClick = { onRemoveClick(model.id) }, onFixClick = { onFixErrorClick(model.id) }, onTestClick = { onTestClick(model.id) }, + onMoveUp = if (isReorderingEnabled && index > 0) { + { onMove(index, index - 1) } + } else { + null + }, + onMoveDown = if (isReorderingEnabled && index < actionList.size - 1) { + { onMove(index, index + 1) } + } else { + null + }, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index 735ae05da8..2415f99184 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -482,6 +482,16 @@ private fun TriggerList( onEditClick = { onEditClick(model.id) }, onRemoveClick = { onRemoveClick(model.id) }, onFixClick = onFixErrorClick, + onMoveUp = if (isReorderingEnabled && index > 0) { + { onMove(index, index - 1) } + } else { + null + }, + onMoveDown = if (isReorderingEnabled && index < triggerList.size - 1) { + { onMove(index, index + 1) } + } else { + null + }, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index 8053721ef3..a1e4ee66a4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -39,6 +39,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -60,11 +63,67 @@ fun TriggerKeyListItem( onEditClick: () -> Unit = {}, onRemoveClick: () -> Unit = {}, onFixClick: (TriggerError) -> Unit = {}, + onMoveUp: (() -> Unit)? = null, + onMoveDown: (() -> Unit)? = null, ) { val draggableState = rememberDraggableState { dragDropState?.onDrag(Offset(0f, it)) } + val primaryText = when (model) { + is TriggerKeyListItemModel.Assistant -> when (model.assistantType) { + AssistantTriggerType.ANY -> stringResource( + R.string.assistant_any_trigger_name, + ) + + AssistantTriggerType.VOICE -> stringResource( + R.string.assistant_voice_trigger_name, + ) + + AssistantTriggerType.DEVICE -> stringResource( + R.string.assistant_device_trigger_name, + ) + } + + is TriggerKeyListItemModel.FloatingButton -> if (model.buttonName.isBlank()) { + stringResource(R.string.trigger_key_floating_button_description_empty) + } else { + stringResource( + R.string.trigger_key_floating_button_description, + model.buttonName, + ) + } + + is TriggerKeyListItemModel.KeyEvent -> model.keyName + + is TriggerKeyListItemModel.EvdevEvent -> model.keyName + + is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource( + R.string.trigger_error_floating_button_deleted_title, + ) + + is TriggerKeyListItemModel.FingerprintGesture -> when (model.gestureType) { + FingerprintGestureType.SWIPE_UP -> stringResource( + R.string.trigger_key_fingerprint_gesture_up, + ) + + FingerprintGestureType.SWIPE_DOWN -> stringResource( + R.string.trigger_key_fingerprint_gesture_down, + ) + + FingerprintGestureType.SWIPE_LEFT -> stringResource( + R.string.trigger_key_fingerprint_gesture_left, + ) + + FingerprintGestureType.SWIPE_RIGHT -> stringResource( + R.string.trigger_key_fingerprint_gesture_right, + ) + } + } + + val moveUpLabel = stringResource(R.string.accessibility_action_move_up) + val moveDownLabel = stringResource(R.string.accessibility_action_move_down) + Column(modifier = modifier.fillMaxWidth()) { ElevatedCard( modifier = Modifier @@ -81,7 +140,19 @@ fun TriggerKeyListItem( dragDropState?.onDragStart(index, offset) }, onDragStopped = { dragDropState?.onDragInterrupted() }, - ), + ) + .semantics { + if (isReorderingEnabled) { + customActions = buildList { + onMoveUp?.let { action -> + add(CustomAccessibilityAction(moveUpLabel) { action(); true }) + } + onMoveDown?.let { action -> + add(CustomAccessibilityAction(moveDownLabel) { action(); true }) + } + } + } + }, colors = CardDefaults.elevatedCardColors( containerColor = if (isDragging) { MaterialTheme.colorScheme.surfaceContainerHighest @@ -100,7 +171,7 @@ fun TriggerKeyListItem( Icon( modifier = Modifier.size(24.dp), imageVector = Icons.Rounded.DragHandle, - contentDescription = null, + contentDescription = stringResource(R.string.drag_handle_for, primaryText), tint = MaterialTheme.colorScheme.onSurface, ) } @@ -127,57 +198,6 @@ fun TriggerKeyListItem( } } - val primaryText = when (model) { - is TriggerKeyListItemModel.Assistant -> when (model.assistantType) { - AssistantTriggerType.ANY -> stringResource( - R.string.assistant_any_trigger_name, - ) - - AssistantTriggerType.VOICE -> stringResource( - R.string.assistant_voice_trigger_name, - ) - - AssistantTriggerType.DEVICE -> stringResource( - R.string.assistant_device_trigger_name, - ) - } - - is TriggerKeyListItemModel.FloatingButton -> if (model.buttonName.isBlank()) { - stringResource(R.string.trigger_key_floating_button_description_empty) - } else { - stringResource( - R.string.trigger_key_floating_button_description, - model.buttonName, - ) - } - - is TriggerKeyListItemModel.KeyEvent -> model.keyName - - is TriggerKeyListItemModel.EvdevEvent -> model.keyName - - is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource( - R.string.trigger_error_floating_button_deleted_title, - ) - - is TriggerKeyListItemModel.FingerprintGesture -> when (model.gestureType) { - FingerprintGestureType.SWIPE_UP -> stringResource( - R.string.trigger_key_fingerprint_gesture_up, - ) - - FingerprintGestureType.SWIPE_DOWN -> stringResource( - R.string.trigger_key_fingerprint_gesture_down, - ) - - FingerprintGestureType.SWIPE_LEFT -> stringResource( - R.string.trigger_key_fingerprint_gesture_left, - ) - - FingerprintGestureType.SWIPE_RIGHT -> stringResource( - R.string.trigger_key_fingerprint_gesture_right, - ) - } - } - Spacer(Modifier.width(8.dp)) if (model.error == null) { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index a081813537..c7ff462120 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -486,6 +486,8 @@ Drag the handles to adjust priorities. The item at the top is the most important. You must tap the item to enable sorting and toggle ascending/descending. Example: To sort by Actions (ascending) first and Triggers (descending) second: tap and drag Actions to the first position and set it to ascending, then tap and drag Triggers to the second position and set it to descending. Drag handle for %1$s + Move up + Move down Show example Turn on notifications From c2eed0af5a0ef46389eb297ee8feebca914fd4f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 00:38:40 +0000 Subject: [PATCH 06/17] #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 b27fbe43fd3234e6968e55e7476b3247b899ac6b Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 29 May 2026 15:31:51 +0200 Subject: [PATCH 07/17] #1369 reformat and move code --- .../keymapper/base/actions/ActionListItem.kt | 14 ++- .../base/trigger/TriggerKeyListItem.kt | 117 ++++++++++-------- 2 files changed, 77 insertions(+), 54 deletions(-) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt index e4e00967d4..55b451082c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt @@ -96,10 +96,20 @@ fun ActionListItem( if (isReorderingEnabled) { customActions = buildList { onMoveUp?.let { action -> - add(CustomAccessibilityAction(moveUpLabel) { action(); true }) + add( + CustomAccessibilityAction(moveUpLabel) { + action() + true + }, + ) } onMoveDown?.let { action -> - add(CustomAccessibilityAction(moveDownLabel) { action(); true }) + add( + CustomAccessibilityAction(moveDownLabel) { + action() + true + }, + ) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index a1e4ee66a4..854f145664 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -70,56 +70,7 @@ fun TriggerKeyListItem( dragDropState?.onDrag(Offset(0f, it)) } - val primaryText = when (model) { - is TriggerKeyListItemModel.Assistant -> when (model.assistantType) { - AssistantTriggerType.ANY -> stringResource( - R.string.assistant_any_trigger_name, - ) - - AssistantTriggerType.VOICE -> stringResource( - R.string.assistant_voice_trigger_name, - ) - - AssistantTriggerType.DEVICE -> stringResource( - R.string.assistant_device_trigger_name, - ) - } - - is TriggerKeyListItemModel.FloatingButton -> if (model.buttonName.isBlank()) { - stringResource(R.string.trigger_key_floating_button_description_empty) - } else { - stringResource( - R.string.trigger_key_floating_button_description, - model.buttonName, - ) - } - - is TriggerKeyListItemModel.KeyEvent -> model.keyName - - is TriggerKeyListItemModel.EvdevEvent -> model.keyName - - is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource( - R.string.trigger_error_floating_button_deleted_title, - ) - - is TriggerKeyListItemModel.FingerprintGesture -> when (model.gestureType) { - FingerprintGestureType.SWIPE_UP -> stringResource( - R.string.trigger_key_fingerprint_gesture_up, - ) - - FingerprintGestureType.SWIPE_DOWN -> stringResource( - R.string.trigger_key_fingerprint_gesture_down, - ) - - FingerprintGestureType.SWIPE_LEFT -> stringResource( - R.string.trigger_key_fingerprint_gesture_left, - ) - - FingerprintGestureType.SWIPE_RIGHT -> stringResource( - R.string.trigger_key_fingerprint_gesture_right, - ) - } - } + val primaryText = getPrimaryText(model) val moveUpLabel = stringResource(R.string.accessibility_action_move_up) val moveDownLabel = stringResource(R.string.accessibility_action_move_down) @@ -145,10 +96,20 @@ fun TriggerKeyListItem( if (isReorderingEnabled) { customActions = buildList { onMoveUp?.let { action -> - add(CustomAccessibilityAction(moveUpLabel) { action(); true }) + add( + CustomAccessibilityAction(moveUpLabel) { + action() + true + }, + ) } onMoveDown?.let { action -> - add(CustomAccessibilityAction(moveDownLabel) { action(); true }) + add( + CustomAccessibilityAction(moveDownLabel) { + action() + true + }, + ) } } } @@ -302,6 +263,58 @@ fun TriggerKeyListItem( } } +@Composable +private fun getPrimaryText(model: TriggerKeyListItemModel): String = when (model) { + is TriggerKeyListItemModel.Assistant -> when (model.assistantType) { + AssistantTriggerType.ANY -> stringResource( + R.string.assistant_any_trigger_name, + ) + + AssistantTriggerType.VOICE -> stringResource( + R.string.assistant_voice_trigger_name, + ) + + AssistantTriggerType.DEVICE -> stringResource( + R.string.assistant_device_trigger_name, + ) + } + + is TriggerKeyListItemModel.FloatingButton -> if (model.buttonName.isBlank()) { + stringResource(R.string.trigger_key_floating_button_description_empty) + } else { + stringResource( + R.string.trigger_key_floating_button_description, + model.buttonName, + ) + } + + is TriggerKeyListItemModel.KeyEvent -> model.keyName + + is TriggerKeyListItemModel.EvdevEvent -> model.keyName + + is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource( + R.string.trigger_error_floating_button_deleted_title, + ) + + is TriggerKeyListItemModel.FingerprintGesture -> when (model.gestureType) { + FingerprintGestureType.SWIPE_UP -> stringResource( + R.string.trigger_key_fingerprint_gesture_up, + ) + + FingerprintGestureType.SWIPE_DOWN -> stringResource( + R.string.trigger_key_fingerprint_gesture_down, + ) + + FingerprintGestureType.SWIPE_LEFT -> stringResource( + R.string.trigger_key_fingerprint_gesture_left, + ) + + FingerprintGestureType.SWIPE_RIGHT -> stringResource( + R.string.trigger_key_fingerprint_gesture_right, + ) + } +} + @Composable private fun getErrorMessage(error: TriggerError): String { return when (error) { From e1a2c852e4ffb19592246fe67cf49e85eeb8cb09 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 29 May 2026 15:38:17 +0200 Subject: [PATCH 08/17] #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 09/17] #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 10/17] #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 From 248907c1488ca31ee4c719e309749c3512d5f3ab Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 00:16:01 +0000 Subject: [PATCH 11/17] #2140 feat: add monochrome app icon for themed icon support on Android 13+ Adds ic_launcher_monochrome.xml using evenOdd fill paths derived from the existing foreground icon so the DPad and keyboard shapes are recognisable in a single colour. Updates all adaptive-icon XML files (release, debug, ci) to reference the new monochrome layer. --- CHANGELOG.md | 4 ++++ .../ci/res/mipmap-anydpi-v26/ic_launcher.xml | 1 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 1 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 1 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 1 + .../res/drawable/ic_launcher_monochrome.xml | 24 +++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 1 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 1 + 8 files changed, 34 insertions(+) create mode 100644 base/src/main/res/drawable/ic_launcher_monochrome.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index c0872a3650..f848f8fed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +## Added + +- #2140 Add monochrome app icon layer for themed icon support on Android 13+. + ## Changed - #1369 Add content descriptions to drag handles and custom "Move up"/"Move down" accessibility actions for trigger and action list items, improving TalkBack support for reordering. diff --git a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd1fd..1084c24082 100644 --- a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd1fd..1084c24082 100644 --- a/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/ci/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd1fd..1084c24082 100644 --- a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd1fd..1084c24082 100644 --- a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/base/src/main/res/drawable/ic_launcher_monochrome.xml b/base/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000000..228496484e --- /dev/null +++ b/base/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd1fd..1084c24082 100644 --- a/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/base/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd1fd..1084c24082 100644 --- a/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/base/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file From f28691a68aa2a2c927dab156e52b021199f199da Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 00:18:11 +0000 Subject: [PATCH 12/17] #2074 fix: scope item drag to handle only, restoring list scroll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the .draggable() modifier was applied to the entire ElevatedCard, causing any vertical swipe on an item to start a reorder instead of scrolling the parent LazyColumn. Moving the modifier to the DragHandle icon means: - touch anywhere on the card body → list scrolls normally - touch the drag handle and swipe → item reorders immediately - long-press anywhere on the list → item reorders (unchanged via dragContainer) --- CHANGELOG.md | 4 ++++ .../keymapper/base/actions/ActionListItem.kt | 23 ++++++++++--------- .../base/trigger/TriggerKeyListItem.kt | 23 ++++++++++--------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0872a3650..01172559fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +## Fixed + +- #2074 Scrolling the action or trigger list no longer accidentally moves items; reordering by drag now only activates from the drag handle or via long-press. + ## Changed - #1369 Add content descriptions to drag handles and custom "Move up"/"Move down" accessibility actions for trigger and action list items, improving TalkBack support for reordering. diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt index 55b451082c..c8493d9604 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt @@ -82,16 +82,6 @@ fun ActionListItem( .heightIn(min = 48.dp) .height(IntrinsicSize.Min) .padding(start = 16.dp, end = 16.dp) - .draggable( - state = draggableState, - enabled = isDraggingEnabled, - orientation = Orientation.Vertical, - startDragImmediately = false, - onDragStarted = { offset -> - dragDropState?.onDragStart(index, offset) - }, - onDragStopped = { dragDropState?.onDragInterrupted() }, - ) .semantics { if (isReorderingEnabled) { customActions = buildList { @@ -130,7 +120,18 @@ fun ActionListItem( if (isReorderingEnabled) { Icon( - modifier = Modifier.size(24.dp), + modifier = Modifier + .size(24.dp) + .draggable( + state = draggableState, + enabled = isDraggingEnabled, + orientation = Orientation.Vertical, + startDragImmediately = true, + onDragStarted = { offset -> + dragDropState?.onDragStart(index, offset) + }, + onDragStopped = { dragDropState?.onDragInterrupted() }, + ), imageVector = Icons.Rounded.DragHandle, contentDescription = stringResource(R.string.drag_handle_for, model.text), tint = MaterialTheme.colorScheme.onSurface, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index 854f145664..6370ac90b2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -82,16 +82,6 @@ fun TriggerKeyListItem( .heightIn(min = 48.dp) .height(IntrinsicSize.Min) .padding(start = 16.dp, end = 16.dp) - .draggable( - state = draggableState, - enabled = isDraggingEnabled, - orientation = Orientation.Vertical, - startDragImmediately = false, - onDragStarted = { offset -> - dragDropState?.onDragStart(index, offset) - }, - onDragStopped = { dragDropState?.onDragInterrupted() }, - ) .semantics { if (isReorderingEnabled) { customActions = buildList { @@ -130,7 +120,18 @@ fun TriggerKeyListItem( if (isReorderingEnabled) { Icon( - modifier = Modifier.size(24.dp), + modifier = Modifier + .size(24.dp) + .draggable( + state = draggableState, + enabled = isDraggingEnabled, + orientation = Orientation.Vertical, + startDragImmediately = true, + onDragStarted = { offset -> + dragDropState?.onDragStart(index, offset) + }, + onDragStopped = { dragDropState?.onDragInterrupted() }, + ), imageVector = Icons.Rounded.DragHandle, contentDescription = stringResource(R.string.drag_handle_for, primaryText), tint = MaterialTheme.colorScheme.onSurface, From b83e893f9ebe77d766591d406b37cb2c6462f3dd Mon Sep 17 00:00:00 2001 From: sds100 Date: Mon, 1 Jun 2026 16:19:02 +0200 Subject: [PATCH 13/17] bump version to 4.2.0 and update changelog --- CHANGELOG.md | 7 +------ app/version.properties | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0872a3650..f3e7173f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,9 @@ -## Unreleased +## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0) ## Changed - #1369 Add content descriptions to drag handles and custom "Move up"/"Move down" accessibility actions for trigger and action list items, improving TalkBack support for reordering. - #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 - - #2076 Use "any input device" as the default for triggers. diff --git a/app/version.properties b/app/version.properties index d45def2351..0fa4ca5276 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ -VERSION_NAME=4.1.1 +VERSION_NAME=4.2.0 VERSION_CODE=252 From d26633e702ebe544ec63c5e232d67a09501ef920 Mon Sep 17 00:00:00 2001 From: Jack Ambler <54366245+jambl3r@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:09:53 +0200 Subject: [PATCH 14/17] fix: Support email string formatting fix --- base/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 3fe19c6b92..e6561e8dc0 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -1526,7 +1526,7 @@ Unsatisfied with Key Mapper To request a refund, please go to the Google Play Store app to find your payment receipt. Once you find the receipt, include the Transaction ID (starting with GPA) in this email.\n\nMy Transaction ID: Key Mapper Bug report - Please fill the following information so I can help you.\n\n1. Device model: %s\n2. Android version: %s\n3. Key Mapper version: %s\n4, Key maps (make a backup in the home screen menu)\n6. Screenshot of Key Mapper home screen\n6. Describe the problem you are having + Please fill the following information so I can help you.\n\n1. Device model: %s\n2. Android version: %s\n3. Key Mapper version: %s\n4. Key maps (make a backup in the home screen menu)\n5. Screenshot of Key Mapper home screen\n6. Describe the problem you are having The advanced triggers are paid feature but you downloaded the FOSS build of Key Mapper that does not include this closed source module or Google Play billing. Please download Key Mapper from Google Play to get access to this feature. Key Mapper FOSS support Floating Buttons and Assistant trigger aren\'t sold in this FOSS build because RevenueCat and Google Play in-app billing aren\'t included.\n\nYou can still support development on Ko-fi. From 43c38b0ae60b38e2420231ad610c7854702c88c4 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 3 Jun 2026 18:41:11 +0200 Subject: [PATCH 15/17] use correct monochrome foreground icon --- .../res/drawable/ic_launcher_monochrome.xml | 74 ++++++++++++++----- 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/base/src/main/res/drawable/ic_launcher_monochrome.xml b/base/src/main/res/drawable/ic_launcher_monochrome.xml index 228496484e..be1d8157f9 100644 --- a/base/src/main/res/drawable/ic_launcher_monochrome.xml +++ b/base/src/main/res/drawable/ic_launcher_monochrome.xml @@ -1,24 +1,58 @@ - - - - - + android:viewportWidth="24" + android:viewportHeight="24"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 801b7145b9106d4dae307e84c55c6e3970b047d7 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 3 Jun 2026 19:50:48 +0200 Subject: [PATCH 16/17] bump version code --- app/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.properties b/app/version.properties index 0fa4ca5276..1d239ae17f 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,2 +1,2 @@ VERSION_NAME=4.2.0 -VERSION_CODE=252 +VERSION_CODE=253 From 7b260c4a80c119a8f7b59c2e624ab04e90799c16 Mon Sep 17 00:00:00 2001 From: sds100 Date: Wed, 3 Jun 2026 19:51:07 +0200 Subject: [PATCH 17/17] add changelog release date --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe73d8616..b4ca051484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [4.2.0](https://github.com/sds100/KeyMapper/releases/tag/v4.2.0) +#### 03 June 2026 + ## Added - #2140 Add monochrome app icon layer for themed icon support on Android 13+.