diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad0e23a167..b4ca051484 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,22 @@
+## [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+.
+
+## 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.
+- #262 Add "TalkBack gesture" action to simulate TalkBack navigation gestures (swipes, multi-finger taps, and multi-directional swipes).
+- #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/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/app/version.properties b/app/version.properties
index f93e854d79..1d239ae17f 100644
--- a/app/version.properties
+++ b/app/version.properties
@@ -1,2 +1,2 @@
-VERSION_NAME=4.1.0
-VERSION_CODE=251
+VERSION_NAME=4.2.0
+VERSION_CODE=253
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
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..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,17 @@ sealed class ActionData : Comparable {
{ it.settingKey },
{ it.value },
)
+
+ 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..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,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.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
@@ -69,6 +70,7 @@ object ActionDataEntityMapper {
ActionEntity.Type.MODIFY_SETTING -> ActionId.MODIFY_SETTING
ActionEntity.Type.CREATE_NOTIFICATION -> ActionId.CREATE_NOTIFICATION
+
ActionEntity.Type.TOAST -> ActionId.TOAST
}
@@ -874,6 +876,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 +1340,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 +1530,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/ActionListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt
index f842381aae..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
@@ -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
@@ -74,16 +82,28 @@ 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 {
+ 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,9 +120,20 @@ 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 = 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/ActionUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt
index fe8323f74d..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,6 +5,7 @@ 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
@@ -209,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
}
@@ -235,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
}
@@ -448,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,
@@ -462,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 -> {
@@ -522,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,
)
@@ -542,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,
)
@@ -560,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 -> {
@@ -583,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) {
@@ -647,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),
@@ -659,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 -> {
@@ -681,11 +772,17 @@ 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)
}
}
}
+
+ 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) {
@@ -743,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,
@@ -764,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/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/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/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 0f125641dc..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,6 +8,8 @@ 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.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
import io.github.sds100.keymapper.base.utils.DndModeStrings
@@ -61,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?
@@ -202,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) {
@@ -1213,6 +1223,13 @@ class CreateActionDelegate(
return null
}
+
+ ActionId.TALKBACK_GESTURE -> {
+ 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/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/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/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureStrings.kt
new file mode 100644
index 0000000000..e6956c4f69
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureStrings.kt
@@ -0,0 +1,153 @@
+package io.github.sds100.keymapper.base.actions.talkback
+
+import io.github.sds100.keymapper.base.R
+
+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/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureType.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureType.kt
new file mode 100644
index 0000000000..db81147d59
--- /dev/null
+++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/talkback/TalkBackGestureType.kt
@@ -0,0 +1,42 @@
+package io.github.sds100.keymapper.base.actions.talkback
+
+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,
+
+ // 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.
+}
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 c1d5991a29..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,6 +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.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
@@ -561,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,
@@ -570,7 +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<*> {
+ 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 7ae1761d8c..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,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.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
@@ -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/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,
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..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
@@ -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,18 @@ 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 = getPrimaryText(model)
+
+ 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
@@ -72,16 +82,28 @@ 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 {
+ 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
@@ -98,9 +120,20 @@ 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 = null,
+ contentDescription = stringResource(R.string.drag_handle_for, primaryText),
tint = MaterialTheme.colorScheme.onSurface,
)
}
@@ -127,57 +160,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) {
@@ -282,6 +264,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) {
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/drawable/ic_launcher_monochrome.xml b/base/src/main/res/drawable/ic_launcher_monochrome.xml
new file mode 100644
index 0000000000..be1d8157f9
--- /dev/null
+++ b/base/src/main/res/drawable/ic_launcher_monochrome.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml
index a6cb758233..e6561e8dc0 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
@@ -1242,6 +1244,66 @@
Force stop app
Close and clear app from recents
Modify setting
+ TalkBack gesture
+ 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
+ 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
@@ -1381,6 +1443,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,9 +1523,10 @@
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
+ 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.
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